<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Archana</title>
    <description>The latest articles on DEV Community by Archana (@chanadev).</description>
    <link>https://dev.to/chanadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3881394%2Ff285cc75-1bf0-4174-862f-c6c7754f30ee.jpg</url>
      <title>DEV Community: Archana</title>
      <link>https://dev.to/chanadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chanadev"/>
    <language>en</language>
    <item>
      <title>Ten files is not a budget</title>
      <dc:creator>Archana</dc:creator>
      <pubDate>Fri, 11 Sep 2026 01:28:04 +0000</pubDate>
      <link>https://dev.to/chanadev/ten-files-is-not-a-budget-4ih5</link>
      <guid>https://dev.to/chanadev/ten-files-is-not-a-budget-4ih5</guid>
      <description>&lt;p&gt;I have an MCP server that digests a GitHub repository into markdown so a model can read it without cloning anything. Directory tree, plus the contents of the files that matter.&lt;/p&gt;

&lt;p&gt;I pointed it at a real repository and got this back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: result (109,668 characters) exceeds maximum allowed tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Not a truncated answer. Not a partial digest with a warning. Nothing. The tool call failed and the caller was left with an error message where a repository summary should have been.&lt;/p&gt;

&lt;p&gt;The cause was a constant I had written months earlier without thinking about it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MAX_FILE_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32_000&lt;/span&gt;
&lt;span class="n"&gt;TOP_N_FILES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Ten files, each capped at 32,000 characters. I had reasoned that ten files is a reasonable amount of a repository to show. Which is true, and also not a size limit at all. Ten files is anywhere from a few hundred characters to 320,000 depending on whose repository you point it at.&lt;/p&gt;
&lt;h2&gt;
  
  
  The obvious fix, and why I did not just do it
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to add a total character cap. Pick a number, fill it, stop.&lt;/p&gt;

&lt;p&gt;The problem is picking the number. I could have reasoned my way to one. 50,000 sounds fine. 32,000 sounds fine too. So does 64,000. Any of them sounds fine, which is a good sign that reasoning is not the tool for the job, and is exactly how &lt;code&gt;TOP_N_FILES = 10&lt;/code&gt; got there in the first place.&lt;/p&gt;

&lt;p&gt;So before changing anything I measured. Six repositories, spanning the range I would realistically encounter: two small ones of my own, a mid-sized MCP server, a docs-heavy list repo, and two large real codebases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repository&lt;/th&gt;
&lt;th&gt;files&lt;/th&gt;
&lt;th&gt;tree&lt;/th&gt;
&lt;th&gt;file content&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GopherMCP/GopherCache&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;4,505&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,594&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pyarchana/gopher&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;591&lt;/td&gt;
&lt;td&gt;23,640&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24,231&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sktime/sktime-mcp&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;3,296&lt;/td&gt;
&lt;td&gt;100,490&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;103,786&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;astral-sh/uv&lt;/td&gt;
&lt;td&gt;1,578&lt;/td&gt;
&lt;td&gt;65,530&lt;/td&gt;
&lt;td&gt;142,337&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;207,867&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;modelcontextprotocol/python-sdk&lt;/td&gt;
&lt;td&gt;1,643&lt;/td&gt;
&lt;td&gt;57,306&lt;/td&gt;
&lt;td&gt;160,182&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217,488&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;punkpeye/awesome-mcp-servers&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;td&gt;228,137&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;228,310&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fifty-fold range between the smallest and largest output, from the same tool with the same settings.&lt;/p&gt;

&lt;p&gt;Three things fell out of that table that I would not have guessed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tree alone can eat everything
&lt;/h2&gt;

&lt;p&gt;Look at the tree column for uv and the python SDK. 65,530 and 57,306 characters, just to list filenames.&lt;/p&gt;

&lt;p&gt;I had been thinking of the budget as a cap on file contents. But on a repository with 1,600 files, the directory listing on its own is larger than any sensible total. If I had capped only the file content, uv would have blown past any budget I set before fetching a single file.&lt;/p&gt;

