feat(m4): the vocabulary collateral strip — VOCAB-SPARING at 1.5B and 3B, AS-SCORED ONLY - #13
Conversation
M4 widens M3's probe side from the 12-concept subset to all 60 M1 concepts, keeping the 12 characterized directions as primes: 12 x 180 ablated + 180 clean = 2,340 cells per subject. It closes the bound M3's own Honest limits section stated — nothing so far showed that deleting France spares the other 48 concepts. - m4_strip.py, cut from m3_matrix.py (never from the certified m0_anchor.py), with its own byte-frozen GATE_WORDING taken verbatim from D20 including the AS-SCORED ONLY qualifier and both string templates. The 255 M1-recorded and 468 M3-recorded cells are graded FIRST — 633 cells of the 2,340, the union of the two surfaces — and any mismatch exits INVALID before a new cell is read. D22's span and ASCII bars now run over all 60 scored words plus the 12 direction words. oracle.py is untouched (a fourth byte-shared consumer). - m4_verdict.py owns strip_verdict() — precedence NOT A RESULT > DEGENERATE > UNDERPOWERED > the level bar, the failing label being the lineage null "not shown" — plus the cross-subject AND over 1.5B and 3B. The runner imports it, so exactly one implementation of the pre-committed verdict string exists. - test_m4.py (111 tests) pins the six-round review's hard-won lessons, every one a prose rule that broke when read literally: the residual selector selects exactly 2 / 2 / 0 gate-arm cells case-insensitively (case-exact selects 0; "fills the span" selects every cell), the conservative read fails in place at 41 / 71 / 84, AS-SCORED ONLY attaches at bar level only, and every realized n and ceiling is cross-checked against the recorded M1 artifacts. Gates dry-run clean on all three subjects; six wrong-arm inputs exit INVALID with named reasons; a --limit 18 smoke reproduced its reused cells bit-for-bit and wrote nothing. No real cell has been run yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
The four decisions Kyle froze 2026-07-29 — (a) across the board — with both post-freeze amendments to the D20 wording package recorded, the second separately because Amendment 1's ratification quote covers only Amendment 1. Lands with the code PR, per the M0/M1/M2/M3 pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
… estimate A self-review pass on the analysis package's edge cases found that a conservative read which AGREES perfectly on the point estimate but has a wide interval still fires AS-SCORED ONLY. That is the frozen rule read literally — D20 states the trigger as 'a conservative read's Wilson 95% lower bound is below 0.5', not 'a conservative read disagrees' — and it errs conservative, so it stands unchanged. Pinned so a later reader does not 'fix' a frozen gate. Unreachable in M4's own runs, where the concept-level n is 23 / 41 / 43, all >= MIN_N. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
…S-SCORED ONLY Three subjects, 2,340 cells each, ~50 minutes on MPS, $0. Gate arm (gated non-subset items surviving all 12 deletions): 0.5B 11/41 = 0.268 [0.157, 0.419] -> not shown (off-gate) 1.5B 51/71 = 0.718 [0.605, 0.810] -> VOCAB-SPARING - AS-SCORED ONLY 3B 63/84 = 0.750 [0.648, 0.830] -> VOCAB-SPARING - AS-SCORED ONLY The qualifier fired at both gate-bearing subjects on a real divergence: the pre-registered concept-level collapse reads 0.585 (lower 0.434) and 0.605 (lower 0.456), below the bar while the item-level gate clears it, so per D20 those are the honest numbers to quote. Round 4's F20 amendment is why they ride inside the verdict string instead of sitting in prose. Re-certification, two generations deep: M1 255/255 and M3 468/468 cells reproduced bit-for-bit with concept_mass exact, on every subject. Because that surface includes all 180 clean cells, every realized n was knowable before the run — arms 41/71/84, concepts 23/41/43, ceilings 35/41, 69/71, 82/84 with exactly the named misses. All landed. Descriptive findings the gate never asked for: finding 1 generalizes out of sample (no prime is a wrecking ball; specific probe columns collapse — copper 6/12, mosquito 8/12 at 1.5B), category-block collateral is real in the wider vocabulary and does NOT dissolve with scale (re-scoping M3, whose within arm was 30/34 countries), and 0.5B is the first measured divergence between the subset-12's robustness and the wider roster's (73% of gate-arm items damaged, vs 28% and 25%). Spine per the per-stage rhythm: M4-BRIEF results + honest limits, ROADMAP row, LEARNING M4 section + 3 recall questions, PROJECT status and facts, HANDOFF rewritten, README status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
Both should-fix findings from PR #13's adversarial review. F1 — CI executed each test file as a plain script (`uv run test_m4.py`). With no `__main__` guard the files imported, defined their functions and exited 0, so the workflow's rc only ever reflected import errors: CI has run zero assertions since fdcbfcc, and this branch's 1,269-line test_m4.py was the newly-unprotected surface while HANDOFF claimed the suite was green. Now `uv run pytest -q "$f"` per file, keeping the per-suite log grouping. Verified: the loop collects and runs all six suites, 396 cases. F2 — every predecessor suite pins its own artifacts to its own module, a convention M2 added after the M1 re-score JSONs silently kept publishing a retracted ORACLE_WORDING sentence (test_m2.py:660). test_m4.py had dropped it: nothing tied results/m4-strip-*.json to m4_strip.GATE_WORDING, the bar, the prime roster, or strip_verdict()'s templates — so a later edit would land green while three committed JSONs kept publishing the superseded version that PROJECT/README/ROADMAP/HANDOFF/M4-BRIEF all quote. It matters more at M4 than it did at M3 because M4 is the close-out: no successor suite is scheduled to pin it. Four parametrized cases added — frozen wording and constants, the two-generation re-certification and frozen plan read off the artifacts themselves, the verdict re-derived byte-for-byte from strip_verdict(), and every pre-registered n and ceiling. All held at HEAD, so no re-run; a mutation check (bar 0.5 -> 0.55) fails 12 cases, confirming the pins bite. F3 and F4 (both nice-to-have) are recorded as follow-ups in HANDOFF, not fixed here: honouring F3's frozen per-pair-texture clause would change the artifacts and cost a re-run for a non-verdict-bearing readout, and F4 has no reachable path. Suite: 396 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
F5 from PR #13's round-2 review — a defect the previous commit created. Making CI really invoke pytest exposed that both dry-run tests pass `--lens lenses/qwen2.5-0.5b-instruct-n100.pt` into the real `main()`, which `torch.load`s the artifact before the `--dry-run` exit. Those weights are gitignored by decision K3, so on a clean `actions/checkout` the load fails, `main()` exits INVALID (2), and the tests assert 0 — the branch would have turned a vacuously-green workflow into a deterministically red one, while HANDOFF claimed the gate now works. Both tests now supply the artifact synthetically via monkeypatched `torch.load`. The guarantee under test is untouched: `from_pretrained` still raises if the checkpoint is loaded, which is what the case is actually about. `test_m3.py`'s copy is fixed too — pre-existing code, but this branch is what puts it in front of a working CI gate. The rejected alternative was `skipif(not lens.exists())`, which would green CI by re-hollowing the guarantee the CI fix just restored. Verified the way the reviewer reproduced it: exported tracked files only (`git ls-files | tar`, so `lenses/` holds just PROVENANCE.md) and ran the workflow's own loop there — 59/89/83/124/35/6 = 396 passed, rc=0. Also F7 (nice-to-have, a false claim I introduced in the previous commit): `PUBLISHED_VERDICTS`' comment said five docs quote those strings verbatim. They do not — the docs paraphrase the numbers in their own prose, so no doc edit could ever fail that test. Reworded to what the pin actually does (code <-> artifact only). Fixed rather than deferred because it is a one-line correction of an untrue statement about what a test guarantees, which is the same class of drift test_m2.py:660 exists to prevent. F6 (nice-to-have) is carried as a follow-up: the job named `offline-suites` now really fetches four Qwen2.5 tokenizers from HuggingFace, so its name and header comment overstate the offline claim. 390 of 396 pass with no network at all. Fixing it means choosing between caching ~/.cache/hugging face and adding a `network` marker to deselect — a workflow design call, not a correctness fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
F8 from PR #13's round-3 review. HANDOFF is this project's durable record (a wiki sentinel per CLAUDE.md) and the review mailbox lives outside the repo, so once this merges HANDOFF is the only in-repo account of the review — and it had frozen at round 1. - Three follow-ups, not two: F6 was accepted in round 2 and appeared nowhere in the repo. Recorded with the one line that matters operationally — the job named `offline-suites` now really fetches four Qwen2.5 tokenizer repos, 390 of 396 pass with no network, so a future red build there is network, not logic. - Recorded that F5's fix edited `test_m3.py`, a closed milestone's suite, so a later M3 audit is not surprised by a post-PASS edit. Scoped explicitly: one test changed; m3_matrix.py, its frozen GATE_WORDING and its published artifacts are untouched. - Replaced "verified locally to collect and run all 396 cases" with the stronger provable claim: CI is green on the real runner at 40e7c26. - Refreshed the stale suite counts (test_m4.py 112 -> 124, suite 384 -> 396) for the same reason. Docs-only; no code, workflow or artifact change. Suite: 396 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy
Adversarial review — CLEAR TO MERGE (rounds: 3)
Follow-ups (nice-to-have): F3 per-pair degeneracy texture (honouring the frozen clause would change the artifacts → a ~50-min re-run for a readout pre-declared non-verdict-bearing at n ≤ 3; the write-up owes a deviations row instead); F4 unguarded second battery parse (no reachable path); F6 Coverage: complete on all three rounds. Round 1 read the three behavioural files in full plus their neighbours, and audited the results JSONs by re-deriving every headline statistic from the raw recorded spans rather than reading the diff; rounds 2 and 3 each read their round's full diff. No round recorded an uncovered file-group. What the review established about the scienceThe reviewer independently recomputed the D9(b) oracle and the D20 residual selector over all 2,340 cells × 3 subjects from the raw The two should-fixes that mattered form a pair worth stating plainly: this PR added a 1,269-line test suite that CI was not running, and fixing that immediately exposed that two tests depended on gitignored 73 MB–587 MB lens weights. The first fix was verified by running the corrected loop; the second by exporting tracked files only ( No finding was disputed, so the neutral judge was never dispatched. Zero-context reviewer + neutral judge on disputes; anchored at |
Close-out stage M4, built to the frozen
docs/M4-BRIEF.md(D19–D22, frozen by Kyle 2026-07-29, (a) across the board, with two ratified post-freeze amendments to the D20 wording package).M3's own Honest limits stated the bound this closes: "the matrix measures collateral among 12 concepts, not across the vocabulary … Nothing here shows that deleting France spares the other 48 M1 concepts." M4 runs that experiment — keep the 12 characterized directions as the primes, widen the probes to all 60 M1 concepts.
Result
M4 VERDICT: VOCAB-SPARING at 1.5B AND 3B — AS-SCORED ONLY.
not shownThe qualifier fired, and it earned its existence. The concept-level collapse sits below the bar at both gate-bearing subjects while the item-level gate clears it — so per D20 those are the honest numbers to quote, and Amendment 1 (round 4, F20) is why they ride inside the verdict string rather than sitting in prose. Without it, the published label would have been a bare
VOCAB-SPARINGover a number the brief itself had already named as the honest one.What's in the diff
m4_strip.py— cut fromm3_matrix.py(never from the certifiedm0_anchor.py), own byte-frozenGATE_WORDINGverbatim from D20 including theAS-SCORED ONLYqualifier and both string templates. 2,340 cells/subject. The 255 M1-recorded and 468 M3-recorded cells (633 once the 90-cell overlap is counted once) are graded FIRST; any mismatch exits INVALID. D22's bars widened to all 60 scored words + the 12 direction words.oracle.pyuntouched — a fourth byte-shared consumer.m4_verdict.py— ownsstrip_verdict()(NOT A RESULT > DEGENERATE > UNDERPOWERED > the level bar; failing label the lineage nullnot shown) plus the cross-subject AND. The runner imports it, so one implementation of the verdict string exists.test_m4.py— 112 cases (full suite 384, green), pinning the six-round review's hard-won lessons: the residual selector selects exactly 2/2/0 gate-arm cells case-insensitively (case-exact selects 0; "fills the span" selects every cell), fail-in-place keeps the arm at 41/71/84,AS-SCORED ONLYattaches at bar level only, and every realized n and ceiling is cross-checked against the recorded M1 artifacts.docs/DECISIONS.md— D19–D22 appended, both amendments recorded (the second separately, since Amendment 1's ratification quote covers only Amendment 1).Discipline record
GATE_WORDINGfrozen as code and gates dry-run before the first real cell; six wrong-arm inputs exit INVALID with named reasons;--limit 18smoke reproduced its reused cells bit-for-bit and wrote nothing. Both re-certifications passed bit-for-bit on all three subjects (M1 255/255, M3 468/468,concept_massexact). Every pre-registered n and ceiling landed exactly — a run that disagreed would have been an INVALID cross-check, not a power surprise.Findings the gate never asked for
coppertakes 6/12 andmosquito8/12 at 1.5B,eagle8/12 andplatinum9/12 at 3B; 32/53 and 33/55 gated columns take zero collateral. Bimodal — which is why the item- and concept-level statistics diverge.Out of scope, flagged not fixed
.github/workflows/ci.ymlrunsuv run test_<name>.py, which executes each test file as a plain script; no test file has a__main__guard, so CI has been passing vacuously without running a test. The suites do run underuv run pytestlocally. One-line fix, belongs in its own PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01KkTj11WoNyTF9SGXvrxxVy