ADR-022: The scaled region eval — the ruler shrinks, and the global cluster is systematic¶
Status: Accepted Date: 2026-08-02 Deciders: San Lee
Related: ADR-014 (the region field, and the judge gate this
eval stands on) · ADR-007 (floors come from measured runs) ·
ADR-020 (the L4 critic fixed 6 of the 7 global misses at
~4× cost — the alternative this eval prices) · ADR-017
(the disclosed two-axis limit this partly lifts) ·
spec · release runbook
Context¶
The region axis shipped in v3.0.0 with exactly one number: 87.0% on n=54, whose 95%
Wilson interval is [75.6%, 93.6%] — 18 points wide. Two questions could not be answered
inside an interval that wide.
- Is a future region change a real regression? A prompt edit that moves the number five
points is indistinguishable from noise at n=54. HANDOFF job 2 — a prompt clause aimed at
the
globalcluster — had no ruler to be measured against. - Is the named error cluster systematic? All seven region misses on the gold set were
rows whose true label was
globaland which the model pulled to a specific region, inferring a theater from the US actor where the snippet stated no place. Seven rows is a story, not a measurement.
ADR-014 made the judge conditional rather than assumed: it had to validate against human
labels on this axis before it could grade at scale. It did — 100.0% region agreement on
the gold 54. The harness landed run-ready in #154 (spec: docs/specs/scaled-region-eval.md),
and the paid pass was owner-run on 2026-08-02.
Decision¶
Record the measurement, and read it as a ruler rather than a headline. Full report:
evals/scale_eval_v3.txt; per-row predictions in
evals/scale_predictions_v3.csv with its provenance sidecar; region confusion matrix in
evals/scale_confusion_v3_region.csv. This ADR is the durable verdict; the release it rides
is v3.2.0 (MINOR — additive measurement, SYSTEM_PROMPT / src/api.py / the
{category, operational_domain, region} contract all untouched).
What was measured¶
300 DVIDS snippets (data/scale/scale_set.csv, the same ids v2.1.0 used — reused, not
resampled, per the spec's §3 fork), workhorse claude-sonnet-5, answer key the
claude-opus-4-8 judge, same prompt and call path that cleared the ADR-014 gate.
| Workhorse vs judge | Accuracy | 95% CI (Wilson) | Macro-F1 |
|---|---|---|---|
| Region | 88.3% (265/300) | [84.2%, 91.5%] | 0.904 |
| Category | 91.7% (275/300) | [88.0%, 94.3%] | 0.763 |
| Domain | 89.3% (268/300) | [85.3%, 92.3%] | 0.868 |
The deliverable is the interval, not the point estimate. Region's CI narrows from 18 points at n=54 to 7 points at n=300. The accuracy landing at 88.3% against the gold set's 87.0% is corroboration — a better-known number, which is the whole ask.
The global cluster is confirmed systematic¶
Answer-key global rows |
70 |
| …of which pulled to a specific region | 17 |
Converse (over-called global) |
10 |
| All region disagreements | 35 |
| The pull's share of them | 49% (17/35) |
| Pulled to | americas 16, indo-pacific 1 |
Seven rows could not separate a behavior from a run of luck. Seventeen out of seventy, 49% of
every region disagreement, and 16 of 17 landing on americas, can: the model infers a theater
from the US actor when the snippet anchors nothing, exactly as the n=54 cluster suggested and
exactly what the rubric's no-guessing rule forbids.
This ADR arms HANDOFF job 2; it does not implement it. No prompt change is made here.
Measuring first is the method, and the clause now has a ruler with a 7-point interval to be
measured against instead of an 18-point one. Note the cost that has accrued to that job since
it was written: #137/#142 pinned the published snapshot to the prompt that produced it, so
touching SYSTEM_PROMPT hard-reds both gen_metrics_artifact.py and src/eval_gate.py until
a paid gold re-run rewrites the sidecar.
The answer-key caveat, preserved verbatim¶
These numbers are workhorse-vs-judge agreement, not a second human answer key. The judge's
measured disagreement with humans on region was 0/54 — and 0/54 is itself a wide interval
([93.4%, 100%]). So the n=300 figures are read alongside the human-graded n=54 figures in
evals/gold_eval_v3.txt, never instead of them. The report states this in its own header, and
any surface quoting the n=300 number inherits the obligation to say so.
Two further honesty items the report derives from the data rather than hardcoding: the DVIDS
wire is US-actor-heavy, so americas is 148/300 (49%) and africa has n=3 — which drags the
region macro-F1 (0.904) down through thin classes rather than through a quality drop. Read
overall accuracy plus the well-populated per-label rows.
Fork 1 — where the n=300 numbers get published: bake-off precedent¶
They cannot be data-metric/metric:-marked anywhere, because evals/metrics.json publishes
the live gold block only and no artifact key exists for a scale figure; architecture's
UNMARKED_ALLOWED ratchet is shrink-only. Three options were on the table (runbook "Design
forks"): (a) frozen dated figures in this repo, bare prose elsewhere; (b) extend metrics.json
with scale keys; (c) raise the ratchet.
Taken: (a). The numbers live in this repo — the committed eval artifacts plus a frozen,
dated figure treatment in README.md carrying no managed metric markers. Architecture and
portfolio point at the classifier rather than quoting it. This is precisely the ADR-017
bake-off precedent: a frozen dated measurement must not drift with a live artifact, and a
marker is a promise that a number tracks the artifact. (b) would be a deliberate SYS-019
amendment turning a dated run into a published live key — the bake-off deliberately declined
that. (c) is against the ratchet's own shrink-only rule.
Fork 2 — a thresholds.toml floor: no¶
No floor is added; evals/thresholds.toml and src/eval_gate.py are untouched. ADR-007's
rule is that floors come from measured runs, and this measurement now exists — but a floor
also needs run-to-run noise under it, which one run cannot supply (that is how the v2 floors
were sized). The v2.1.0 scale numbers have carried no thresholds for the same reason: a scale
pass is a dated measurement, not a live gate, and the offline gate grades a free committed
snapshot while re-measuring this one costs 600 calls. Revisitable after a second run.
Consequences¶
- The region axis has a usable ruler. A 7-point interval makes a real region regression distinguishable from noise for the first time; HANDOFF job 2 becomes measurable.
- The
globalcluster is evidence, not anecdote — and it is now also the at-scale price comparison for ADR-020's declined critic (which fixed 6/7 of the cluster at ~4× calls). - No published number moves. The gold block in
evals/metrics.jsonis computed fromevals/gold_predictions_v3.csv(n=54), which this run does not touch; the only byte that changes in the artifact isversion. All eight gated floors stay byte-identical. - No live gate gets stricter, and no prompt, model, or contract changes. The shipped classifier is byte-for-byte the one that produced the v3.0.0 gold numbers.
- A dated figure is now a maintenance obligation: the README treatment carries its date and its n, and must not be re-quoted as if it tracked the live artifact.
Downstream surfaces¶
- Eval artifacts (new, frozen records):
evals/scale_eval_v3.txt,evals/scale_predictions_v3.csv,evals/scale_predictions_v3.provenance.json,evals/scale_confusion_v3_region.csv. Never regenerated or overwritten, per the repo's frozen-record rule. - Version chain (one commit, or CI is red):
pyproject.toml→3.2.0, regeneratedevals/metrics.json, the pinned literal + measured-at docstring intests/test_metrics_artifact.py, andsrc/api.py's FastAPIversion=. - Release paperwork:
CHANGELOG.md([3.2.0]heading + link refs),CLAUDE.md(the "Not yet shipped" bullet retires),HANDOFF.md(job 1 → shipped; owner-only pending list),decisions/README.md(this index row). - Prose that named the eval as future work:
README.md(the "Current state" heading and paragraph, the(v3.1.0)gate line, the "no region labels until v3.1.0" baseline caveat, and the new frozen region-CI table beside the v2.1.0 scaled-eval section),src/ml_loop.py,src/baseline_ml.py,docs/specs/l4-multi-agent.md,docs/specs/ml-baseline-bakeoff.md. - Other repos (per the release runbook's dependency chain):
architecture(program/README.md— the guarded<!-- version:classifier -->marker and the roadmap bullet, which points here rather than quoting the new figures per fork 1; the portal regenerates itself on that push), andkb-agent(kb/projects/defense-news-classifier.md, theningest.py --accept+index.py). - Decisions: ADR-014 (its judge gate is what licensed this run; unamended — correct on its date), ADR-007 (fork 2 applies its rule rather than changing it), ADR-020 (this is the at-scale sizing of the cluster its critic fixed), ADR-017 (its disclosed "no region labels" limit is now dated rather than open-ended).
- Not touched, deliberately:
evals/thresholds.toml,src/eval_gate.py,classify.SYSTEM_PROMPT,src/api.py's behavior, every frozen v2 record (evals/scale_eval.txt,evals/baseline_eval.txt,evals/l4_eval.txt,evals/gold_eval_v3.txt), and portfolio / learning-notes — whose gates stay green and whose optional prose is deferred, fork 1 keeping the new figures out of them regardless.