&lt;p&gt;The tree needed its own cap, at a fraction of the total, and a note saying how many entries it left out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The ranking was picking the wrong files
&lt;/h2&gt;

&lt;p&gt;This is the one that actually mattered, and I only saw it because I printed what was being selected rather than just how big it was.&lt;/p&gt;

&lt;p&gt;Here is what my tool chose to show about &lt;strong&gt;uv&lt;/strong&gt;, a Rust build tool:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;27,194  test/ecosystem/airflow/pyproject.toml
32,048  test/ecosystem/home-assistant-core/pyproject.toml
27,517  test/ecosystem/pandas/pyproject.toml
 9,156  test/ecosystem/jupyterlab/pyproject.toml
 8,757  test/ecosystem/black/pyproject.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Eighty-nine thousand characters of other projects' dependency lists, scraped out of uv's test fixtures. The only uv code that made it in was three &lt;code&gt;main.rs&lt;/code&gt; files, all of them thin entry points, and &lt;code&gt;Cargo.toml&lt;/code&gt; ranked below every one of those fixtures.&lt;/p&gt;

&lt;p&gt;And here is &lt;strong&gt;awesome-mcp-servers&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;32,048  README-fa-ir.md
32,048  README-ja.md
32,048  README-ko.md
32,048  README-pt_BR.md
32,048  README-th.md
32,048  README-zh.md
32,048  README-zh_TW.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same README in seven languages, 224,000 characters of it.&lt;/p&gt;

&lt;p&gt;The cause was one line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PRIORITY_NAMES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Any file called &lt;code&gt;pyproject.toml&lt;/code&gt; got a thousand points, wherever it sat. A vendored copy six directories deep in a test fixture scored exactly the same as the one at the root describing the actual project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why the measurement mattered.&lt;/strong&gt; If I had shipped the budget on its own, the digest would have gone from 207,867 characters of the wrong files to 40,000 characters of the wrong files. Smaller, still useless, and now looking deliberate.&lt;/p&gt;
&lt;h2&gt;
  
  
  The file it wanted most came back empty
&lt;/h2&gt;

&lt;p&gt;One more, which I would never have found by reading code.&lt;/p&gt;

&lt;p&gt;In the awesome-mcp-servers run, the top-ranked file scored 1200 and returned &lt;strong&gt;zero characters&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     0  score=1200   README.md
32,048  score=200    README-fa-ir.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;GitHub's contents API will not serve a file over 1MB. It does not return an error. It returns &lt;code&gt;200 OK&lt;/code&gt; with an empty &lt;code&gt;content&lt;/code&gt; field and &lt;code&gt;encoding: "none"&lt;/code&gt;. So the base64 decode succeeds, produces an empty string, and the file silently disappears.&lt;/p&gt;

&lt;p&gt;That README is 1,616,144 bytes. The single most important file in the repository was being dropped without a word, and seven translations of it were filling the space instead. The fix is to notice &lt;code&gt;encoding: "none"&lt;/code&gt; and refetch through the blobs endpoint, which serves up to 100MB.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A total budget, not a file count.&lt;/strong&gt; 40,000 characters, configurable. Spent on the tree first, then files in priority order, until it runs out. The number of files now falls out of what fits instead of being pinned in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cap on the tree&lt;/strong&gt;, a quarter of the total, with a line saying how many entries it omitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A per-file ceiling&lt;/strong&gt; of 40% of what remains, so one large file cannot crowd out everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranking by position, not just name.&lt;/strong&gt; The priority bonus now decays with directory depth, so the root manifest beats a vendored one. Test and fixture directories lose points, vendored ones lose more, examples lose only a little because sometimes an examples directory is the best documentation a project has. Files in the repository's primary language, which the API already tells you, gain some.&lt;/p&gt;

