<?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: tauridev</title>
    <description>The latest articles on DEV Community by tauridev (@tauridev).</description>
    <link>https://dev.to/tauridev</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%2F4019232%2F9ef8616e-3149-4666-96a2-5340e2d2e3a3.png</url>
      <title>DEV Community: tauridev</title>
      <link>https://dev.to/tauridev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tauridev"/>
    <language>en</language>
    <item>
      <title>LLM self-review vs. a blind re-run on 160 gold-labeled name-matching pairs — no measurable difference at ceiling</title>
      <dc:creator>tauridev</dc:creator>
      <pubDate>Sun, 12 Jul 2026 01:43:06 +0000</pubDate>
      <link>https://dev.to/tauridev/llm-self-review-vs-a-blind-re-run-on-160-gold-labeled-name-matching-pairs-no-measurable-14c2</link>
      <guid>https://dev.to/tauridev/llm-self-review-vs-a-blind-re-run-on-160-gold-labeled-name-matching-pairs-no-measurable-14c2</guid>
      <description>&lt;p&gt;If you ship anything an LLM produced and calm your nerves by asking the &lt;em&gt;same&lt;/em&gt; model to "please double-check it," this is a small, pre-registered measurement of whether that actually buys you anything.&lt;/p&gt;

&lt;p&gt;Short version: on this task it bought nothing measurable — but mostly because the task turned out to be too easy, which is a lesson in itself about evaluation design.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Setup: &lt;code&gt;gpt-5.4-mini&lt;/code&gt;, 160 gold-labeled "are these two records the same person?" pairs, pre-registered before the run.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The first pass was near-ceiling: 159/160 overall, 120/120 on the confirmation set.&lt;/strong&gt; So there was almost nothing left to fix — I ended up measuring "does re-review &lt;em&gt;preserve&lt;/em&gt; correct answers," not "does it &lt;em&gt;fix&lt;/em&gt; errors."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-review (shown its own draft) tied a blind re-run.&lt;/strong&gt; Across all 160 pairs both scored 156/160. On the pre-registered confirmation set, the single discordant pair gives exact McNemar p = 1.0 — no power to tell them apart, not proof of equivalence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversals of correct answers happened in &lt;em&gt;both&lt;/em&gt; conditions, equally (3 and 3).&lt;/strong&gt; Not specific to self-review; consistent with plain run-to-run variation (single runs, so I can't causally isolate the extra pass itself).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest caveat:&lt;/strong&gt; with the first pass this high, I could not test the thing I actually wanted to test. Treat this as the ceiling data point of a series, not a verdict on self-correction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproducibility scope
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This is one model, one prompt, &lt;code&gt;reasoning_effort=low&lt;/code&gt;, single runs in a fixed order, on an 80/80 balanced diagnostic benchmark (60/60 confirmation + 20/20 hard). Nothing here generalizes to "LLM self-correction" in general. It's a concrete, reproducible measurement on one task, with all raw judgments and the pre-registration public.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why measure this
&lt;/h2&gt;

&lt;p&gt;"Self-correction" reports point both ways. Self-Refine (Madaan et al. 2023) reports large gains from iterative self-feedback; Huang et al. 2024 ("LLMs Cannot Self-Correct Reasoning Yet") and Kamoi et al. 2024 (a critical cross-task survey) find that without a reliable external signal, self-correction often doesn't help and can degrade. Those negative results are mostly on reasoning tasks, so I treat any agreement here as a cross-genre extrapolation, not a replication.&lt;/p&gt;

&lt;p&gt;The practical version of the question: an LLM says "these two records are the same person / different people." Does asking the same model to re-check make it safer? Entity matching (deciding whether two &lt;em&gt;given&lt;/em&gt; records refer to one entity) is easy to build gold labels for, so you can measure this without subjective judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Task: match a Japanese footballer's Japanese name (kanji/kana) against a romaji name — e.g. is &lt;code&gt;久保建英&lt;/code&gt; the same person as &lt;code&gt;Takefusa Kubo&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Model: &lt;code&gt;gpt-5.4-mini-2026-03-17&lt;/code&gt;, &lt;code&gt;reasoning_effort=low&lt;/code&gt;. &lt;strong&gt;Measured: 2026-07.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;160 pairs: a confirmation set of 120 (60 true-match / 60 non-match) plus 40 hard pairs.&lt;/li&gt;
&lt;li&gt;Surface forms come from &lt;strong&gt;different sources&lt;/strong&gt; (Japanese side = Wikidata, romaji side = a Transfermarkt-derived dataset), so club-name spellings and coverage don't line up perfectly.&lt;/li&gt;
&lt;li&gt;The model never sees dates of birth, IDs, or URLs — only names and club history. Gold is verified against an independent attribute (DOB), not the linking ID.&lt;/li&gt;
&lt;li&gt;Cost: 480 calls (160 × A/B/C), ~$0.45 total.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I measured (pre-registered)
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A — first pass:&lt;/strong&gt; judge the two records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B — blind re-run:&lt;/strong&gt; the byte-identical prompt, fresh context, no draft shown. ("Independent" here means &lt;em&gt;A not shown&lt;/em&gt;, not statistical independence.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C — two-stage review:&lt;/strong&gt; the original data &lt;strong&gt;plus A's structured output&lt;/strong&gt; as a draft, with a neutral "KEEP or CHANGE" instruction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pre-registered primary comparison is &lt;strong&gt;C vs B on the 120-pair confirmation set&lt;/strong&gt; (balanced accuracy difference + a paired interval). C vs B measures the &lt;em&gt;whole&lt;/em&gt; "hand the model its own output and ask it to re-review" workflow, not the anchoring effect in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The first pass was already at the ceiling
&lt;/h3&gt;

&lt;p&gt;A scored &lt;strong&gt;120/120&lt;/strong&gt; on the confirmation set and &lt;strong&gt;159/160&lt;/strong&gt; overall. That single fact reframes the whole experiment: there was essentially nothing to fix, so I measured &lt;em&gt;maintenance near the ceiling&lt;/em&gt;, not &lt;em&gt;error correction&lt;/em&gt;.&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%2Fo1utplj4sfkzjanrnd1p.png" 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%2Fo1utplj4sfkzjanrnd1p.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Re-review did not beat a blind re-run
&lt;/h3&gt;

&lt;p&gt;Descriptively, across all 160 pairs &lt;strong&gt;B and C both scored 156&lt;/strong&gt;. On the pre-registered confirmation set the 2×2 is: both correct 117, C-only-correct 1, B-only-correct 0, both-wrong 2. That single discordant pair (&lt;code&gt;川辺駿&lt;/code&gt; / Hayao Kawabe) gives exact &lt;strong&gt;McNemar p = 1.0&lt;/strong&gt; — which means "no power to distinguish," &lt;strong&gt;not&lt;/strong&gt; "proven equivalent." The balanced-accuracy gap is +0.8 pt, but with one discordant pair the paired bootstrap lower bound is pinned at 0, so I don't lean on it. I don't claim equivalence either.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. "Destruction" happened in both conditions, and it isn't self-review-specific
&lt;/h3&gt;

&lt;p&gt;Reversals of A's correct answers, over all 160 pairs: &lt;strong&gt;A→B = 3, A→C = 3&lt;/strong&gt; (each 3/159 = 1.9%, Wilson 0.64–5.4%). On the confirmation set B breaks 3 and C breaks 2, but that flips on the hard set — subset-dependent. C's final verdict actually matches A on &lt;strong&gt;157/160&lt;/strong&gt;; the few it moved were all in the wrong direction, but with A at 159/160, "everything it moved was wrong" is close to arithmetically inevitable — not evidence C selectively destroys correct answers.&lt;/p&gt;

&lt;p&gt;Deviations from the pre-registration (honest)&lt;/p&gt;

&lt;p&gt;The pre-registered CI was a paired bootstrap (one of two registered options); it degenerated with a single discordant pair, so I report exact McNemar as an &lt;em&gt;added, post-hoc&lt;/em&gt; auxiliary and don't lean on the interval. Wilson intervals were pre-registered for the secondary counts but missing from the first analysis script (added afterward). A pre-registered fame-stratified breakdown wasn't run (no clean popularity proxy). "Confirmatory" claims are limited to C-vs-B on the confirmation set.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Honest scope note:&lt;/strong&gt; I did not measure human review, a different model, or any tool that checks the answer against ground truth. So I can't say external verification is better — only that handing the same model its own output didn't beat a plain re-run here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What I actually do now:&lt;/strong&gt; at this accuracy, I don't spend the extra pass. If I want a real second opinion, I reach for a &lt;em&gt;different&lt;/em&gt; signal (a rule, a lookup, a second model), not the same model re-reading the same cues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything I got wrong
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My non-match pairs were too easy.&lt;/strong&gt; I first built "same surname, different person" pairs, but the Japanese-side player was consistently older (mean 3.5-year gap), so the model could separate them on &lt;em&gt;era&lt;/em&gt; alone. I rebuilt them to minimize the birth-year gap. &lt;em&gt;Lesson: the benchmark author has to suspect their own benchmark is too easy.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My headline was inflated, and my own data killed it.&lt;/strong&gt; I started to write "self-review destroys correct answers" — then noticed a blind re-run destroyed just as many. The mechanism was "a second pass at ceiling," not "self-review." &lt;em&gt;Lesson: reconstruct the alternative baseline in your own data before you name a cause.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I almost claimed the hard cases needed club matching.&lt;/strong&gt; 79 of 80 non-matches are actually separable by &lt;em&gt;reading&lt;/em&gt; the given name. Only one truly needed an auxiliary attribute — and it's the one the model missed:&lt;/li&gt;
&lt;/ol&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%2F8oblose5pkl3c03p5d2l.png" 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%2F8oblose5pkl3c03p5d2l.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;荒井悠汰&lt;/code&gt; and a different person written &lt;code&gt;Yuta Arai&lt;/code&gt; have identical romaji but different birth dates. Reading can't separate them; you need club history. That one homonym was A's only error (a single case, n=1) — and re-review didn't fix it. &lt;em&gt;Lesson: the same model, re-reading the same cues, repeats the same miss.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ceiling effect (central).&lt;/strong&gt; With 159/160 correct up front, "can it fix errors" is essentially untested; I measured preservation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory contamination is unaddressed.&lt;/strong&gt; These are public players; I can't separate "inferred the reading" from "recalled the person." The fame-stratified probe I pre-registered didn't run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same-model self-loop only.&lt;/strong&gt; No external verifier, human, or ground-truth check. Not a Self-Refine replication (no separate feedback stage; one shot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark representativeness.&lt;/strong&gt; The confirmation set is a fixed 60/60 diagnostic split with hand-built non-matches; not a production match-rate distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single runs, fixed order, one model, &lt;code&gt;reasoning_effort=low&lt;/code&gt;.&lt;/strong&gt; The ceiling and the null may be specific to this low-effort setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/axiom-pro/llm-nayose-matching" rel="noopener noreferrer"&gt;https://github.com/axiom-pro/llm-nayose-matching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PREREGISTRATION.md&lt;/code&gt; (committed before the run, &lt;code&gt;d7a3328&lt;/code&gt;) → &lt;code&gt;src/run_experiment.py&lt;/code&gt; → &lt;code&gt;src/analyze.py&lt;/code&gt; recomputes every number from &lt;code&gt;run_results.json&lt;/code&gt;. Result commits: &lt;code&gt;fd42585&lt;/code&gt;, analysis add-ons &lt;code&gt;74a00b5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Public: pair info, model judgments, analysis code, pre-registration. Withheld: DOB/verification attributes and non-redistributable raw data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Ask it again" isn't automatically safer.&lt;/strong&gt; Whether re-review helps depends on how much room there is to improve and where the checking signal comes from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same model re-reading the same input isn't a second opinion.&lt;/strong&gt; If the cues are identical, so are the blind spots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before measuring self-correction, make sure the first pass can actually fail.&lt;/strong&gt; A ceiling task can't tell you whether re-review fixes errors — it can only show whether it preserves them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't read this as "LLM self-review is useless." Read it as: &lt;strong&gt;on a near-ceiling task, an extra same-model pass showed no measurable value over a blind re-run — and the interesting failure was a homonym that re-reading can't catch.&lt;/strong&gt; The next data point is a deliberately harder set where the first pass drops to 75–90%.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo and pre-registration above. I'm explicit about what I did **not&lt;/em&gt;* measure: no human, no second model, no ground-truth checker — so I make no claim about external verification here.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an English adaptation of my Japanese article — written by me in Japanese, restructured and translated with AI assistance, human-reviewed. Japanese original on Qiita (a Japanese dev-blogging platform): &lt;a href="https://qiita.com/axiompro70/items/e186a0fe97a8318a2e9c" rel="noopener noreferrer"&gt;https://qiita.com/axiompro70/items/e186a0fe97a8318a2e9c&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;About me: I measure AI-assisted development practices and verify AI-produced code/data/judgments against ground truth — GitHub: axiom-pro.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>nlp</category>
    </item>
    <item>
      <title>What actually breaks when a free Cloudflare D1 database hits its 500 MB wall</title>
      <dc:creator>tauridev</dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:57:02 +0000</pubDate>
      <link>https://dev.to/tauridev/what-actually-breaks-when-a-free-cloudflare-d1-database-hits-its-500-mb-wall-4g9l</link>
      <guid>https://dev.to/tauridev/what-actually-breaks-when-a-free-cloudflare-d1-database-hits-its-500-mb-wall-4g9l</guid>
      <description>&lt;p&gt;Every "free serverless DB" comparison ranks capacity, price, and cold-start. Almost none answer the question you actually hit in production: &lt;strong&gt;what happens at the free-tier wall?&lt;/strong&gt; So I filled a Cloudflare D1 database to its limit over the REST API and poked it.&lt;/p&gt;

&lt;p&gt;The honest short version: the wall is &lt;strong&gt;not&lt;/strong&gt; a full read-only lockout, a plain &lt;code&gt;DELETE&lt;/code&gt; clears it immediately (but that recovery is not quota-free), and — the part I got wrong &lt;strong&gt;twice&lt;/strong&gt; — building an index on a blob column roughly &lt;strong&gt;doubles&lt;/strong&gt; your storage, so it slams into the wall on its own. This post has been revised twice: my own &lt;code&gt;size_after&lt;/code&gt; logs, and then a follow-up experiment, corrected me. I'll show both corrections instead of hiding them.&lt;/p&gt;

&lt;p&gt;Scope: &lt;strong&gt;Workers Free, throwaway DBs, over the D1 REST API &lt;code&gt;/query&lt;/code&gt;, 2026-07.&lt;/strong&gt; The coarse wall was reproduced on a second, freshly-created database; the exact-ceiling pin and the index-doubling are each n=1 (flagged). This is a REST probe, not a Worker Binding probe — don't generalize endpoint-specific behavior (especially &lt;code&gt;VACUUM&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  "The free-tier wall" is not one wall
&lt;/h2&gt;

&lt;p&gt;D1 has &lt;strong&gt;several&lt;/strong&gt; walls, with different scopes and — per Cloudflare's own error list — different &lt;em&gt;messages&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wall&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Value (Free)&lt;/th&gt;
&lt;th&gt;Documented message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;per-DB storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;per database&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;500 MB&lt;/strong&gt; (Paid: 10 GB)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Exceeded maximum DB size&lt;/code&gt; — action: "delete rows / shard"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;account storage&lt;/td&gt;
&lt;td&gt;account&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Your account has exceeded D1's maximum account storage limit…&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily rows read&lt;/td&gt;
&lt;td&gt;account&lt;/td&gt;
&lt;td&gt;5,000,000/day&lt;/td&gt;
&lt;td&gt;daily-limit-exceeded → "can't run queries"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily rows written&lt;/td&gt;
&lt;td&gt;account&lt;/td&gt;
&lt;td&gt;100,000/day&lt;/td&gt;
&lt;td&gt;same (&lt;strong&gt;INSERT, UPDATE, and DELETE all count as writes&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(Free daily counters reset at 00:00 UTC; storage is not daily-reset. Max row 2 MB; 100 columns; 10 DBs/account. All confirmed against the D1 docs on 2026-07-09.)&lt;/em&gt; &lt;strong&gt;I measured the first one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1 — the wall is 500 MB (decimal); pinned to one row on one DB, reproduced coarsely on a second
&lt;/h2&gt;

&lt;p&gt;Filling with bulk inserts stalls around ~499.9 MB. To pin it, I then looped &lt;strong&gt;single-row&lt;/strong&gt; inserts at the wall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;… 24 single-row inserts succeed (size_after climbs)
25th: HTTP 400  [{"code":7500,"message":"Exceeded maximum DB size"}]
last successful size_after = 499,994,624 bytes  (≈ 499.99 MB ≈ 476.83 MiB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ceiling — &lt;strong&gt;499,994,624&lt;/strong&gt; — is &lt;code&gt;500,000,000&lt;/code&gt; (decimal 500 MB) minus 5,376 bytes, enforced at SQLite's 4 KB page granularity (&lt;code&gt;499,994,624 = 122,069 × 4096&lt;/code&gt;). I reproduced the &lt;strong&gt;coarse&lt;/strong&gt; wall on a second, freshly-created DB: it stopped at the identical &lt;code&gt;499,884,032&lt;/code&gt; and returned the same &lt;code&gt;7500&lt;/code&gt;. One caveat so you can hold me to my own logs: the redacted &lt;code&gt;results/*.jsonl&lt;/code&gt; carry &lt;strong&gt;no account IDs&lt;/strong&gt;, and &lt;code&gt;served_by_colo&lt;/code&gt; is Anycast edge routing, not an account signal — so what's verifiable from the evidence is &lt;em&gt;a second database&lt;/em&gt;, and the byte-exact &lt;code&gt;499,994,624&lt;/code&gt; pin is from the single-row probe on &lt;strong&gt;one&lt;/strong&gt; of them (n=1). The wall firing at all means these DBs are on &lt;strong&gt;Workers Free&lt;/strong&gt; (Paid = 10 GB).&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2 — not read-only, but small writes aren't "exempt" either (measured)
&lt;/h2&gt;

&lt;p&gt;My first draft claimed small writes slip past the wall while big ones don't. Wrong — and my own numbers show why. Those 24 single-row inserts above consumed only &lt;code&gt;499,994,624 − 499,884,032 = 110,592 bytes&lt;/code&gt; (~110 KB) of headroom before even a single-row insert was rejected. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads always pass&lt;/strong&gt; — near-tautological for a storage cap; reads don't grow the DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes pass only while headroom remains.&lt;/strong&gt; Small writes weren't bypassing the wall; they were filling the last sliver before it. Call it slack, not exemption. (One headroom-exhaustion sequence, but the 25th insert failing at zero headroom is a clean counterexample to "small writes are exempt" — one is enough.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result 3 — recovery is a plain DELETE, no upgrade (but it spends write quota)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;size_after&lt;/span&gt; &lt;span class="mi"&gt;499&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="n"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mi"&gt;243&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;884&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;032&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;244&lt;/span&gt; &lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;immediately&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;rows_written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;
&lt;span class="n"&gt;bulk&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt;                    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mi"&gt;7500&lt;/span&gt; &lt;span class="nv"&gt;"cannot VACUUM from within a transaction: SQLITE_ERROR"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maybe&lt;/span&gt; &lt;span class="n"&gt;REST&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;specific&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;DELETE&lt;/code&gt; drops D1's reported size &lt;strong&gt;immediately&lt;/strong&gt; and writes resume — no &lt;code&gt;VACUUM&lt;/code&gt;, no plan upgrade (&lt;strong&gt;recovery class R1&lt;/strong&gt;). Two honest qualifiers: (1) &lt;strong&gt;it isn't quota-free&lt;/strong&gt; — D1 counts &lt;code&gt;DELETE&lt;/code&gt; as a write (docs: &lt;em&gt;"Write operations include INSERT, UPDATE, and DELETE"&lt;/em&gt;), so this one 19,998-row delete spent ~20% of the daily 100k budget, and the cost scales with &lt;strong&gt;row count, not bytes freed&lt;/strong&gt; (this delete freed ~256 MB ≈ 12.8 KB/row; whether it's linear per chunk I didn't separately measure); (2) &lt;code&gt;size_after&lt;/code&gt; is D1's &lt;em&gt;reported&lt;/em&gt; size — the meter dropping doesn't prove the physical file shrank (cf. workerd#1618, local).&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 4 — CREATE INDEX doubles your storage (the thing I got wrong twice)
&lt;/h2&gt;

&lt;p&gt;At the wall, &lt;code&gt;CREATE INDEX&lt;/code&gt; fails with &lt;code&gt;out of memory: SQLITE_NOMEM&lt;/code&gt;. My first draft lumped that in as "large allocation blocked"; after review I "corrected" it to "that's a memory limit, independent of the storage wall." &lt;strong&gt;Both were wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a &lt;strong&gt;control DB at 300 MB&lt;/strong&gt; — well &lt;em&gt;below&lt;/em&gt; the 500 MB wall — &lt;code&gt;CREATE INDEX&lt;/code&gt; failed not with &lt;code&gt;SQLITE_NOMEM&lt;/code&gt; but with &lt;strong&gt;&lt;code&gt;7500 "Exceeded maximum DB size"&lt;/code&gt;&lt;/strong&gt; (storage!). Below the wall, a storage error? I measured the mechanism directly — deleted the control to 80 MB and indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bench&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;029&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;440&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;MiB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;
  &lt;span class="n"&gt;size_after&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;029&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;440&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mi"&gt;167&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;923&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;712&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="err"&gt;×&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt; &lt;span class="n"&gt;growth&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measured&lt;/span&gt; &lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;An index on a blob column carries a copy of the column's data as its keys, so it roughly doubles storage&lt;/strong&gt; — inferred from this one growth point, not proven in general. If that ×2 holds at other sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;storage before index&lt;/th&gt;
&lt;th&gt;after (~×2)&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;80 MB&lt;/td&gt;
&lt;td&gt;160 MB&lt;/td&gt;
&lt;td&gt;✅ succeeds (under the wall)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300 MB&lt;/td&gt;
&lt;td&gt;~600 MB&lt;/td&gt;
&lt;td&gt;⛔ &lt;code&gt;Exceeded maximum DB size&lt;/code&gt; (over the 500 MB wall)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 MB (at the wall)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;⛔ &lt;code&gt;SQLITE_NOMEM&lt;/code&gt; (OOMs during the build)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the index failure is &lt;strong&gt;storage-bound via the index's own footprint&lt;/strong&gt;, not "memory, unrelated to the wall." A rough rule of thumb — &lt;em&gt;if&lt;/em&gt; the ×2 holds — is that on D1 Free you can only index a column holding up to ~250 MB of data (2 × 250 = 500 MB), but that's an extrapolation, not measured near 250 MB. The &lt;code&gt;SQLITE_NOMEM&lt;/code&gt; is just what you see when you're already &lt;em&gt;at&lt;/em&gt; the wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error code, and what I did not measure
&lt;/h2&gt;

&lt;p&gt;Cloudflare's error list classifies these by &lt;strong&gt;message&lt;/strong&gt; (&lt;code&gt;Exceeded maximum DB size&lt;/code&gt;, &lt;code&gt;…account storage limit…&lt;/code&gt;, &lt;code&gt;…isolate memory limit…&lt;/code&gt;); the number &lt;code&gt;7500&lt;/code&gt; appears &lt;strong&gt;nowhere&lt;/strong&gt; in the docs. So &lt;code&gt;7500&lt;/code&gt; is a generic REST wrapper — the message is the discriminator.&lt;/p&gt;

&lt;p&gt;Not measured / n=1 / extrapolated (so not claimed): the &lt;strong&gt;index ×2 is a single measurement&lt;/strong&gt; at one size/row-shape; the ~250 MB threshold is extrapolated (not measured near 250 MB); DELETE linearity across chunks is not measured (one 19,998-row delete); the account 5 GB wall, the daily read wall (5M/day — &lt;em&gt;reads&lt;/em&gt; stop, account-scoped), and Worker Binding behavior are all untested; &lt;code&gt;size_after&lt;/code&gt; behaves like a coherent logical meter (dropped on the one DELETE, ~doubled on the one index build) but the docs don't say physical vs logical. One full cycle (fill 39k + recover 20k + control 24k ≈ 84k &lt;code&gt;rows_written&lt;/code&gt;) fits in one UTC day but not two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/axiom-pro/free-tier-quota-probe
&lt;span class="nb"&gt;cd &lt;/span&gt;free-tier-quota-probe
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run selftest
&lt;span class="c"&gt;# .env: CF_ACCOUNT_ID / CF_API_TOKEN (D1:Edit) / D1_DATABASE_ID / D1_CONTROL_DATABASE_ID / BILLING_SAFE=confirmed&lt;/span&gt;
node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env bin/d1-admin.mjs create wallprobe   &lt;span class="c"&gt;# create the DB via API (no dashboard needed)&lt;/span&gt;
npm run ping        &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; d1
npm run fill        &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; d1 &lt;span class="nt"&gt;--confirm-destructive&lt;/span&gt;
npm run probe-slack &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; d1                        &lt;span class="c"&gt;# pins the wall + measures the headroom&lt;/span&gt;
npm run fill        &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; d1 &lt;span class="nt"&gt;--control&lt;/span&gt; &lt;span class="nt"&gt;--cap-mb&lt;/span&gt; 300 &lt;span class="nt"&gt;--confirm-destructive&lt;/span&gt;
npm run probe-index &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; d1                        &lt;span class="c"&gt;# the index-doubling test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node ≥ 20, ESM, D1 over native &lt;code&gt;fetch&lt;/code&gt; (60 s timeout + one retry). Redacted captures are frozen in &lt;code&gt;results/*.jsonl&lt;/code&gt;; predictions (and the follow-up amendment) are in &lt;code&gt;PREREGISTRATION.md&lt;/code&gt;. &lt;strong&gt;Repo:&lt;/strong&gt; &lt;code&gt;https://github.com/axiom-pro/free-tier-quota-probe&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about measurement discipline — filling the wall instead of quoting the limit, and correcting my own assumptions on the record. This post's Results 2 and 4 are the live examples: I reasoned wrong twice and only the measurements got it right. Written with AI assistance; every number is from the captured `results/&lt;/em&gt;.jsonl`, and the doc facts were re-verified against Cloudflare's D1 docs on 2026-07-09. A Japanese version of this write-up also exists.*&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>database</category>
      <category>sqlite</category>
      <category>serverless</category>
    </item>
    <item>
      <title>I had a local LLM review the same code 51 times: majority voting picks persistence, not correctness</title>
      <dc:creator>tauridev</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:48:35 +0000</pubDate>
      <link>https://dev.to/tauridev/i-had-a-local-llm-review-the-same-code-51-times-majority-voting-picks-persistence-not-correctness-4g15</link>
      <guid>https://dev.to/tauridev/i-had-a-local-llm-review-the-same-code-51-times-majority-voting-picks-persistence-not-correctness-4g15</guid>
      <description>&lt;p&gt;If you use an LLM to review code — or run it several times and keep "whatever most runs agree on" — this experiment suggests the votes you're counting measure &lt;strong&gt;persistence&lt;/strong&gt;, not &lt;strong&gt;correctness&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Setup: Qwen2.5-Coder 1.5B / 7B (Apache-2.0) via Ollama 0.31.1, single RTX 4060 Ti, Q4_K_M quantization. N=51 repeated audits of the same small Python files with predeclared seeded defects (temp-0 baseline: N=11 per condition). The per-run records (normalized findings plus a SHA-256 of each raw response), ground truth, and aggregation code are public — every number quoted in this article regenerates deterministically from the frozen records via an independent verification script.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fully pinned = deterministic (on this rig).&lt;/strong&gt; At temperature 0 with a fixed GPU and quantization, the finding sets were identical across runs — Jaccard similarity &lt;strong&gt;1.00&lt;/strong&gt; in all four conditions (N=11 each). In a probe, six repeats within one seed and six different seeds all produced &lt;em&gt;byte-identical&lt;/em&gt; output: our temp-0 configuration also forces &lt;code&gt;top_k=1&lt;/code&gt; (greedy decoding in Ollama/llama.cpp), so the seed had no observable effect. "LLMs are always nondeterministic" is folklore, at least under fully pinned local conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling at temperature 0.7 breaks agreement.&lt;/strong&gt; On the defect-seeded file — 51 runs, one seed per run (0–50) — run-to-run Jaccard dropped to &lt;strong&gt;0.52&lt;/strong&gt; (7B) and &lt;strong&gt;0.07&lt;/strong&gt; (1.5B). For 7B, allowing ±2 lines of tolerance recovers 0.52 → &lt;strong&gt;0.80&lt;/strong&gt;: over half of the disagreement is consistent with &lt;em&gt;the same bug pointed at a different line&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The core finding: majority voting selects persistence, not correctness.&lt;/strong&gt; A real resource leak that the model detected in &lt;strong&gt;45 of 51 runs&lt;/strong&gt; split its votes across two adjacent lines (26 + 19 — no run reported both, so those really are 45 distinct runs). Under the strict line+category key, the majority threshold of 26 barely keeps one line; one vote higher, at k=27, a defect found 45 times is gone. Meanwhile, two "grey" pattern warnings (&lt;code&gt;eval&lt;/code&gt; and &lt;code&gt;shell=True&lt;/code&gt; on &lt;em&gt;constants&lt;/em&gt;, from the decoy file) survived at 33/51 and 28/51, because the model points at exactly the same place every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest caveats up front:&lt;/strong&gt; on the defect-seeded file, against predeclared ground truth with category + ±2-line matching, the 7B's per-run recall was only 0.31 — but precision was 0.98. Within this benchmark: when it speaks, it's right, and it misses a lot. The 1.5B was too noisy to carry any claim (it hit 25 of the file's 32 lines at least once across 51 runs), so it serves as a contrast group, not evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠ &lt;strong&gt;Reproducibility scope:&lt;/strong&gt; the &lt;em&gt;existence and rough magnitude&lt;/em&gt; of the variance reproduces; exact numbers are rig-dependent (GPU / quantization / inference-engine version). The repo ships frozen per-run records — normalized findings plus a hash of each raw response; the raw response bodies themselves were not retained — and readers re-run only the deterministic aggregation layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why measure this
&lt;/h2&gt;

&lt;p&gt;"LLM-as-a-judge" is everywhere, and its known weakness is nondeterminism. Prior work measures &lt;em&gt;score&lt;/em&gt; stability — &lt;a href="https://arxiv.org/abs/2502.20747" rel="noopener noreferrer"&gt;Klishevich et al. (arXiv:2502.20747)&lt;/a&gt; measured scalar-verdict consistency over 70 commits at temp 0, and a Japanese industrial blog measured same-score rates on cloud APIs. What I couldn't find measured anywhere: whether &lt;strong&gt;the set of findings itself&lt;/strong&gt; — &lt;em&gt;which&lt;/em&gt; issues, on &lt;em&gt;which&lt;/em&gt; lines — is stable across runs, on a local model, free to reproduce, with run records frozen and shipped. That's the gap this fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;code&gt;Qwen2.5-Coder&lt;/code&gt; 1.5B and 7B (Apache-2.0), run locally via Ollama. Chosen because they're free, local, and require no API key — anyone can rerun generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling design:&lt;/strong&gt; temp-0 conditions use a &lt;strong&gt;fixed seed&lt;/strong&gt; and force &lt;code&gt;top_k=1, top_p=1.0&lt;/code&gt;; temp-0.7 conditions use &lt;strong&gt;one seed per run (0, 1, …, 50)&lt;/strong&gt; with Ollama's default sampler otherwise (&lt;code&gt;top_k=40, top_p=0.9&lt;/code&gt;; not swept). The temp-0.7 numbers therefore measure &lt;strong&gt;across-seed sampling variability at temperature 0.7&lt;/strong&gt; — not same-seed runtime nondeterminism (with a fixed seed, repeats would be trivially identical).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output format:&lt;/strong&gt; Ollama structured outputs (JSON-schema-enforced). Each finding is &lt;code&gt;{line_number, category, severity, description}&lt;/code&gt;, category constrained to an enum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity of a finding:&lt;/strong&gt; the composite key &lt;code&gt;(line_number, category)&lt;/code&gt;. Paraphrases are ignored; two findings are "the same" iff they name the same place and problem type. I deliberately did &lt;em&gt;not&lt;/em&gt; use embedding similarity — it happily merges "missing null check on line 12" with "missing null check on line 45".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target files (4):&lt;/strong&gt; &lt;code&gt;target_a&lt;/code&gt; (7 seeded defects), &lt;code&gt;clean&lt;/code&gt; (no defects — false-positive baseline), &lt;code&gt;decoy&lt;/code&gt; (looks dangerous, low actual harm — this classification gets revised later, see pitfall #4), &lt;code&gt;easy&lt;/code&gt; (6 obvious defects).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground truth:&lt;/strong&gt; predeclared in a &lt;code&gt;gt.csv&lt;/code&gt; &lt;em&gt;outside&lt;/em&gt; the code before measurement. No hints in the code or the prompt — I learned this one the hard way (pitfall #1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-layer reproducibility:&lt;/strong&gt; layer 1 (generation, nondeterministic) ran once and its per-run records are frozen in the repo; layer 2 (aggregation, Python stdlib only) is fully deterministic and is what you reproduce.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inference&lt;/td&gt;
&lt;td&gt;Ollama 0.31.1&lt;/td&gt;
&lt;td&gt;local, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;Qwen2.5-Coder 1.5B / 7B&lt;/td&gt;
&lt;td&gt;Q4_K_M (verified via &lt;code&gt;ollama show&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;NVIDIA RTX 4060 Ti (CUDA)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ollama ps&lt;/code&gt; reported 100% GPU offload during runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregation&lt;/td&gt;
&lt;td&gt;Python 3.12, stdlib only&lt;/td&gt;
&lt;td&gt;layer 2 fully deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Measured: 2026-07. Per-cell provenance (model quantization/size/family, Ollama version, prompt/schema/GT hashes, backend) is recorded in the frozen records.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability = Jaccard between runs (no ground truth involved):&lt;/strong&gt; &lt;code&gt;matched findings / union&lt;/code&gt;, computed with &lt;strong&gt;optimal bipartite matching&lt;/strong&gt; — naive greedy matching gives order-dependent values (a bug I actually shipped and had to fix). Reported as the mean over all run pairs within a condition, with a &lt;strong&gt;line-tolerance window w&lt;/strong&gt; (±0 strict / ±2 / ±5) reported alongside — this doubles as the sensitivity analysis for the identity key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validity = precision / recall / false positives against GT.&lt;/strong&gt; Unless noted, recall/precision are per-run means on &lt;code&gt;target_a&lt;/code&gt;, category-strict, line ±2. Precision excludes empty runs by definition — and there were none on &lt;code&gt;target_a&lt;/code&gt; (0/51); recall averages over all runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Majority voting = keep findings that appear in ≥ k of N runs.&lt;/strong&gt; Examined on two axes: threshold k × key granularity w.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncertainty:&lt;/strong&gt; 95% CIs from bootstrap resampling &lt;strong&gt;at the run level&lt;/strong&gt;. One honest structural caveat: 51 audits of the &lt;em&gt;same&lt;/em&gt; file are pseudo-replication — nothing here generalizes across codebases, and I don't claim it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result 1: pin everything, and it's deterministic
&lt;/h2&gt;

&lt;p&gt;All four temp-0 conditions (2 models × {defect-seeded, easy} files, N=11): Jaccard &lt;strong&gt;1.00&lt;/strong&gt; at every tolerance level, zero variance across runs. In a separate probe on one condition, six repeats within a single seed and six different seeds (1, 2, 3, 101, 202, 303) produced byte-identical outputs — our temp-0 configuration also forces &lt;code&gt;top_k=1&lt;/code&gt;, i.e. greedy decoding in Ollama/llama.cpp, so the seed had no observable effect there. Folklore says "LLMs always wobble"; under fully pinned local conditions on this rig, they didn't.&lt;/p&gt;

&lt;p&gt;📐 The honest asterisk on "deterministic"&lt;/p&gt;

&lt;p&gt;In a separate spike I did observe a single category flip at temp 0 — only ever on the &lt;em&gt;first&lt;/em&gt; run after model load, which also had the longest latency (cold-cache/warm-up signature). With a warm-up run added, the residual disappeared. I can't pin the mechanism (GPU parallel-reduction ordering is the usual suspect; cold cache isn't ruled out), but it isn't the seed. This is consistent with &lt;a href="https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/" rel="noopener noreferrer"&gt;Thinking Machines' "Defeating Nondeterminism in LLM Inference"&lt;/a&gt;, which attributes serving-time nondeterminism primarily to &lt;strong&gt;batch-size variation&lt;/strong&gt; — a local single-request setup is effectively fixed-batch, which is consistent with the 1.00 here. The same mechanism would also predict the temp-0 flutter Klishevich et al. saw on &lt;em&gt;cloud APIs&lt;/em&gt; (variable batching), though they discuss other candidate causes as well — read this as consistency with one proposed mechanism, not a proven attribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: sample at temperature 0.7, and it falls apart
&lt;/h2&gt;

&lt;p&gt;People raise temperature precisely to widen candidate coverage across runs. At temp 0.7 on the defect-seeded file (one seed per run, 0–50), run-to-run Jaccard was &lt;strong&gt;0.52&lt;/strong&gt; [0.47, 0.59] for 7B and &lt;strong&gt;0.07&lt;/strong&gt; [0.04, 0.11] for 1.5B.&lt;/p&gt;

&lt;p&gt;Two distinct failure modes hide in those numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line drift (7B):&lt;/strong&gt; 0.52 recovers to &lt;strong&gt;0.80&lt;/strong&gt; when the identity key tolerates ±2 lines. Over half of the disagreement is consistent with the same bug pointed at a different line — though tolerance matching can also create accidental matches, so treat ±2 as an upper bound on "drift".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category flutter (1.5B, and &lt;code&gt;easy&lt;/code&gt;):&lt;/strong&gt; on the easy file, 7B's line-only recall is 0.81 but category-strict recall is 0.42 — the model keeps finding the right &lt;em&gt;place&lt;/em&gt; and labeling it a different &lt;em&gt;problem&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;recall (category-strict)&lt;/th&gt;
&lt;th&gt;recall (line-only)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;target_a&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;0.31&lt;/td&gt;
&lt;td&gt;0.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target_a&lt;/td&gt;
&lt;td&gt;1.5B&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(For 7B on &lt;code&gt;target_a&lt;/code&gt; the two recalls coincide — those misses are genuine non-detections, not drift.)&lt;/p&gt;

&lt;p&gt;And here's where I hit my first big mistake: my early recall numbers were great — because I had left &lt;code&gt;# DEFECT: SQL injection&lt;/code&gt; annotations in the target code as notes to myself, and the whole file goes into the prompt. &lt;strong&gt;The model was reading my answer key.&lt;/strong&gt; Deleting the labels cratered recall and deepened the run-to-run variance. Details in the pitfall log below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3 (the core): majority voting rewards persistence
&lt;/h2&gt;

&lt;p&gt;Majority vote = "keep findings that appear in ≥26 of 51 runs." Here is what that actually selects.&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%2Fa2nangb04fjob6nvez8o.png" 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%2Fa2nangb04fjob6nvez8o.png" alt="Votes per finding across 51 audits (7B, temp 0.7). Grey-smell warnings survive above the majority line; the real ResourceLeak bug splits 26/19 across two lines and straddles the threshold." width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure: votes per finding (blue = predeclared seeded defects, red = persistent findings outside GT, dashed = majority threshold 26/51). Counts are pooled from two target files run under identical conditions: the blue bars come from the defect-seeded file, the red bars from the decoy file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanism.&lt;/strong&gt; With &lt;code&gt;(line, category)&lt;/code&gt; as the identity key, the same bug pointed at a neighboring line becomes &lt;em&gt;a different ballot&lt;/em&gt;. The model detected the ResourceLeak in &lt;strong&gt;45/51 runs&lt;/strong&gt; — but the votes landed 26 on line 16 and 19 on line 17 (no single run reported both lines, so 26+19 really is 45 distinct runs). Loosen the question to "did it say ResourceLeak at all, anywhere?" and it's 49/51 — scattered across &lt;strong&gt;six&lt;/strong&gt; different lines (12, 15, 16, 17, 18, 21). The vote count never sees that. Watch the threshold:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold k&lt;/th&gt;
&lt;th&gt;line 16 (26 votes)&lt;/th&gt;
&lt;th&gt;line 17 (19 votes)&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;k=19&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;both survive (duplicated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k=26 (majority)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;survives by one vote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k=27&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;a defect reported in 45/51 runs is gone&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under the exact majority rule I predeclared (k=26) the leak &lt;em&gt;barely&lt;/em&gt; survives on one line; the point is not that majority voting killed it, but that &lt;strong&gt;whether it survives is governed by how the votes split and where you draw the line — not by the model's detection ability&lt;/strong&gt;. And it's not one anecdote: &lt;strong&gt;2 of the 5 defects that 7B detected at least once (~40%) showed line drift between runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two contrasts sharpen the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unsplit real bugs sail through:&lt;/strong&gt; the SQL injection on line 9 got &lt;strong&gt;51/51&lt;/strong&gt; unanimous votes. Voting doesn't kill strong detections — it kills &lt;em&gt;wobbly-keyed&lt;/em&gt; ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grey findings survive:&lt;/strong&gt; warnings against &lt;code&gt;eval("2 + 2")&lt;/code&gt; (33/51) and &lt;code&gt;subprocess.run(..., shell=True)&lt;/code&gt; with a constant string (28/51) — both low-harm on constants, both from the decoy file — persist because the model reacts to the &lt;em&gt;pattern&lt;/em&gt; and points at exactly the same place every time. (Bonus wobble: it labeled the latter &lt;code&gt;CodeInjection&lt;/code&gt; rather than &lt;code&gt;CommandInjection&lt;/code&gt; — categories flutter too.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;clean&lt;/code&gt; file adds a neat corollary: 7B stayed silent in 37/51 runs (72.5%), and when it &lt;em&gt;did&lt;/em&gt; complain, it complained about something different almost every time — non-empty clean runs agree at just &lt;strong&gt;0.12&lt;/strong&gt;. (Counting silent–silent pairs as agreement 1.0 the unconditional mean is 0.53; I report the conditional number because silence agreeing with silence isn't informative here.) Inconsistent false alarms are exactly what majority voting removes. Consistent grey warnings are exactly what it cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold × key-granularity sensitivity&lt;/strong&gt; (majority k=26):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File (7B)&lt;/th&gt;
&lt;th&gt;w ±0&lt;/th&gt;
&lt;th&gt;±2&lt;/th&gt;
&lt;th&gt;±5&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FP &lt;strong&gt;0&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;scattered false alarms die (as intended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;decoy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 persistent&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;consistent grey findings survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;target_a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TP 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3 *&lt;/td&gt;
&lt;td&gt;* the +1 is a double-credit artifact (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;easy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TP 2 / FP 1&lt;/td&gt;
&lt;td&gt;3 / 2&lt;/td&gt;
&lt;td&gt;3 / 1 †&lt;/td&gt;
&lt;td&gt;looser keys rescue split votes — at a price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;† The &lt;code&gt;easy&lt;/code&gt; false-positive count is non-monotonic in w (1→2→1) because widening the window changes how findings cluster — adjacent clusters merge or get absorbed. This is a property of line-window clustering, disclosed rather than hidden.&lt;/p&gt;
&lt;/blockquote&gt;

📐 Why the ±5 "rescue" is double-counting, not detection

At w ±5, the ResourceLeak cluster (mostly line 16) becomes wide enough to overlap **two distinct** ground-truth defects — a connection leak (lines 6–11) and a file-handle leak (lines 15–17) — so one cluster gets credited to both (the aggregation deliberately allows multi-credit; that design choice is exactly what this row exposes). To be precise: no run put a ResourceLeak vote inside the connection-leak lines themselves (6–11); the nearest was a single vote at line 12 — one line outside, and far below any threshold. So the "+1 true positive" at ±5 is a windowing artifact, not a detection. Loosening keys rescues split votes, but the rescue is not free: on `easy` it temporarily raises false positives (1→2 at ±2), and here it manufactures a detection that never cleared any threshold. No free lunch.



&lt;p&gt;&lt;strong&gt;So: majority voting filters for &lt;em&gt;salience of the pattern response&lt;/em&gt;, not truth.&lt;/strong&gt; Persistent findings survive whether they're real bugs or grey smells; real bugs whose expression wobbles can fall. "Persistent" and "correct" are different properties, and the vote measures the first one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Honest scope note: in this sample the core claim rests mainly on one drifting real bug plus two persistent grey findings — read it as an &lt;strong&gt;observed mechanism&lt;/strong&gt;, not a general law. It is &lt;em&gt;not&lt;/em&gt; an argument against aggregation per se — this is also not the &lt;a href="https://arxiv.org/abs/2203.11171" rel="noopener noreferrer"&gt;self-consistency&lt;/a&gt; setting, which votes over &lt;em&gt;normalized&lt;/em&gt; final answers; the failure mode here lives precisely in the un-normalized key. Change the key (category-only, AST node, defect region) and the behavior changes — which is itself the point: threshold × key granularity is a design decision with no free lunch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What I actually do now:&lt;/strong&gt; treat multi-run LLM review as a &lt;strong&gt;candidate pool&lt;/strong&gt;, not an auto-verdict. Prioritize high-frequency findings for review, &lt;em&gt;don't&lt;/em&gt; discard low-frequency findings in severe categories, and let tests, static analysis, and a human's understanding of the spec make the final call.&lt;/p&gt;
&lt;h2&gt;
  
  
  Everything I got wrong (measurement-instrument failures included)
&lt;/h2&gt;

&lt;p&gt;The most useful part of this experiment was watching my own tooling and my own memory fail. Five mistakes, in the order I made them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My answer key leaked into the prompt.&lt;/strong&gt; I'd annotated defect lines with &lt;code&gt;# DEFECT: ...&lt;/code&gt; comments as notes to myself — and the audit prompt includes the whole file. High recall was the model reading my answers. Removing the labels cratered recall and deepened variance. &lt;em&gt;Never let the benchmark's answers be visible to the system under test.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I thought I was benchmarking CPU inference. It was 100% GPU.&lt;/strong&gt; I had designed a thread-count variable (&lt;code&gt;num_thread&lt;/code&gt; 1 vs 4) — then &lt;code&gt;ollama ps&lt;/code&gt; showed 100% GPU offload, where &lt;code&gt;num_thread&lt;/code&gt; is a no-op. My thread axis had measured nothing. &lt;em&gt;Record your accelerator provenance instead of assuming it — the act of recording is what exposed the error.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I broke my own measuring instrument while "fixing" it.&lt;/strong&gt; A one-line "optimization" to a clustering helper introduced a classic append-then-clear aliasing bug that silently corrupted the majority-vote true/false-positive counts. I caught it only because a hand count said 2 where the tool said 3. Every number quoted in this article's text is now recomputed by an independent script (&lt;code&gt;verify_independent.py&lt;/code&gt;) that bypasses the aggregation code entirely; the full threshold×tolerance table and the bootstrap CIs still come from the aggregation pipeline. &lt;em&gt;Fixes need verification most of all.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My "false positives" were real smells.&lt;/strong&gt; I'd built &lt;code&gt;decoy&lt;/code&gt; assuming its &lt;code&gt;eval&lt;/code&gt;/&lt;code&gt;shell=True&lt;/code&gt;-on-constants patterns were harmless, so surviving warnings counted as false positives. But Bandit and friends flag exactly these. The finding got &lt;em&gt;stronger&lt;/em&gt; through the correction: the claim isn't "false positives survive voting" — it's "&lt;strong&gt;persistence survives voting, regardless of truth&lt;/strong&gt;", and I renamed those findings "persistent findings outside GT".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own memory fabricated a result.&lt;/strong&gt; An early draft said "the &lt;code&gt;md5&lt;/code&gt; warning also survived the vote." The frozen records say md5 got &lt;strong&gt;0 votes in 51 runs&lt;/strong&gt;. My memory had synthesized it from the &lt;em&gt;impression&lt;/em&gt; of pitfall #4. Caught in pre-publication log reconciliation. &lt;em&gt;The experimenter's memory is not a primary source either — recheck the frozen records every time you write a number.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is "don't trust LLM output" applied symmetrically: to the model, to my own instruments, and to my own claims.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Small sample: N=51, two model sizes, one rig (RTX 4060 Ti / CUDA / Q4_K_M / Ollama 0.31.1). CPU, other GPUs, other quantizations may behave differently.&lt;/li&gt;
&lt;li&gt;The temp-0.7 condition varies the seed per run (by design — that's what sampling the distribution means here), so it measures across-seed variability at that temperature, not a pure temperature effect with everything else held.&lt;/li&gt;
&lt;li&gt;The core claim rests on few instances (one drifting bug, two grey findings). Mechanistically coherent, statistically thin.&lt;/li&gt;
&lt;li&gt;"Smaller models wobble more" is a hypothesis from a 2-point comparison — capability and variance are confounded.&lt;/li&gt;
&lt;li&gt;Category-strict recall is low; this is not a benchmark of absolute audit quality. Some ground-truth entries depend on runtime contracts (e.g., whether a config loader may return None) — they are predeclared seeded defects under this benchmark's stated assumptions, not universally adjudicated bugs.&lt;/li&gt;
&lt;li&gt;Structured output (JSON-schema-constrained decoding) modifies the logit distribution; the variance measured here is variance &lt;em&gt;under&lt;/em&gt; that constraint. Free-form review text may behave differently.&lt;/li&gt;
&lt;li&gt;No causal claims. Scope: free, local, small models, this corpus.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Generation is nondeterministic and &lt;em&gt;not&lt;/em&gt; the reproduction target; the frozen per-run records are. The deterministic layer regenerates every table in this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/axiom-pro/llm-audit-nondeterminism
&lt;span class="nb"&gt;cd &lt;/span&gt;llm-audit-nondeterminism

&lt;span class="c"&gt;# Layer 2 (deterministic, no LLM needed): regenerate all numbers from frozen records&lt;/span&gt;
python scripts/aggregate.py            &lt;span class="c"&gt;# -&amp;gt; results/m2_summary.json&lt;/span&gt;
python scripts/verify_independent.py   &lt;span class="c"&gt;# independent recomputation of every number quoted in the text — should print ALL PASS&lt;/span&gt;
python scripts/make_figures.py         &lt;span class="c"&gt;# -&amp;gt; figures/nofreelunch.png (needs Matplotlib)&lt;/span&gt;

&lt;span class="c"&gt;# Layer 1 (optional, needs Ollama + ~5GB VRAM for 7B Q4_K_M):&lt;/span&gt;
&lt;span class="c"&gt;#   regenerating gives you a *different* distribution — that's the point&lt;/span&gt;
&lt;span class="c"&gt;#   OLLAMA_EXE=/path/to/ollama bash scripts/run_m2.sh&lt;/span&gt;
&lt;span class="c"&gt;#   python scripts/seed_probe.py       # confirm seeds have no observable effect at temp 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numerical aggregation and verification need Python 3.12 and only the standard library; figure regeneration additionally needs Matplotlib. A few seconds total. (On Windows, set &lt;code&gt;PYTHONUTF8=1&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin everything and local LLM review was deterministic on this rig&lt;/strong&gt; — including deterministically repeating the same misses. Determinism ≠ correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One review run is a sample, not a verdict.&lt;/strong&gt; At practical temperatures the finding set moves between runs, mostly by line drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Majority voting measures agreement strength, not truth.&lt;/strong&gt; Use multi-run review as a candidate generator; adjudicate with tests, static analysis, and humans.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you remember one sentence, please don't make it "LLM code review is useless" — that's not what the data says. Make it: &lt;strong&gt;multi-run review is a decent candidate generator; what's unsafe is automatic adjudication over a brittle vote key.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All per-run records (normalized findings + response hashes), ground truth, aggregation code, and the independent verification script: &lt;a href="https://github.com/axiom-pro/llm-audit-nondeterminism" rel="noopener noreferrer"&gt;github.com/axiom-pro/llm-audit-nondeterminism&lt;/a&gt;. Anything not measured (other GPUs, quantizations, larger models) is listed as not measured.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an English adaptation of &lt;a href="https://zenn.dev/tauridev/articles/llm-audit-nondeterminism" rel="noopener noreferrer"&gt;my Japanese article on Zenn&lt;/a&gt; — written by me in Japanese, restructured and translated with AI assistance, human-reviewed. If you spot an error, comments and issues are open; I'll verify against the frozen records and correct with a changelog.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About me:&lt;/strong&gt; I build developer tools in Rust/Tauri and measure AI-assisted development practices with frozen records and independent recomputation — GitHub: &lt;a href="https://github.com/axiom-pro" rel="noopener noreferrer"&gt;axiom-pro&lt;/a&gt;.&lt;/p&gt;

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