&lt;p&gt;Here is the same table after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repository&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GopherMCP/GopherCache&lt;/td&gt;
&lt;td&gt;4,594&lt;/td&gt;
&lt;td&gt;5,005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pyarchana/gopher&lt;/td&gt;
&lt;td&gt;24,231&lt;/td&gt;
&lt;td&gt;39,263&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sktime/sktime-mcp&lt;/td&gt;
&lt;td&gt;103,786&lt;/td&gt;
&lt;td&gt;39,231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;astral-sh/uv&lt;/td&gt;
&lt;td&gt;207,867&lt;/td&gt;
&lt;td&gt;39,321&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;modelcontextprotocol/python-sdk&lt;/td&gt;
&lt;td&gt;217,488&lt;/td&gt;
&lt;td&gt;39,224&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;punkpeye/awesome-mcp-servers&lt;/td&gt;
&lt;td&gt;228,310&lt;/td&gt;
&lt;td&gt;39,168&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;uv's digest now leads with &lt;code&gt;Cargo.toml&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt;. awesome-mcp-servers leads with its actual README. My own small repo got &lt;em&gt;bigger&lt;/em&gt;, because it was only ever showing 10 of its 22 files and now shows all of them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The thing I would tell past me
&lt;/h2&gt;

&lt;p&gt;I nearly shipped a one-line change. Add a constant, clamp the output, close the issue. It would have passed review, passed tests, and produced a tool that was confidently wrong in a smaller font.&lt;/p&gt;

&lt;p&gt;What stopped it was spending an hour printing what the tool actually produced against real inputs. Not unit tests, which only check what I already thought to assert. Not reading the code, which is where the bug had been sitting unnoticed for months. Just running it against six real repositories and looking at the output.&lt;/p&gt;

&lt;p&gt;If you are building anything that assembles context for a model, you are making size and selection decisions whether you notice them or not. Print what your tool actually sends, against real inputs, at real sizes. The numbers are frequently not what you expect, and the interesting failure is rarely the one you set out to fix.&lt;/p&gt;



&lt;p&gt;The tool is &lt;a href="https://github.com/pyarchana/gopher" rel="noopener noreferrer"&gt;pyarchana/gopher&lt;/a&gt;, an MCP server that fetches, caches and digests context for Claude. The measurements above live in &lt;a href="https://github.com/pyarchana/gopher/issues/5" rel="noopener noreferrer"&gt;#5&lt;/a&gt; and &lt;a href="https://github.com/pyarchana/gopher/issues/14" rel="noopener noreferrer"&gt;#14&lt;/a&gt; if you want the full before and after.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pyarchana" rel="noopener noreferrer"&gt;
        pyarchana
      &lt;/a&gt; / &lt;a href="https://github.com/pyarchana/gopher" rel="noopener noreferrer"&gt;
        gopher
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      One MCP server for Claude: digest any GitHub repo, keep memory across conversations, and extract facts locally with Ollama
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Gopher&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/pyarchana/gopher/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/pyarchana/gopher/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One MCP server that fetches, caches, and digests context for Claude.&lt;/p&gt;

&lt;p&gt;Gopher is three things that used to be three separate servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fetch&lt;/strong&gt;: point it at a GitHub repo and get back a clean Markdown digest: full directory tree, plus the contents of the files that actually matter. Filters out binaries, lock files, &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;venv&lt;/code&gt;, and the rest of the noise, then ranks what's left. No README? It builds one for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cache&lt;/strong&gt;: persistent memory across conversations, stored as two plain files you can read yourself: a structured &lt;code&gt;context.json&lt;/code&gt; and an append-only &lt;code&gt;diary.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;digest&lt;/strong&gt;: reads a conversation transcript, extracts the facts with a local Ollama model, and merges them into the cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs locally over stdio. Nothing leaves your machine except GitHub API calls.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tools&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Tool&lt;/th&gt;

&lt;th&gt;What it does&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;fetch_github_repo(repo_url)&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;Markdown digest of a public repo, tree plus top files&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;read_context()&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;Return&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;…&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pyarchana/gopher" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Five bugs in my LLM app that never threw an error</title>
      <dc:creator>Archana</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:31:58 +0000</pubDate>
      <link>https://dev.to/chanadev/five-bugs-in-my-llm-app-that-never-threw-an-error-1ejk</link>
      <guid>https://dev.to/chanadev/five-bugs-in-my-llm-app-that-never-threw-an-error-1ejk</guid>
      <description>&lt;p&gt;I spent two and a half weeks building an agent that reads a project's code review history and remembers what it decided. 86,321 comments from pandas, distilled into 298 conventions, served out of CockroachDB behind a Lambda.&lt;/p&gt;

&lt;p&gt;Every serious bug I hit had the same shape. Nothing raised. Tests passed. The demo worked. And somewhere in the middle, a component was doing absolutely nothing.&lt;/p&gt;

&lt;p&gt;Here are five, with the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;bool("false")&lt;/code&gt; is &lt;code&gt;True&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The whole point of the system is that it refuses when it does not know. The answering prompt returns JSON with an &lt;code&gt;answered&lt;/code&gt; field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model that writes &lt;code&gt;"answered": "false"&lt;/code&gt; instead of &lt;code&gt;"answered": false&lt;/code&gt; turns a refusal into an answer, because a non-empty string is truthy. The one behaviour the project exists to guarantee, defeated by a quotation mark.&lt;/p&gt;

&lt;p&gt;Now every model response goes through a Pydantic model that fails towards doing nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnswerOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelOutput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;answered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;   &lt;span class="c1"&gt;# coerces "false" -&amp;gt; False
&lt;/span&gt;    &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An answer becomes a refusal. A drafted rule becomes unusable. A contradiction verdict becomes "compatible", which retires nothing. A garbled response is not evidence for writing to memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A &lt;code&gt;null&lt;/code&gt; discarded whole responses, six times out of six
&lt;/h2&gt;

&lt;p&gt;The fix for bug 1 caused bug 2, which is funnier than it was at the time.&lt;/p&gt;

&lt;p&gt;The prompt that turns a maintainer's comment into a rule says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"rationale": "why, one sentence, only if they gave a reason"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A maintainer who states a convention flatly gets &lt;code&gt;null&lt;/code&gt; back, which is correct JSON for "no reason given". And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DraftedRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;   &lt;span class="c1"&gt;# null is not a str
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pydantic rejects &lt;code&gt;null&lt;/code&gt; for a plain &lt;code&gt;str&lt;/code&gt;. Validation is all or nothing. So the entire response was thrown away and replaced with the inert default, which downstream reads as the model having declined.&lt;/p&gt;

&lt;p&gt;A real maintainer correction became &lt;code&gt;{"status":"ignored","reason":"no convention stated"}&lt;/code&gt;. Six reproductions, six failures, on a payload that was otherwise exactly what I asked for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@field_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_null_text_is_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotation&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;null&lt;/code&gt; where a string was expected means empty, not malformed. Note the &lt;code&gt;field.annotation is str&lt;/code&gt; check: &lt;code&gt;scope_pattern: str | None&lt;/code&gt; genuinely means None, and coercing that one would turn "applies everywhere" into a pattern matching nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 68 of my 79 path rules matched zero files
&lt;/h2&gt;

&lt;p&gt;Each rule stores a pattern for which files it applies to. I matched with &lt;code&gt;fnmatch&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas/core/frame.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas/core/%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patterns were &lt;strong&gt;SQL LIKE&lt;/strong&gt;, not globs. &lt;code&gt;pandas/tests/%&lt;/code&gt;, &lt;code&gt;pandas/core/%&lt;/code&gt;, &lt;code&gt;pandas/tests/%/conftest.py&lt;/code&gt;. Nothing asked the extraction prompt for that and nothing documented it. The model just wrote it, presumably because the rules were destined for a database.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;%&lt;/code&gt; is not an fnmatch wildcard. So 68 of 79 anchored rules silently matched nothing, and the agent ran entirely on the two rules that happened to be written &lt;code&gt;pandas/**/*.py&lt;/code&gt;, which cover 63% of the repository.&lt;/p&gt;

&lt;p&gt;It looked like it was working the entire time. The fix is one line, and the bug was invisible for a week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The similarity score I was tuning had no signal in it
&lt;/h2&gt;

&lt;p&gt;The agent comments on pull requests, choosing which conventions apply by embedding a description of the change and taking the nearest rules.&lt;/p&gt;

&lt;p&gt;It commented on 38 of 40. I assumed a bad threshold and started tuning. At 1.05 it spoke on 95% of pull requests; at 0.95, on 18%.&lt;/p&gt;

&lt;p&gt;Then I measured the distribution instead. For 25 real pull requests, distance to the nearest rule versus the tenth nearest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query phrasing&lt;/th&gt;
&lt;th&gt;median nearest&lt;/th&gt;
&lt;th&gt;median spread, 1st to 10th&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title only&lt;/td&gt;
&lt;td&gt;0.954&lt;/td&gt;
&lt;td&gt;0.097&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;title plus directories&lt;/td&gt;
&lt;td&gt;0.947&lt;/td&gt;
&lt;td&gt;0.100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;paths and counts&lt;/td&gt;
&lt;td&gt;0.958&lt;/td&gt;
&lt;td&gt;0.105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;written as a question&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;td&gt;0.095&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The nearest rule is not meaningfully nearer than the tenth, under any phrasing. Every rule is a general statement about the same codebase, so every rule is similar to any description of a change to it.&lt;/p&gt;

&lt;p&gt;There was nothing to threshold. Retrieval now runs on path matching, which is a fact, and similarity only orders what the paths already admitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. GitHub's &lt;code&gt;author_association&lt;/code&gt; describes the present
&lt;/h2&gt;

&lt;p&gt;I filtered to maintainer comments using GitHub's &lt;code&gt;author_association&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;It reports whether someone has write access &lt;strong&gt;now&lt;/strong&gt;, not when they wrote the comment. One reviewer wrote 74,077 comments on pandas between 2012 and 2025, more than twice anyone else. He has since left the org, so all 74,077 come back as &lt;code&gt;CONTRIBUTOR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Filtering on that field discarded about a third of the corpus, including the most experienced reviewers the project ever had. No error, no warning, just a smaller number that looked plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;None of these raised. Four of the five I found by counting something rather than by reading code: how often does it speak, how many rules match anything, how far apart are the distances really.&lt;/p&gt;

&lt;p&gt;A library used wrong fails loudly. A model used wrong returns confident, well-formatted output that is quietly detached from what you meant.&lt;/p&gt;

&lt;p&gt;So pick a number that should hold if the system works, and go and look at it. Not "does it return results" but "how often does it speak, and is that the rate I intended". Not "did it cite something" but "do the citations resolve".&lt;/p&gt;

&lt;p&gt;Code is at &lt;a href="https://github.com/pyarchana/precedent" rel="noopener noreferrer"&gt;github.com/pyarchana/precedent&lt;/a&gt;, MIT.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>debugging</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Google's Workspace Intelligence Might Actually Fix the Thing That Bugs Me Most About AI Tools</title>
      <dc:creator>Archana</dc:creator>
      <pubDate>Thu, 23 Apr 2026 03:00:57 +0000</pubDate>
      <link>https://dev.to/chanadev/googles-workspace-intelligence-might-actually-fix-the-thing-that-bugs-me-most-about-ai-tools-10n7</link>
      <guid>https://dev.to/chanadev/googles-workspace-intelligence-might-actually-fix-the-thing-that-bugs-me-most-about-ai-tools-10n7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Okay so I've been watching the Google Cloud NEXT '26 announcements drop today and there's a lot. TPUs, agentic platforms, a $750M partner fund. But the thing that actually made me stop scrolling was &lt;strong&gt;Workspace Intelligence&lt;/strong&gt; :)&lt;/p&gt;

&lt;p&gt;Not because it's the most technically impressive thing on the list. It's not. But it might be the one that changes how I (and probably you) actually work day to day.&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that's always bothered me about AI in Google Docs
&lt;/h2&gt;

&lt;p&gt;Every time I open Docs and try to use Gemini to help me write something, I spend the first few minutes basically re-introducing myself. Here's the project. Here's the tone we're going for. Here's who the audience is. Here's the doc we drafted last month that this is building on.&lt;/p&gt;

&lt;p&gt;It's exhausting :) The AI is smart but it has no idea who I am or what I'm working on. Every prompt is a cold start.&lt;/p&gt;

&lt;p&gt;That's exactly what Workspace Intelligence is supposed to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;Google is calling it a "semantic layer" across all your Workspace apps. Gmail, Docs, Sheets, Slides, Drive, Chat, Calendar. The idea is that instead of each app being its own isolated thing, there's now a shared understanding of your work running underneath all of them :)&lt;/p&gt;

&lt;p&gt;It learns what projects you're actively working on, who you're collaborating with, what your writing sounds like, and what's actually on your plate right now. Then every time you use Gemini anywhere in Workspace, it's pulling from that context instead of starting fresh.&lt;/p&gt;

&lt;p&gt;Practically speaking, here's what that looks like across the apps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gmail&lt;/strong&gt; gets an AI Inbox that doesn't just summarize. It suggests what you should actually &lt;em&gt;do&lt;/em&gt;, and drafts replies that sound like you, not like a generic assistant :)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Chat&lt;/strong&gt; gets an "Ask Gemini" mode that Google is framing as a "unified command line for your work." You say what you need, it goes off and does it across Workspace. Schedules the meeting, pulls the file, writes the brief.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs&lt;/strong&gt; can now edit multiple images at once for visual consistency, build infographics from your own business data, and handle comment threads. Including making edits to the doc based on the feedback in those comments :)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slides&lt;/strong&gt; can build a full deck from a single prompt, and it'll actually use your company's real templates and visual styles. Not a generic blue gradient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sheets&lt;/strong&gt; lets you build and edit spreadsheets by just... talking to it. And it can pull data from your emails, files, and chat history to populate things :)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drive&lt;/strong&gt; is getting "Projects," a smarter way to organize everything around active work rather than just being a folder system that you dig through.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bit I find most interesting
&lt;/h2&gt;

&lt;p&gt;There's a line in Google's announcement that stood out to me: Workspace Intelligence learns your "unique work style, voice, and formatting preferences" so outputs sound "authentically like you."&lt;/p&gt;

&lt;p&gt;I'm genuinely curious how well that works in practice :) Voice matching is one of those things that sounds great in a keynote and then either impresses you or makes you cringe when you see the actual output. I haven't been able to test it yet, but it's the thing I'm most excited (and slightly skeptical) about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about the "let AI read all my stuff" concern?
&lt;/h2&gt;

&lt;p&gt;Yeah, this is the obvious question. Google is essentially asking you to let a system reason across your email, your calendar, your chat history, and your files. That's a lot of trust.&lt;/p&gt;

&lt;p&gt;Their answer: customer data isn't used for ads, isn't reviewed by humans, and isn't used to train models outside Workspace without your permission :) Admins can control which data sources it touches, so if your org doesn't want it reading Gmail, they can turn that off. Client-side encryption and sovereign data controls (US and EU, with more coming) are part of the rollout too.&lt;/p&gt;

&lt;p&gt;Whether that's enough will depend on your org's risk appetite. But at least they're not burying it in the fine print.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I land on this
&lt;/h2&gt;

&lt;p&gt;I think the individual features are nice, but the real bet here is the compounding effect :) The more Workspace Intelligence understands your context, the more useful every individual tool becomes. That's a different kind of value than "we added a summarize button."&lt;/p&gt;

&lt;p&gt;Google's basically trying to turn Workspace from a collection of apps you switch between into something that actually understands what you're trying to get done. Whether that plays out the way they're describing it, or whether it turns into a very well-marketed context window, is something we'll only know once people start using it for real.&lt;/p&gt;

&lt;p&gt;But I'm paying attention. And honestly? I think this one's worth your time to follow :)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Are you using Google Workspace day to day? Curious whether this kind of persistent context layer actually sounds useful to you, or if it feels like AI for AI's sake. Let me know below :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
    </item>
  </channel>
</rss>
