From ffb6f1a40f12e7d2b0a1d179e24a892fade390e2 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:41:43 -0600 Subject: [PATCH] =?UTF-8?q?docs(phase-26a):=20A2=20=E2=80=94=20the=20full?= =?UTF-8?q?=20audit;=2028=20findings=20survive;=20the=20endgame=20plan=20w?= =?UTF-8?q?as=20majority-fiction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 38 agents / 2.24M tok / 0 err. 32 findings raised -> 28 SURVIVED adversarial verification (4 REFUTED, 16 downgraded). 40 scanners measured CLEAN. Full write-up: docs/tooling-audit.md ROUND 2. THE ROOT CAUSE — one bug, ~10 times: a hand-maintained model of the corpus layout (a file allowlist, a single-.c assumption, a func_-only regex, a REGION_SUB dict) sitting on top of a filesystem that already answers the question. Every TU split silently widened it. DECAY PROVEN: .run/fuel_manifest.json (Jul 8) recorded 130 stubs; the same tool today returns 30. The Phase-26 splits moved ~100 stubs out from under a dict literal last edited in Phase 22 — and nobody noticed, because an un-nominated target produces SILENCE, not an error. MEASURED: 91.6% of ALL remaining project gain is invisible to target selection (true 994,633 ins; the manifest sees 83,305). 117 of 127 reach-134 fns never nominated. harvest_verify cannot see 56,742 of 58,717 (96.6%) open stubs. wave_targets hands 78 of 87 targets a nonexistent asm path. THREE RESULTS OVERTURN SETTLED CONCLUSIONS: 1. Phase-22's 'the permuter's fuel is exhausted' is UNSAFE. grinder banks through harvest_verify, which sees ONE TU — 1,290 of its own 1,298 queued fns live in another. 99% could never have banked. '0 banks since Phase 21' is equally consistent with 'the tool could not bank'. 2. The Phase-25/26 endgame plan is MAJORITY-FICTION. family-manifest.md advertises 2,758 multi-member families / 11.0 MB; 1,071 of them / 6.80 MB (62% of the byte-weight) are ALREADY FULLY MATCHED. The ranking — the file's whole purpose — is sorted mostly on dead work. 3. A CORPUS defect the byte-gate is structurally blind to: symbols.us.txt:981 puts a main-EXE DATA symbol (listCdBuffer = 0x80180000) into every overlay's symbol stack, but in overlay space that address is CODE. splat cuts 97 real functions in half and invents 96 phantom ones = 193 slices NOBODY CAN EVER MATCH, in 97 of 134 overlays — and the build stays byte-identical and green, because the .s halves are pasted back verbatim. A perfect correctness oracle, a null coverage oracle. What saved us: sig_image was RIGHT (58,524/58,621 vs spimdisasm; correct on all 97 disagreements). A SECOND INDEPENDENT ORACLE is the only reason it was visible at all. FIX RESTRUCTURED around the root cause: ONE derived corpus oracle (A3) + ~10 DELETED scanners — not ten fixed regexes. Plus the listCdBuffer corpus fix (A4) and the closeness oracle (A5, which lies on 155 functions, feeding false walls into reserved_walls()). decision-log (R31): the why, and the design lesson — a derived fact cannot rot; a hand-maintained copy of it is a liability that grows with every structural change. We had no instrument that could report ABSENCE: every gate we owned answered 'is this right?', none answered 'is this all?' --- .run/audit/a1_harvest_fuel.json | 13 + docs/decision-log.md | 68 +++ docs/tooling-audit.md | 810 ++++++++++++++++++++++++++++++++ phase-ends/CURRENT_PHASE.md | 9 +- 4 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 .run/audit/a1_harvest_fuel.json diff --git a/.run/audit/a1_harvest_fuel.json b/.run/audit/a1_harvest_fuel.json new file mode 100644 index 000000000..796cb4d19 --- /dev/null +++ b/.run/audit/a1_harvest_fuel.json @@ -0,0 +1,13 @@ +{ + "_comment": "A1 finding: 4 fns matched ONLY in ov_SC01_077, still INCLUDE_ASM in 133 overlays.", + "_why_never_propagated": "3 of 4 are defined in src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c, a _jr_* split file. dedup_propagate.overlay_files allowlists only ('_a','_o0','_o0b','_after') -- so the propagator could not SEE the definitions. The dedup group was registered anyway, and dedup_integrate's missing bank-check printed [ OK ] on the lie. Two silent-skip bugs compounding: one created the hole, the other hid it.", + "_harvest_in": "A5 (dedup_propagate overlay_files glob fix)", + "targets": [ + {"addr": "0x80128ED8", "nins": 53, "def": "src/ov_SC01_077/ov_SC01_077_a.c:451", "siblings": 133, "hidden_ins": 7049}, + {"addr": "0x8012C098", "nins": 21, "def": "src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c:449", "siblings": 133, "hidden_ins": 2793}, + {"addr": "0x8012C0EC", "nins": 42, "def": "src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c:470", "siblings": 133, "hidden_ins": 5586}, + {"addr": "0x8012C750", "nins": 52, "def": "src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c:594", "siblings": 133, "hidden_ins": 6916} + ], + "total_hidden_instructions": 22344, + "total_member_instances": 532 +} diff --git a/docs/decision-log.md b/docs/decision-log.md index 229096beb..4ae2263d6 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -774,3 +774,71 @@ build already proves. The tool that guards byte-honesty was the one least able t around it* — avoided a subtle honesty trap. Writing a PhaseEnd that says "milestone: not met, closing anyway" when the real story is "our measuring tape was short" would have been technically true and substantively misleading. **Phase boundaries should follow the work, not the paperwork.** + +--- + +## 2026-07-14 (session 9, A2) — The audit found the endgame plan was majority-fiction + +**What we ran.** 6 auditor agents over the 18 unaudited PARSE+GATE/SELECT tools, each finding handed to an +independent skeptic told to REFUTE it. 38 agents, 2.24M tokens. 32 findings raised → **28 survived**, 4 refuted, +16 downgraded, and **40 scanners measured clean**. The skeptic pass earned its keep: it killed four claims and +corrected magnitudes in both directions. + +**The root cause is singular, and it is not a regex.** Almost every finding is the same defect: + +> a hand-maintained model of the corpus layout — a file allowlist, a single-`.c` assumption, a `func_`-only +> symbol regex, a `REGION_SUB` dict — sitting on top of a **filesystem that already answers the question**. + +An overlay's source is spread over up to 14 `.c` files (`.c`, `_a`, `_o0`, `_o0b`, `_after`, and the Phase-26 +`_jr_` carves). Tools written when there was one file still believe there is one file. **The decay is +measurable:** `.run/fuel_manifest.json` from Jul 8 recorded 130 live stubs; the same tool run today returns **30**. +The Phase-26 splits moved ~100 stubs out from under a dict literal last edited in Phase 22. **Nobody noticed, +because a target that is never nominated produces silence, not an error.** + +**Why this is worse than a wrong answer.** 91.6% of all remaining project gain is invisible to the target-selection +layer (994,633 instructions of real work; the manifest sees 83,305). 117 of the 127 reach-134 functions — the +entire high-ROI band — are never nominated by anything. We were about to run Task 7's crack waves against that. + +**Three results overturn things we had written down as settled:** + +1. **"The permuter's fuel is exhausted" (Phase 22) is unsafe.** The grinder banks through `harvest_verify`, which + can only see one translation unit — and **1,290 of the grinder's own 1,298 queued functions live in a different + one**. 99% of its queue could never have banked, however good the permuter's output was. "7 all-time banks, 0 + since Phase 21" is *equally consistent* with "the tool could not bank" as with "there was nothing to bank." + We concluded the latter and moved on. **Re-test before repeating it.** + +2. **The Phase-25/26 endgame plan is majority-fiction.** `docs/family-manifest.md` — the document the whole + structural-family endgame is planned from — advertises 2,758 multi-member families holding 11.0 MB of hidden + leverage. **1,071 of them (6.80 MB, 62% of the advertised byte-weight) are already fully matched.** The + matched-set oracle scans a single overlay. So the byte-weight *ranking*, which is the entire purpose of the + file, is sorted mostly on dead work, and the real targets are buried under phantoms. + +3. **A corpus defect the byte-gate cannot see, and never could.** `config/symbols.us.txt:981` declares + `listCdBuffer = 0x80180000` — a main-EXE **data** symbol — and every overlay's splat config loads that file. In + overlay space, 0x80180000 is **code**. splat therefore cuts 97 real functions in half and invents 96 phantom + ones: **193 slices that cannot be matched by anyone**, across 97 of 134 overlays. You cannot write C for a + function that ends on a `lui` with no return, nor for one that begins by reading the assembler temp `$at`. They + sit in the harvest queue as ordinary work items, so agents burn on them indefinitely and the failure reads as an + intrinsic compiler wall. **And the full-binary byte-gate stays green the entire time**, because the `.s` halves + are pasted back verbatim in original order. + + This is the purest instance of the thesis that motivated the audit: *the byte-gate is a perfect correctness + oracle and a null coverage oracle.* And note precisely what rescued us — **`sig_image` was right.** Its + independently-computed function boundaries agree with spimdisasm on 58,524 of 58,621 functions, and on all 97 + disagreements sig_image is demonstrably correct. **A second, independent oracle is the only reason the defect + was visible at all.** That is a design lesson worth more than the fix: when one oracle is structurally blind to + a class of error, the answer is not a better assertion inside it — it is a second oracle that can disagree with it. + +**The fix follows the root cause: ONE derived corpus oracle, and ~10 deleted scanners.** Not ten fixed regexes. +`tools/corpus.py` answers — from the filesystem and the proven invariant, with coverage assertions baked in — +*which files make up a binary*, *which stubs are live*, *which functions are matched* (sig − stubs, derived, never +re-parsed), and *where a function's asm lives* (globbed, because splat already wrote the truth). Then the allowlists, +the `REGION_SUB` dict, the single-TU regexes, and `census_conflict_callees` in its entirety all get deleted. This is +the "best outcome is a deleted scanner" rule (R33) applied at scale. + +**Hindsight / for the wiki.** The strategic error was not writing any one of these tools badly. It was **letting the +corpus layout become a fact that lived in ten places**. Each split was a correct, well-gated change to the *build*; +none of them updated the ten private models of the tree, and nothing existed to notice. **A derived fact has no +maintenance cost and cannot rot; a hand-maintained copy of it is a liability that grows with every structural +change.** And the reason it stayed invisible for four phases is the deepest lesson of the audit: *we had no +instrument that could report absence.* Every gate we owned answered "is this right?" — none answered "is this all?" diff --git a/docs/tooling-audit.md b/docs/tooling-audit.md index 9d90dc530..0210ac44b 100644 --- a/docs/tooling-audit.md +++ b/docs/tooling-audit.md @@ -688,3 +688,813 @@ Same class of fix in jtbl_carve.all_data_labels: accept any `(?:dlabel|glabel)\s - **assertion to add:** `assert len(stub_map(ov)) + n_named_skipped == text.count("INCLUDE_ASM") - n_comment_lines` per overlay, and separately `assert extract_unit(ov,addr) is not None` for every addr family_hseq is about to label MATCHED (the two checks together make a named stub impossible to mistake for a match). + + +--- + +# ROUND 2 (Phase 26-A, task A2) — the remaining 18 GATE/SELECT tools + +> **38 agents · 2.24M tokens · 0 errors. 32 findings raised -> 28 SURVIVED adversarial verification, 4 REFUTED.** +> **40 scanners measured CLEAN** (listed at the end — do not re-audit them). +> Same method as round 1: an over-approximating candidate detector, `gap = candidates - parsed`, every item in the +> gap classified; then an independent SKEPTIC per finding, told to REFUTE it. The skeptics killed 4 findings and +> downgraded 16 — read the verdicts, not the raw claims. + +## THE ROOT CAUSE — one bug, ~10 times + +Almost every finding below is the SAME defect: + +> **A hand-maintained model of the corpus layout — a file allowlist, a single-`.c` assumption, a `func_`-only +> symbol regex, a `REGION_SUB` dict — sitting on top of a filesystem that already answers the question.** + +Every TU split silently widened it. An overlay's source is spread over up to 14 `.c` files (`.c`, `_a`, `_o0`, +`_o0b`, `_after`, and the Phase-26 `_jr_` carves). Tools written when there was one file still believe there +is one file. **Proof of decay:** the on-disk `.run/fuel_manifest.json` (Jul 8) recorded 130 stubs; the same tool run +today returns **30** — the Phase-26 splits moved ~100 stubs out from under a dict literal last edited in Phase 22. +Nobody noticed, because a target that is never nominated produces silence, not an error. + +**The R33 answer is therefore singular: ONE derived corpus oracle, and ~10 deleted scanners.** See A3. + +## THE HEADLINE NUMBERS (all measured, all skeptic-confirmed) + +| what | measured | +|---|---| +| Remaining project gain **invisible to target selection** | **91.6%** — true 994,633 ins; the manifest sees 83,305 | +| reach-134 functions never entering the manifest | **117 of 127** (the entire high-ROI band) | +| `grinder`'s own queue that **can never bank** | **1,290 of 1,298 (99%)** | +| Open overlay stubs `harvest_verify` **cannot see** | **56,742 of 58,717 (96.6%)** | +| `family_manifest` multi-member families that are **already fully matched** | **1,071 of 2,758 — 62% of the advertised byte-weight** | +| Permanently-unmatchable stub slices from **one bad symbol line** | **193** (97 truncated + 96 phantom), across 97 of 134 overlays | +| Functions given a **provably-wrong** closeness score | **155** | +| `wave_targets` targets handed an asm path **that does not exist** | **78 of 87** | + +### The three that change what we believe + +1. **The Phase-22 verdict "the permuter's fuel is exhausted" is unsafe.** `grinder` banks through + `harvest_verify`, which can only see one TU — **99% of the grinder's own queue could never have banked**, no + matter how good the permuter's output was. "0 banks in Phase 22" is equally consistent with "the tool could + not bank" as with "there was nothing to bank." **Re-test before repeating that conclusion (A11).** +2. **The Phase-25/26 endgame plan is majority-fiction.** `docs/family-manifest.md` advertises "2,758 multi-member + families / 11.0 MB of hidden leverage." **1,071 of them / 6.80 MB are already fully matched** — phantom targets, + 62% of the advertised byte-weight — because the matched-set oracle scans ONE overlay. The byte-weight RANKING, + which is the entire point of the file, is sorted mostly on dead work. True frontier: 1,475 families / 3.9 MB. +3. **A corpus defect the byte-gate is structurally blind to.** `config/symbols.us.txt:981` declares + `listCdBuffer = 0x80180000` — a main-EXE **data** symbol — and every overlay's splat config loads that file. But + in overlay space 0x80180000 is **code**. splat therefore cuts 97 real functions in half and invents 96 phantom + ones: **193 slices that can never be matched by anyone** (you cannot write C for a function that ends on a `lui` + with no return, nor one that begins by reading `$at`). They sit in the harvest queue as ordinary work, so agents + burn on them forever and the failure reads as an intrinsic compiler wall. **The full-binary byte-gate stays + green throughout** — the `.s` halves are pasted back verbatim in original order. This is the purest example yet + of the audit's thesis: *a perfect correctness oracle, and a null coverage oracle.* + Note what saved us: `sig_image` was **right** and independently contradicted the corpus. The second oracle is + what made the defect visible at all. + +## THE SURVIVING FINDINGS (skeptic-verified severity) + +### [CRITICAL] `tools/build_fuel_manifest.py` — CONFIRMED +- **scanner:** live_stubs() — the hardcoded 3-file allowlist at tools/build_fuel_manifest.py:79-80 (`files = {"main": src//.c, "a": ..._a.c, "o0": ..._o0.c}`), applied to STUB_RE at :86 +- **counts:** candidates **264** / parsed **30** / real skips **233** +- **evidence:** ov_SC01_077 has 14 .c files. `grep -c INCLUDE_ASM src/ov_SC01_077/*.c` → 267 lines; STUB_RE matches 264 unique symbols (the 3 non-matches are prose in comments — justified). live_stubs() reads only 3 of the 14 files and returns 30. Measured, today: `.venv/bin/python tools/build_fuel_manifest.py` prints `live stubs: 30`. The 234 skipped stubs, by file: ov_SC01_077_jr_8015AE2C.c=45, ov_SC01_077_after.c=44, _jr_8012ACE0.c=36, _jr_80178D40.c=26, _jr_801734BC.c=24, _jr_8016AB6C.c=22, _jr_80182268.c=22, _jr_8015444C.c=11, _jr_801380E0.c=4. Concrete: src/ov_SC01_077/ov_SC01_077_jr_801734BC.c:INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80178004); — reach=134, nins=165, gain=22,110 ins — the SINGLE highest-ROI remaining function in the whole overlay, and it is invisible to every tool downstream. Ditto func_8014F4C0 (after.c, reach 134, 141 ins, 18,894) and func_8014ADE0 (after.c, reach 134, 139 ins, 18,626). ZERO of the 234 are justified exclusions: every one is a live INCLUDE_ASM stub with a real .s on disk. +- **blast radius:** corrupt SELECTION + corrupt METRICS, LIVE. Measured against the real corpus by monkeypatching live_stubs() to a full glob and re-running the manifest (.run/audit/target-selection/fm_full.json vs fm_now.json): true remaining byte-weighted gain = 994,633 ins across 263 func_ stubs; the manifest sees 83,305 ins across 30 → **91.6% of ALL remaining project gain is invisible**. 117 of the 127 reach-134 functions (the entire high-ROI band) never enter the manifest. 51 of the 234 appear in NO other artifact either (not in .run/backlog.jsonl) — they have never been seen by any tool in the project. Downstream: worklist.py (100% of its rows), wave_targets.py (100% of its pools), and the ROI ranking in docs/worklist.md all consume this manifest and inherit the hole verbatim. +- **R33:** KEEP the scanner, DELETE the allowlist. This scanner is NOT re-deriving a proven invariant — the INCLUDE_ASM stub set IS the invariant (a function not wrapped in INCLUDE_ASM is byte-exact), so enumerating stubs is the correct primitive and cannot be replaced. What must die is the hand-maintained list of filenames: that is a second, decaying model of the corpus layout sitting on top of a filesystem that already answers the question. Every Phase-19/26 split silently widened the hole because nobody updated a dict literal. +- **fix:** Delete the allowlist. `for p in sorted(glob.glob(f'src/{source}/{source}*.c'))` and derive the region tag from the filename suffix (`main` for the bare file, else the suffix: `a`, `o0`, `o0b`, `after`, `jr_`). This is exactly what tools/wave_targets.py:55 already does correctly — promote that glob into ONE shared `tools/live_stubs.py` helper and have build_fuel_manifest, wave_targets and exemplar_miner all import it, so the corpus can never again be enumerated three different ways. +- **assertion (R32):** In build_fuel_manifest: `raw = sum(1 for p in glob(f'src/{ov}/{ov}*.c') for l in open(p) if 'INCLUDE_ASM(' in l and not l.lstrip().startswith('//'))` then `assert len(stubs) + len(named_skipped) == raw, f'live_stubs coverage hole: parsed {len(stubs)} of {raw} INCLUDE_ASM stubs'`. Hard-fail, do not warn. +- **skeptic:** Tried to refute; could not. RAN: (1) globbed all 14 src/ov_SC01_077/*.c with the tool's own STUB_RE/FUNC_RE -> 264 unique symbols = 263 func_ + 1 named (listCdBuffer, src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:5549); replayed the hardcoded 3-file dict -> 30. Real silent skips = 233, NOT 234 — the claim's 234th is listCdBuffer, which FUNC_RE excludes on purpose and the tool reports as named_stubs_skipped (a justified exclusion; per-file _jr_80178D40 is 25, not 26). Every other per-file count matches. (2) Ran `.venv/bin/python tools/build_fuel_manifest.py` -> "live stubs: 30". Reproduces. (3) Justified-exclusion test: globbed asm/ov_SC01_077/nonmatchings/*/.s for each skipped symbol -> 233/233 have a real .s on disk. All are live INCLUDE_ASM stubs in ordinary Phase-19/26 TU splits; none are justified. (4) Blast radius: monkeypatched live_stubs() to a full glob and re-ran main() otherwise unmodified -> full = 263 targets / 994,633 byte-weighted ins / 127 reach-134; current = 30 / 83,305 / 10. 91.6% of remaining project gain invisible; 117 of 127 reach-134 fns never enter the manifest. Top invisible items reproduce exactly (func_80178004 reach134/165ins/22,110; func_8014F4C0 18,894; func_8014ADE0 18,626). (5) LIVE, not latent: grep of the checked-in docs/worklist.md and .run/worklist.json shows ZERO mentions of func_80178004, func_8014F4C0, func_8014ADE0 and omits 197 of the 233 — the corrupt ranking is already the artifact on disk. worklist.py and wave_targets.py --rclass have no target source other than this manifest, so 100% of their rows/pools inherit the hole. 50 of 233 (claim said 51) are absent from .run/backlog.jsonl too. (6) Decay proven: on-disk .run/fuel_manifest.json (Jul 8) has total_stubs=130; same tool today returns 30 — the Phase-26 splits (src files Jul 13-14) moved ~100 stubs out from under a dict literal last edited in Phase 22. (7) Evidence lines verified: allowlist at tools/build_fuel_manifest.py:79-80, STUB_RE.findall at :86, cited stub at src/ov_SC01_077/ov_SC01_077_jr_801734BC.c:3304. ADDITION the claim missed: tools/wave_targets.py:23 holds a SECOND copy of the same decaying layout model (REGION_SUB = main/a/o0) — deleting the allowlist alone yields targets whose region cannot map to an asm subdir, so both must be fixed. + +### [HIGH] `harvest_verify.py (+ its 3 no-src callers)` — DOWNGRADED +- **scanner:** tools/harvest_verify.py:59 — `stubbed = set(re.findall(r'INCLUDE_ASM\("' + re.escape(a.asm_subdir) + r'", (func_[0-9A-Fa-f]+|DsMix)\)', src0))` — scans EXACTLY ONE file (`a.src`) and silently drops every draft not stubbed in it +- **counts:** candidates **58717** / parsed **1975** / real skips **56742** +- **evidence:** The claim 'harvest_verify derives from make build + SHA1, so a parse hole makes it CONSERVATIVE not wrong' is TRUE for the *acceptance* decision and FALSE for the *coverage* decision. Line 59 decides WHICH drafts get to see the gate at all — and it can only ever see one TU. + +MEASURED fleet-wide (over-approximating detector = `INCLUDE_ASM("...", );` over every src/**/*.c): + 134 overlays: 1975 stubs live in src//.c ; 56742 live in the SPLIT TUs (_jr_*, _after, _a, _o0). + -> 96.6% of all open stubs are in a file harvest_verify was never pointed at. + (1200 _jr_*.c files exist and hold 50399 stubs — 83% of the fleet's total.) + +THREE of the six gate_stage callers pass NO --src, so run_gate defaults to src//.c: + tools/orchestrator.py:89 `gate_stage.run_gate(a.drafts, source_tag="worker", commit=a.commit)` + tools/grinder.py:191 `gate_stage.run_gate(DRAFTS, binary=binary, source_tag="grinder", commit=True)` + tools/idiom_hunt.py:222 `gate_stage.run_gate(os.path.relpath(d, REPO), binary=b, propagate=False, ...)` +(bulk_harvest.py:85, lora_grind.py:154 and recover_integration.py:138 DO loop the splits — they are clean.) + +ov_SC01_077, THE canonical harvest binary and orchestrator.py's hard default: 13 open stubs in the main .c, 251 in the splits, 264 total. The worker-wave gate can bank at most 4.9% of it. + +GRINDER, measured against its OWN work queue (.run/backlog.jsonl open near-misses, unique fn+binary): 8 are hosted in a main .c; 1290 are hosted in a SPLIT TU. The grinder feeds those to decomp-permuter, gets an output-0-* byte-match, hands it to run_gate with no --src — harvest_verify's line-59 set does not contain the fn, render() is never called, the draft is dropped, and gate_stage re-logs it as a failure. +- **blast radius:** LOST MATCHES, live, large. (a) grinder.py: 1290 of 1298 (99%) of its own queue can never bank — a true permuter byte-match for any of them is silently discarded today. (b) orchestrator.py worker waves: capped at 13/264 (4.9%) of ov_SC01_077. (c) fleet: 56742 of 58717 open overlay stubs (96.6%) unreachable by these three paths. Never a WRONG bank (the SHA1 gate still arbitrates) — purely invisible work. This is the same shape as the 9-function 'compiler wall' that was really a coverage hole. +- **R33:** DELETE THE SCANNER. Two independent reasons. (1) The regex is REDUNDANT: render() already does `if line not in s: return None` — a literal-substring containment check that works for ANY symbol name and any TU. The line-59 set adds nothing but a false narrowing. (2) The stub's home file is a FACT of the tree, not something a caller should guess; harvest_verify re-derives it from a caller-supplied --src that three of six callers get wrong. Derive it from the tree. This is the single highest-value deletion in the group. +- **fix:** Delete the --src/--asm-subdir coupling. harvest_verify should take --binary and DERIVE the map: scan src//*.c once, build {fn -> (file, exact stub text)} from `INCLUDE_ASM("[^"]*",\s*(\w+)\);`, splice each draft into whichever TU actually holds its stub, and run ONE `make build BINARY=` — every TU links into the same binary, so a single SHA1 check gates them all (this is also strictly faster than today's per-split re-gate). The per-caller --src argument then cannot be got wrong, and orchestrator/grinder/idiom_hunt need no change. +- **assertion (R32):** R32: harvest_verify must print and assert `gated + no_stub_anywhere == len(glob(drafts/*.c))`, and DUMP the no_stub_anywhere list by name. A draft that is a live stub in the binary but was not gated is a hard error, not a silent 0. Equivalently: assert that the set of drafts it declines to gate is exactly the set with no INCLUDE_ASM stub anywhere in src//*.c. +- **skeptic:** RAN: (1) read harvest_verify.py:1-134, gate_stage.py:1-289, and the 3 call sites — all four cited file:line locations exist verbatim (harvest_verify.py:59 scanner; orchestrator.py:89, grinder.py:191, idiom_hunt.py:222 all call run_gate with NO --src, so gate_stage.py:_run_gate_locked resolves src=src//.c). (2) Re-derived counts with my own over-approximating regex INCLUDE_ASM("...", ) over src/**/*.c: 1975 main / 56742 split / 58717 total, 134 overlays, 1200 _jr_ files (50398 stubs, claim said 50399 — off by one), ov_SC01_077 = 13 main / 251 split / 264. COUNTS REPRODUCE EXACTLY. These are real skips, not justified exclusions. + +STRONGER EVIDENCE THAN THE CLAIM GAVE: I correlated the grinder's persisted blacklist (.run/auto/grinder_blacklist.json — fns where the permuter WON a byte-match and the gate rejected it, so grinder permanently marked them "plumbing-bound, never re-permute") against per-(fn,binary) host files: 16 of 22 are SPLIT-hosted in ov_SC01_077, 1 main, 5 ABSENT. harvest_verify never compiled those 16 — the permuter byte-matches were discarded unbuilt. The 5 ABSENT ones have no stub and no .s today = they matched anyway, proving the "plumbing-bound" verdict was manufactured by this hole. Realized, persisted lost work. Also confirmed orchestrator's cap: fuel_manifest has 130 targets, only 13 are main-hosted in 077 (57 split-hosted, 60 not in 077). + +WHY DOWNGRADED, NOT CONFIRMED — three corrections: +(a) THE PRESCRIBED FIX IS A NO-OP. R33 reason (1) asserts render()'s `if line not in s` is a containment check "that works for ANY symbol name and any TU". FALSE: s = baseline = src0 = the single --src file's text, and the tool only ever writes open(a.src,'w'). I SIMULATED deleting line 59 against the real 077 tree: all 5 tested split-hosted fns (func_8012E014/80138DE0/8016130C/8016F0E4/8017F290) still return None from render() — the STUB literal is absent from the main .c, and the asm-subdir substring differs too (.../ov_SC01_077_jr_* vs .../ov_SC01_077). Deleting the scanner banks ZERO extra fns; it only turns a silent skip into a logged failure. harvest_verify is a single-TU tool BY CONSTRUCTION; the defect is mislocalized — it lives in the 3 CALLERS that fail to loop the splits (bulk_harvest/lora_grind/recover_integration/family_sweep already do). +(b) "LOST MATCHES, live, large... silently discarded today" is overstated. The grinder is STOPPED: .run/auto/STOP present since 2026-07-02, heartbeat state "stopped" (2026-06-30, banked 0). The Phase 25/26 production path (family_sweep, CURRENT_PHASE Task 4) IS split-aware. Nothing is being discarded right now — the damage is REALIZED-AND-PERSISTED (the poisoned 22-fn blacklist) plus latent-on-restart, not ongoing. +(c) Grinder queue magnitude inflated: replaying its own candidates() filters (status=near, best_draft exists, closeness<=30, nins<=220) I get 1252 unique (fn,binary) — 1263 split records / 5 main — not 1298/1290/8. Direction identical (~99.6% unbankable), number modestly wrong. + +corrected_real_skips = 56742 stands as the scanner-coverage hole (reproduced exactly), but note only ~1252 are skips with a draft actually offered, and 16 are demonstrated realized discards. Severity HIGH not CRITICAL: no wrong bank is ever possible (the SHA1 gate still arbitrates), the 3 broken callers are off the current production path, and the headline remediation is a no-op. + +### [HIGH] `harvest_verify.py` — CONFIRMED +- **scanner:** tools/harvest_verify.py:37 — `_TD = re.compile(r'^[ \t]*typedef\b.*\b(u8|u16|u32|u64|s8|s16|s32|s64|f32|f64)[ \t]*;[ \t]*\n', re.M)`; used by strip_typedefs() to prevent C89 redefinition errors when a self-contained draft is spliced into a TU that already has the types +- **counts:** candidates **5638** / parsed **4774** / real skips **385** +- **evidence:** Over-approximating detector = any line matching `^[ \t]*typedef\b` in a real draft. Run over all 10731 draft .c under .run/drafts*/ and .run/auto/bulk/*/: + candidate typedef lines 5638 ; stripped by _TD 4774 ; GAP 864. +Classified all 864: + 391 multi-line struct openers (`typedef struct {`) -- UNCLASSIFIED, see caveat + 239 REDEFINE a type src/shared/engine_types.h ALSO defines <-- REAL SILENT SKIP + 189 genuinely new draft-local types -- JUSTIFIED (no redefinition) + 45 unclassified +The 239 are names the fleet's own hoisted-type header already owns: Blk16 x43, Blk20 x32, Blk8 x21, S8 x18, Blk32 x16, SV4 x15, uint x12, B8 x11, SVEC x9, PosT x8, MoveT x8, Buf32 x7 ... Each is defined in src/shared/engine_types.h (e.g. engine_types.h:477 `typedef struct { u32 a, b, c, d; } Blk16;`, :570 `typedef unsigned int uint;`), which reaches ALL 1605 game .c via engine_core.h. +_TD only strips typedefs whose TARGET NAME is one of the ten scalar aliases — a struct typedef can never match it. +PROVEN a hard error with the real compiler (tools/bin/gcc-2.7.2-cdk/cc1, -O2 -G0): + redef.c:3: conflicting types for `Blk16' + redef.c:2: previous declaration of `Blk16' +So the TU does not compile, `make build` fails, build() returns None, and harvest_verify files the draft as a NON-MATCH — indistinguishable in the record from 'the compiler will not reproduce these bytes'. +CAVEAT (honest lower bound): my classifier only detects single-line `typedef struct {...} X;`. The 391 multi-line openers close on a `} X;` line that does not begin with `typedef`, so they are NOT in the 239 and some are certainly redefinitions too. 239 is a floor. +- **blast radius:** LOST MATCHES, live. 59 DISTINCT drafts on disk carry a typedef that engine_types.h also defines — each one is a guaranteed compile failure, recorded as a byte-mismatch. 22 of those functions are STILL open INCLUDE_ASM stubs today (func_8012A1BC, func_8012A598, func_8012B4B8, func_8012EA90, func_80133298, func_801425CC, func_80144090, func_8014D820, ...). Those 22 have never once been given the chance to compile, let alone to match. Upper bound on recovery is 22 (they must still be byte-exact) but the floor on the defect is certain: 22 functions were judged by a gate they never reached. +- **R33:** FIX BY DERIVATION, not by extending the regex. The ten-name allowlist (u8|u16|...|f64) is a frozen snapshot of a header that has since grown — engine_types.h is generated and keeps growing, so any hand-maintained allowlist will silently rot again the next time a type is hoisted. The set of provided type names is a FACT of engine_types.h. Read the header. This scanner cannot be deleted (something must reconcile draft-local typedefs against the TU) but its hardcoded list must be. +- **fix:** Derive the strip-set instead of hardcoding it: parse src/shared/engine_types.h (+ common.h) once for every typedef'd name it provides, then strip ANY typedef in a draft — single- or multi-line, scalar or struct — whose declared name is in that set. Keep draft-local types that collide with nothing. +- **assertion (R32):** R32: after strip_typedefs(), assert no surviving `typedef ... ;` in the spliced text names a header-provided type; and assert that when a chunk fails to build, cc1's diagnostics contain no 'conflicting types for' / 'redefinition of' — if they do, that is a PLUMBING failure and must be reported as such, never as a byte-mismatch. +- **skeptic:** I attacked this on five fronts and it survived all of them; where I corrected the claim, it was because the claim UNDER-stated the damage. + +1. COUNTS REPRODUCE. Re-derived from scratch over all 10733 draft .c under .run/drafts*/: candidates=5641, _TD-stripped=4774, GAP=867 (claim: 5638/4774/864 — within file-set noise). Note .run/auto/bulk/*/ is EMPTY (0 dirs), so half the claimed corpus glob contributed nothing — immaterial to the totals. + +2. EVIDENCE EXISTS AT THE CITED LINES. tools/harvest_verify.py:37 is exactly the 10-name regex quoted. src/shared/engine_types.h:477 `typedef struct { u32 a, b, c, d; } Blk16;` and :570 `typedef unsigned int uint;` — both verbatim at the cited lines. Brace-matched extraction shows engine_types.h defines 46 real typedef names (Ent, Obj, Prim, Vec3, SrcB964, Blk20... all genuinely there — I checked the raw grep, they are not parser artifacts). + +3. MECHANISM IS A HARD ERROR, NOT A WARNING. Ran the real tools/bin/gcc-2.7.2-cdk/cc1 -O2 -G0 on a 3-line repro: `A.c:3: conflicting types for 'Blk16'` → exit=33 (nonzero). Control with no redefinition → exit=0. So the TU genuinely does not compile. + +4. IT IS GENUINELY SILENT. Read the rest of harvest_verify.py: build() runs `make` with capture_output=True and THROWS THE STDERR AWAY, returning None on failure; attempt() is `build() == good_sha`; the failure branch does `failed.append(fn); print('- %s (%s)' % (fn, conf))`. A compile failure and a byte-mismatch produce a byte-identical record. The operator cannot distinguish them. This is the definition of a silent skip, and it is in the project's CENTRAL byte-gate. + +5. THE GAP IS NOT A JUSTIFIED EXCLUSION — AND IS BIGGER THAN CLAIMED. The claim's classifier only handled single-line typedefs and honestly flagged 391 multi-line openers as unclassified. I wrote a brace-matching extractor that resolves them. Result: 385 gap typedef occurrences (across 331 distinct draft files) name a type the TU already provides via engine_types.h/common.h — every one an uncompilable redefinition. The claim's 239 was indeed a floor; the true figure is 385. The other 495 gap occurrences are genuinely new draft-local types and ARE a justified exclusion. + +6. BLAST RADIUS IS LIVE, NOT LATENT — AND LARGER. Mapped colliding drafts → still-INCLUDE_ASM functions → target TU: 41 still-open functions have a colliding draft; 39 of them sit in TUs that actually include engine_core.h (only 2 escape). All 8 functions the claim named by hand (func_8012A1BC, func_8012A598, func_8012B4B8, func_8012EA90, func_80133298, func_801425CC, func_80144090, func_8014D820) are in my independently-derived set. The claim said 22; it is really 39. Smoking gun for LIVE: 8 of those 39 (func_8013BD74, func_801425CC, func_80144090, func_80146A6C, func_8014CF04, func_8015DAF8, func_8017B490, func_80184278) are already RECORDED AS FAILED in on-disk gate records (.run/hseq_failed.*, .run/harvest_wave_*.js) — they were fed to a gate, could not possibly compile, and were filed as byte-mismatches. That is realized damage, not a latent shape. + +ONE CORRECTION AGAINST THE CLAIM (does not save it): "engine_types.h reaches ALL 1605 game .c via engine_core.h" is FALSE — only 1470 of 1683 src .c include engine_core.h; 213 TUs never see engine_types.h. But 39 of the 41 affected functions live in TUs that DO include it, so the conclusion is unharmed. + +SEVERITY: HIGH is correct, not CRITICAL. The failure mode is false REDs (lost matches), never false GREENs — the SHA byte-gate remains sound and no wrong code can be accepted. It cannot corrupt the binary. But it is live, sits in the central gate, silently discards up to 39 recoverable functions, and self-perpetuates: engine_types.h is generated and keeps growing (git log shows it grew across Phases 15/19/20), so the frozen 10-name allowlist rots further with every hoisted type. The R33 "fix by derivation — read the header, don't extend the regex" verdict is the right remedy. + +Scratch: /home/musashi/bfm-decomp/.run/audit/skeptic/ (gap.json, engine_types_names.json, collide_files.json, live.json, A.c/B.c/C.c cc1 repro). No builds run, no repo edits. + +### [HIGH] `tools/family_manifest.py` — DOWNGRADED +- **scanner:** the MATCHED-SET oracle — `src077 = glob("src/ov_SC01_077/*.c")` + `stub077` + `matched_hex = {hx for v,(n,hx,hn) in sig077.items() if v not in stub077}` (tools/family_manifest.py:33-36); it scans ONE of 134 overlays for stubs and calls everything else in the fleet unmatched +- **counts:** candidates **51722** / parsed **2252** / real skips **49475** +- **evidence:** Over-approximating detector: apply the tool's own ground-truth rule (a sig function with no INCLUDE_ASM stub in ITS OWN overlay's src is byte-exact) to all 134 overlays instead of only ov_SC01_077. Measured (.run/audit/hseq/a6.py, a7.py, current src): + tool matched_hex (ov077 non-stubs + dedup.us.yaml hashes) = 2,252 h_exact classes + true matched_hex (any overlay's own stubs + dedup) = 51,722 h_exact classes + gap = 49,471 classes = 56,808 instances = 10.68 MB of code that ALREADY HAS C somewhere in the fleet and is still ranked as an unmatched target. +Every one of the 49,471 is a real skip; ZERO are justified exclusions (each is a function whose C body exists in some overlay's src today, exactly the condition matched_hex is meant to express). Cause: since Phase 25 the family_remap/family_sweep campaigns bank matched bodies DIRECTLY into each overlay's own .c (not through config/dedup.us.yaml), so the ov077-only oracle went blind to 133/134 of the fleet's matched code. +Rebuilding the manifest both ways (.run/audit/hseq/a7.py): + TOOL : 44,087 h_norm families | multi-member 2,757 / 10.9 MB + FIXED: 22,730 h_norm families | multi-member 1,475 / 3.9 MB +- **blast radius:** CORRUPT SELECTION — LIVE, and it is the Phase-25/26 endgame plan itself. docs/family-manifest.md (committed 2026-07-10) advertises "multi-member (hidden leverage): 2758 / 11.0 MB". Measured against the real corpus TODAY: 1,071 of those multi-member families / 6.80 MB are ALREADY FULLY MATCHED — pure phantom targets, 62% of the advertised byte-weight. A further 253 families have byte-weight inflated by already-matched members (+0.14 MB). The byte-weight RANKING (the whole point of the file — 'draft these first') is therefore sorted mostly on dead work, so real targets are buried below phantoms. The 3-lever split in the digest is likewise wrong: families matched in some other overlay but not in ov077 are filed as 'draftable'/'absent' instead of free. True frontier: 1,475 families / 3.9 MB. +- **R33:** YES — DELETE, this is the poster child for R33. The proven invariant ('a function not wrapped in INCLUDE_ASM is byte-exact') is per-overlay and already fully implemented in family_hseq.load(). family_manifest re-derives it from ONE overlay's C text and thereby invents a matched set 23× too small. The correct outcome is not a fixed regex but a deleted scanner + one shared invariant-derived loader. +- **fix:** DELETE the matched-set derivation from family_manifest.py. Move family_hseq.load()'s per-overlay ground truth (stub-scan every src/ov_*/*.c, matched := addr not stubbed in its OWN overlay) into a single shared loader module (e.g. tools/fleet_truth.py) and have BOTH family tools import it; matched_hex := {h_exact of every non-stub sig function in any overlay} | dedup hashes. Keep the h_norm grouping/ranking; it is the oracle underneath that must go. +- **assertion (R32):** R32: after building matched_hex, assert its instance-weighted coverage equals progress.py --weighted's fleet matched count (currently 82.8% fn / 283,050 non-stub fns) within 0; the ov077-only oracle would report 2,252 classes and fail instantly. Plus: assert matched_hex is a superset of every dedup hash AND of every non-stub sig addr in every overlay. +- **skeptic:** RAN (all read-only, scratch in .run/audit/skeptic/): s1.py validated the invariant over all 134 overlays (341,671 sig fns; 58,617 stubs; ZERO stubs outside sig; every overlay has .c) — "no INCLUDE_ASM stub in own src => byte-exact" is sound and is already implemented by tools/family_hseq.py:31-35 (docstring: reproduces progress.py 74.8/58.2/30.3 exactly). s2.py reimplemented family_manifest.py:33-37 verbatim vs the per-overlay oracle: TOOL matched_hex = 2,252 (exact match to claim); TRUE = 51,727; GAP = 49,475 (claim said 49,471 — noise); tool-minus-true = 0 (strict subset). Manifest rebuild: TOOL 44,087 fams / multi 2,757 / 10.9 MB vs FIXED 22,727 / 1,475 / 3.9 MB — reproduces the claim. s4.py: 1,071 fully-matched phantom multi-member families / 6.80 MB (exact) + 254 partially inflated / +0.14 MB (exact); 42 of the 60 PUBLISHED top-60 rows are fully-matched phantoms. s5.py + shell sweep ground-truthed the #1 published row func_80133CD4 (399 ins, GIANT): 0 overlays still stub it, 134 have a C body (banked by commit:0581), yet it is still rep_addr of row #1 in .run/family_manifest.json with bw=213864 — a 100%-banked function ranked as the top target. s3.py reconstructed the corpus at commit commit:0506 via git ls-tree/git show: tool saw 2,211 vs true 27,305 — the committed doc was ALREADY 12x wrong when written, not merely rotted. Evidence file:lines exist exactly as cited (family_manifest.py:33-36). + +TRIED TO REFUTE, FAILED: the gap is NOT justified exclusions. Every one of the 49,475 classes is a function whose C body exists in some overlay's own src today — exactly the condition matched_hex is defined to express. No overlay lacks src, no stub falls outside sig, no not-yet-split escape hatch. Real skips = 49,475, zero justified. + +WHY DOWNGRADED FROM CRITICAL: the blast radius is overstated. `grep -rn "family_manifest.json" tools/ Makefile` = ZERO consumers. family_sweep computes its own per-overlay stubs (family_sweep.py:50-52) and its --hseq path reads family_hseq.json, not this. The Phase-26 endgame (docs/family-endgame-megaplan.md, CURRENT_PHASE Tasks 2/4, the heaviest-byte-weight-first curriculum) runs entirely on h_seq via family_hseq.py, which uses the CORRECT oracle; docs/family-hseq.md (2026-07-13) is the live map, and the Phase-25 close explicitly deprecated h_norm as over-fragmenting. So "CORRUPT SELECTION — LIVE, and it is the Phase-25/26 endgame plan itself" is false: no automated path selects from this tool, and the whole-binary byte-gate makes a corrupt BANK impossible — the worst case is wasted drafting. Real, non-latent damage remains (committed docs/family-manifest.md is a 62%-phantom-by-byte-weight target doc, 42/60 of its published ranking dead, still cited by docs/matching-cookbook.md:2433 and PhaseEnd_Phase25.md, and any agent re-running the tool gets a 23x-too-small matched set) — that is human-mediated misdirection, i.e. HIGH, not CRITICAL. The R33 DELETE verdict stands independently: the invariant is already correctly implemented in family_hseq.load(). + +### [HIGH] `tools/family_hseq.py + tools/family_manifest.py` — CONFIRMED +- **scanner:** the sig-corpus ingest — `for p in sorted(glob.glob(".run/sig.ov_*.jsonl"))` in family_hseq.load() (tools/family_hseq.py:40-51) and family_manifest.py:20-30. It takes the sig corpus as the complete function universe and NEVER checks it against the stub universe it just parsed from src. +- **counts:** candidates **58717** / parsed **58621** / real skips **96** +- **evidence:** Over-approximating detector: the stub universe itself. Every INCLUDE_ASM stub is, by the invariant, a real unmatched function that MUST appear in the sig corpus. Measured (.run/audit/hseq/a1.py, a5.py): + stub addresses parsed from src/ov_*/*.c (symbol-agnostic) = 58,717 + stub addresses that have a sig record = 58,621 + stub addresses with NO sig record = 96 (all at 0x80180000, across 96 distinct overlays) +These are NOT empty: reading the assembly the build actually pastes, asm/ov_*/nonmatchings/*/listCdBuffer.s (100 files exist), the 96 sig-less ones total 13,504 instructions — nins range 3 .. 1,768, median 37, and 30 of them are >=80 ins (family_hseq's SUBSTANTIAL campaign band). ZERO justified exclusions: all 96 are stubbed, live, byte-real, unmatched code. +Root cause is upstream (config/symbols.us.txt:981 tags 0x80180000 `// data`, so sig_image drops it as data in 96 of the 100 overlays that carry it) — but BOTH family tools consume the hole in silence and neither can see it. +- **blast radius:** LOST TARGETS — LIVE, and this is the 'invisible to planning' failure exactly. 96 real unmatched functions / 13,504 instructions (incl. one 1,768-ins and two 866-ins monsters, 30 substantial-band in total) appear in NO record of .run/family_hseq.json, NO record of .run/family_manifest.json, and in NEITHER tool's denominator — so they are simultaneously un-nominatable AND invisible in the % complete. They are not phantom-matched; they simply do not exist as far as the entire Phase-26 target map is concerned. Nobody will ever look for them, which is why this survived. +- **R33:** PARTIAL DELETE. The sig corpus is legitimate (it signs the ORIGINAL bytes, immutable w.r.t. src) and cannot be derived from the invariant — keep it. But the tools must stop treating it as self-evidently complete: its completeness IS derivable, by cross-checking it against the INCLUDE_ASM stub set the same loader already builds. The check costs one set-difference and would have caught this on day one. +- **fix:** Fail closed in the shared loader: after parsing stubs and sigs, `missing = stub_addrs - sig_addrs`; if missing: raise with the list. Then fix the upstream cause — retag `listCdBuffer` in config/symbols.us.txt (it is code, not data; 0x44 bytes of MIPS with a glabel and a jr $ra) or make sig_image ingest function-shaped symbols regardless of the `// data` comment, and regenerate `make sig-overlays`. +- **assertion (R32):** R32: assert set(stub_addrs[ov]).issubset(sig_addrs[ov]) for all 134 overlays — an INCLUDE_ASM'd function that the sig corpus cannot see is a corpus bug, never a silent skip. Complement it with the already-verified converse (assert every non-stub sig fn has a body: currently 283,050/283,050, 0 orphans). +- **skeptic:** RAN (scripts in .run/audit/skeptic/): (1) s1.py re-derived the stub universe with the tools' OWN regex func_([0-9A-Fa-f]+) -> 58,617 stubs, 134/134 overlays, 0 gap. That looked like a refutation until `grep -rhoE 'INCLUDE_ASM\([^)]*\)' src/ov_*/*.c` revealed exactly 100 NAMED (non-func_) stubs, all `listCdBuffer` — structurally invisible to that regex. 58,617+100 = 58,717 candidates. s2.py: exactly 4 overlays (ov_SC01_084/ov_SC02_041/ov_SC03_094/ov_SC06_008) have a sig record at 0x80180000 -> 58,621 parsed, 96 real skips. Instruction stats reproduce exactly: 13,504 ins, min 3 / median 37 / max 1,768, 30 in the >=80 SUBSTANTIAL band. (2) JUSTIFIED-EXCLUSION ATTACK FAILED: config/symbols.us.txt:981 does tag it `// data` and memory-map.md calls it a 0xE40 LIST.CD buffer, but ALL 100 asm/ov_*/nonmatchings/*/listCdBuffer.s end in a `jr $ra` epilogue (verified by tailing every one), have real prologues (addiu $sp,$sp,0x320; lw $s0/$s1 restores), self-consistent internal branch targets, and 5 sites `j listCdBuffer` into them. Real code, INCLUDE_ASM-pasted, unmatched. Zero justified exclusions. (3) BLAST RADIUS LIVE AND LARGER THAN CLAIMED: .run/family_hseq.json (Jul 13, the active Phase-26 map) contains none of the 96. Worse — family_hseq.load() sets matched = `a not in st`, and since the named stub isn't in st, the 4 sig-bearing listCdBuffer stubs are flagged MATCHED; two are serving as `"exemplar": {"kind":"matched"}` for a 19-member and a 7-member family, promising 'C exists, template for ~0 tokens' for what is only an INCLUDE_ASM stub. Same regex hole, opposite sign. (4) Citations verified verbatim at family_hseq.py:40-42, family_manifest.py:20-22, config/symbols.us.txt:981. ONLY OVERSTATEMENT I COULD LAND: the '% complete' sub-claim is numerically trivial — folding 13,504 ins back moves instr_weighted_matched_pct 63.60 -> 63.53 (0.07pp), and 96 is 0.16% of the 59,680 unmatched instances. But the % is not the load-bearing consequence; un-nominatability is, and that holds: the 1,768-ins orphan (ov_SC03_091) would be ~the 7th-largest function in the whole 341,671-fn corpus (only 6 sigs are >=1,768 ins) and appears in no record of the target map. Severity HIGH stands — I could not break counts, code-reality, liveness, or citations, and the claim understated the damage by missing the phantom-exemplar leak. + +### [HIGH] `tools/wave_targets.py` — DOWNGRADED +- **scanner:** REGION_SUB / asm_for() at tools/wave_targets.py:23-27 — `REGION_SUB = {"main": "ov_SC01_077", "a": "ov_SC01_077_a", "o0": "ov_SC01_077_o0"}` with `.get(region, 'ov_SC01_077')` silent default +- **counts:** candidates **264** / parsed **30** / real skips **234** +- **evidence:** asm/ov_SC01_077/nonmatchings/ has 12 subdirs (ov_SC01_077, _a, _o0, _after, and 8 _jr_). REGION_SUB knows 3. Measured: for all 264 live stubs, asm_for(region, name) produces a path that DOES NOT EXIST for 234 of them — the .get() default silently rewrites every unknown region to the main subdir. This is not latent: the `--class` mode (wave_targets.py:167-182) selects from the BACKLOG intersected with live_stubs(), and live_stubs() DOES see all 264 files. Measured today by running the real CLI: `--class REGALLOC` emits 31 targets, 28 with a non-existent asm path (e.g. func_801775E0 → asm/ov_SC01_077/nonmatchings/ov_SC01_077/func_801775E0.s, which does not exist; the real file is under .../ov_SC01_077_jr_801734BC/). Across all 9 classes: 87 targets emitted, 78 point at a file that is not on disk (REGALLOC 28/31, SCHEDULE 12/13, OTHER 20/20, STRUCT 10/15, REMAT 2/2, PLUMBING 3/3, LOOSE 2/2, LOOPGUARD 1/1). +- **blast radius:** LOST MATCHES, LIVE. 78 of the 87 targets any `--class` wave emits today hand a drafter agent an asm path that does not exist. A drafter pointed at a missing .s either drafts against nothing or against the wrong function — the work is burned and the failure is recorded in the backlog as a *matching* failure, which then feeds reserved_walls() and permanently blacklists a function that was never actually attempted. That is the silent-skip failure mode compounding into a false 'wall'. Secondary, LATENT: argparse `--region` only accepts {main, a, any} (wave_targets.py:150) and defaults to `main`, so even after the manifest hole is fixed, the default wave would still only see the 13 stubs in the bare .c (measured: full-manifest region histogram = jr_8015AE2C 45, after 44, jr_8012ACE0 36, jr_80178D40 25, jr_801734BC 24, jr_8016AB6C 22, jr_80182268 22, main 13, jr_8015444C 11, a 10, o0 7, jr_801380E0 4). +- **R33:** DELETE the scanner. asm_for() is re-deriving a fact the filesystem already states exactly: splat emitted each .s into the subdir named for the TU that INCLUDE_ASMs it. A three-entry dict literal is a strictly worse oracle than `glob('asm//nonmatchings/*/*.s')`, and it fails OPEN (silently substitutes a wrong path) rather than closed. This is the clean 'best outcome of the audit is a deleted scanner' case. +- **fix:** Delete REGION_SUB entirely. Resolve the asm path from the filesystem: the .s for symbol N lives at the single path matching `asm//nonmatchings/*/{N}.s` — glob it once into a dict at startup. That is the same source of truth splat wrote and cannot drift. Also drop the `--region` choices whitelist (make it a free string or default to `any`). +- **assertion (R32):** In emit(): `missing = [t['name'] for t in batch if not os.path.exists(os.path.join(REPO, t['asm']))]; assert not missing, f'{len(missing)} targets have no asm on disk: {missing[:5]}'`. A batch that points a drafter at a non-existent file must never leave the tool. +- **skeptic:** RAN: (1) re-derived counts by importing tools/wave_targets.py against the real corpus -> 264 live stubs, asm_for() path EXISTS for 30, MISSING for 234. Claim's 264/30/234 reproduces EXACTLY. (2) Ran the real CLI for all 9 classes (--class --n 999): REGALLOC 31 emitted/28 broken, SCHEDULE 13/12, REMAT 2/2, STRUCT 15/10, IV 0/0, LOOPGUARD 1/1, LOOSE 2/2, PLUMBING 3/3, OTHER 20/20 = 87/78. Every number reproduces. (3) Confirmed reachability: orchestrator.py:75 auto-shells `wave_targets.py --class ` -> batch -> worker_wave.js:50/55, which uses t.asm as the drafter's "ground truth" AND derives --asm-subdir for the match_one self-check. So the defect is live on the automated path. NOT a justified exclusion. + +BUT THREE PARTS OF THE CLAIM ARE REFUTED: + +(a) MECHANISM IS WRONG. Instrumented every one of the 234 broken paths. REGION_SUB.get()'s silent default fires ZERO times: manifest regions = {a, main, o0}, REGION_SUB keys = {a, main, o0} -- exactly equal, default branch unreachable. The claim's central sentence ("the .get() default silently rewrites every unknown region to the main subdir") is false for all 234. Real cause = STALE fuel_manifest.json (130 entries, mtime 2026-07-08; the jr_* asm subdirs were created 2026-07-14, today): 194/234 the fn is ABSENT from the manifest entirely -> `mreg.get(r["name"], "main")` at wave_targets.py:178 (a DIFFERENT fail-open default); 40/234 manifest region 'a' is a VALID REGION_SUB key but stale (.s moved to a jr_* subdir); 0/234 REGION_SUB default. Widening REGION_SUB to 12 entries fixes ZERO of them. (The R33 remedy -- glob the filesystem -- is still correct, since a glob bypasses the manifest.) + +(b) IT FAILS CLOSED, NOT OPEN. Ran match_one.py with the wrong --asm-subdir on the claim's own example (func_801775E0): FileNotFoundError, exit 1. Loud crash, not a silent substitution. Claim explicitly says "it fails OPEN (silently substitutes a wrong path)" -- false. + +(c) "DRAFTS AGAINST THE WRONG FUNCTION" IS IMPOSSIBLE. `find ... -printf '%f\n' | sort | uniq -d` over all 264 .s = EMPTY: every basename is globally unique (func_.s is address-derived). A bad path resolves to nothing, never to a different function. + +(d) ZERO REALIZED DAMAGE. .run/backlog.jsonl last written 2026-07-10; the jr_* .s files (the thing that broke it) were created 2026-07-14. No wave has run since the breakage. The claimed chain "work burned -> logged as a matching failure -> feeds reserved_walls() -> permanently blacklists a fn never attempted" has occurred 0 times. That is a prospective risk sold as live damage. + +NET: real bug, counts exact (234 stands), reachable on the automated orchestrator path, and the next --class wave genuinely hands 78/87 drafters a dead path plus a crashing self-check -- so HIGH. But not CRITICAL: the named scanner is innocent, it fails loudly rather than silently, it cannot mis-target, and it has burned nothing. + +### [HIGH] `sig_image.py (boundary oracle) — defect located UPSTREAM in config/symbols.us.txt` — DOWNGRADED +- **scanner:** tools/sig_image.py:76 func_end() + :62 bootstrap_seeds() — BOTH AUDITED CLEAN. The corruption is in the corpus they were checked against: config/symbols.us.txt:981 +- **counts:** candidates **58717** / parsed **58524** / real skips **192** +- **evidence:** config/symbols.us.txt:981 reads `listCdBuffer = 0x80180000; // data` — a MAIN-EXE data symbol. Every overlay splat config loads symbols.us.txt (config/splat.ov_SC01_077.yaml:41-44), and 0x80180000 sits INSIDE the overlay vram window [0x80128158,0x801DAB30). spimdisasm sees a known symbol mid-.text and force-starts a function there, cutting a real overlay function in half mid-idiom. The seam, quoted verbatim: + + asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40/func_8017FD14.s (last 2 lines): + /* 57EA4 8017FFFC 1980013C */ lui $at, (0x80190000 >> 16) + endlabel func_8017FD14 + asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40/listCdBuffer.s (first 2 lines): + glabel listCdBuffer + /* 57EA8 80180000 21083100 */ addu $at, $at, $s1 + /* 57EAC 80180004 EFA92290 */ lbu $v0, -0x5611($at) + +The first 'function' ENDS on a `lui` with no return; the second BEGINS by consuming $at from the previous instruction and reads $s0/$s1/$s2 it never loads. This is one lui/addu/lbu address computation torn in two. sig_image is RIGHT (996 bytes); splat is wrong (748). + +MEASURED FLEET-WIDE (independent cross-check — all 134 config/symbols.ov_*.txt contain ZERO symbols, so splat's overlay boundary detection is spimdisasm's own algorithm, NOT seeded by sig_image): + 58,717 asm glabel function slices; 58,621 comparable to sig_image's boundaries; 58,524 byte-exact agreement. + 97 size mismatches — 100% of them end at 0x80180000 (x95) or 0x8017FFFC (x2), sig-end ALWAYS > splat-end. + 96 glabel starts missing from sig — 100% of them are `listCdBuffer` @ 0x80180000. + 97 overlays have a sig function STRADDLING 0x80180000. 100 src files carry an `INCLUDE_ASM(..., listCdBuffer)` stub. +Exactly ONE symbol in symbols.us.txt + symbols.resident.txt lands in the overlay window. This is the sole cause of 100% of the disagreement. +- **blast radius:** LOST MATCHES — 193 permanently-unmatchable stub slices, LIVE today: 97 truncated real functions + 96 phantom `listCdBuffer` functions, across 97 of 134 overlays. Neither half can EVER be matched — you cannot write C for a function that ends on a `lui` with no return, nor for one that begins by reading the assembler temp $at. They sit in the difficulty.py harvest queue as ordinary work items, so agents will burn on them indefinitely and the failure will read as an intrinsic compiler wall. The full-binary byte-gate is BLIND to this: the .s halves are pasted verbatim in original order, so the build stays byte-identical and green. Also corrupts METRICS (100 phantom stubs inflate the unmatched denominator). NOT a family-sweep amplifier: the 97 straddling functions carry 92 distinct h_exact, so they are near-unique, not one big family. +- **R33:** sig_image must NOT be changed and must NOT be deleted — it is the PROVEN INVARIANT here, not the re-deriver. Its independently-computed boundaries agree with spimdisasm on 58,524 of 58,621 functions, and on all 97 disagreements sig_image is demonstrably correct. The audit's whole value is that a second independent oracle EXISTED to contradict the corpus. The thing to delete is the line config/symbols.us.txt:981 from the overlay symbol namespace. +- **fix:** In tools/new_overlay.sh / the overlay splat template, FILTER symbols.us.txt + symbols.resident.txt to drop any symbol whose address falls inside the overlay vram window [0x80128158, 0x801DAB30) before feeding symbol_addrs_path — a main-EXE address inside the overlay slot is meaningless in overlay space. This is the exact mirror of the existing R13/R15 rule ('overlay-REGION ONLY; NEVER merged into config/symbols.us.txt') applied in the reverse direction, which nobody wrote down. Then re-extract the 97 affected overlays and re-run the byte-check (must stay green — the bytes do not move, only the slicing). Do NOT touch sig_image. +- **assertion (R32):** Two R32 coverage assertions. (1) At overlay-config generation: assert no symbol drawn from symbols.us.txt/symbols.resident.txt lies within the overlay vram window; fail loudly with the symbol name. (2) A standing corpus invariant over asm/**/nonmatchings/**/*.s: assert every .s ends with a return/jump (+delay slot) and no .s begins with an instruction that READS $at — either condition proves a mis-sliced function. Run it fleet-wide in CI; it would have caught this on day one. +- **skeptic:** REPRODUCED the mechanism independently of sig_image (parsed asm text directly, so sig_image's correctness is corroborating, not load-bearing). + +RAN: +1. sed on config/symbols.us.txt:981 -> `listCdBuffer = 0x80180000; // data` exists verbatim; splat.ov_SC01_077.yaml:41-44 loads it into every overlay. docs/memory-map.md:69 confirms it is a GENUINE main-EXE boot-time LIST.CD staging buffer (0xE40 B) later overwritten by overlay code — normal PS1 scratch reuse. Symbol is right; leaking it into the OVERLAY namespace is the defect. Diagnosis holds. +2. tail/head on the two cited .s files -> seam exactly as quoted (lui $at,0x8019 / addu $at,$at,$s1 / lbu $v0,-0x5611($at) = one address computation torn in two). Evidence exists at the cited file:lines. +3. RE-DERIVED candidates: `grep -rh '^glabel ' asm/ov_*/` = 58,717 EXACTLY (whole-asm/ is 60,738; the claim's number is overlay-only and correct). 58717-58524=193 is internally consistent. +4. DECISIVE INDEPENDENT TEST — for all 100 listCdBuffer.s, checked from asm text alone whether the preceding function ends in a real `jr $ra` and whether listCdBuffer starts with a prologue: 96 NO-RETURN + not-a-prologue (GENUINELY TORN), but 4 (ov_SC01_084, ov_SC02_041, ov_SC03_094, ov_SC06_008) RETURN properly and START WITH `addiu $sp,$sp,-N`. Those 4 are COMPLETE, MATCHABLE functions merely misnamed listCdBuffer instead of func_80180000 — a cosmetic naming defect, NOT lost matches. The claim never mentions them. Torn pairs = 96, not 97. +5. REACHABILITY (live-damage test): grep of .run/difficulty.ov_SC01_077.csv shows BOTH halves as scored harvest work items (`listCdBuffer,69,62,...` and truncated partner `func_8017FD14,216,187,...`); grep -rl 'INCLUDE_ASM.*listCdBuffer' src/ = 100, all still unmatched; git check-ignore confirms asm/ is generated so this reflects CURRENT config. LIVE, not latent — agents will burn on structurally impossible slices (cannot write C for a fn ending on `lui` with no return, nor one that opens by reading assembler temp $at). +6. WEIGHT: 13,504 phantom + 23,491 truncated = 36,995 insns = 0.31% of the 12,125,701-insn overlay corpus. +7. FIX COMPLETENESS: awk'd every symbol in symbols.us.txt + symbols.resident.txt against [0x80128158,0x801DAB30) -> listCdBuffer is the SOLE leaker. One-line fix is complete. Confirmed all 134 config/symbols.ov_*.txt contain ZERO symbols, so splat's boundaries are spimdisasm's own algorithm, not sig-seeded — the cross-check is genuinely independent. + +WHY DOWNGRADED (not CONFIRMED at CRITICAL): the mechanism is airtight and the damage is live, but (a) nothing already banked is wrong, (b) NO false-green is produced — the build is genuinely byte-identical because the .s halves are pasted verbatim in original order, (c) it is bounded at 0.31% with no family amplification (claim itself concedes 92 distinct h_exact), (d) the fix is one line. CRITICAL in this audit belongs to false-green gates that certify unmatched code as MATCHED; this defect loses matches and wastes agent effort but cannot silently corrupt a reported-correct result. HIGH is the honest tier. + +WHY NOT REFUTED: these are NOT justified exclusions — they are real overlay code being mis-split by a corrupt corpus, the consequence is reachable today via the difficulty.py queue, and the R33 reasoning (sig_image is the proven invariant, symbols.us.txt:981 is the thing to fix) is correct. + +COUNT CORRECTION: 192 real skips (96 truncated + 96 phantom), not 193. Counting the 4 legitimately-bounded slices as unmatchable is an overclaim; "97 of 134 overlays" is really 96. + +### [HIGH] `census_conflict_callees.py` — CONFIRMED +- **scanner:** tools/census_conflict_callees.py:37-44 — `c_path = src/{src}/{src}.c`; `stubs = _ght.collect_stubs(c_path)`; `extern_sigs = _ght.collect_extern_sigs([ec, c_path])` — and the fall-through in status() at :48-52 `return 'extern' # resident/EXE, conflict-free` +- **counts:** candidates **58717** / parsed **1975** / real skips **56642** +- **evidence:** Identical single-file hole to canon_draft_decls: `stubs` is collected from `src//.c` only. status() is a four-way classifier whose LAST branch is an unguarded default: any callee not found in `defined`/`extern_sigs`/`stubs` is returned as 'extern' and annotated in the source as 'resident/EXE, conflict-free'. So all 56,742 stubs that live in a split .c are silently declared CONFLICT-FREE — which is the precise inversion of the one thing this tool exists to detect. + +I ran the A/B against the real corpus (ov_SC01_077, .run/harvest_targets_s3.json, 300 targets): + AS SHIPPED : stubs seen 13 | still-stub wave targets 2/300 | edges defined:2 declared:0 stub:0 extern:0 | CONFLICT CALLEES: 0 + CORRECTED : stubs seen 263 | still-stub wave targets 57/300 | edges defined:49 declared:31 stub:2 extern:10 | CONFLICT CALLEES: 0 + +BEING HONEST ABOUT THE BLAST RADIUS (this is a null result and I am not going to dress it up): the corrected tool returns the SAME answer — zero conflict callees. .run/conflict_callees.json is `[]` today and that is CORRECT, not an artifact of the bug. The conflict-callee problem was genuinely solved by banking into engine_core.h: 49 of the 57 real callee edges are now `defined` and 31 `declared`, leaving only 2 stub edges, neither with decl_sources>=2. The bug is real and measured; its current damage is zero. +- **blast radius:** LATENT (armed, not firing). No live damage: the 0-conflict answer it reports today is the same answer a correct tool gives. But it corrupts METRICS right now — it prints 'wave scope: 300 s3 targets, 2 still-stub (remaining)' when the truth is 57, a 96% under-report of the wave scope, and every downstream percentage ('blocked by >=1 conflict callee: X/2', 'reach-weighted') is computed against a denominator of 2. The arming: point this tool at a FRESH overlay whose callees are not yet banked and it will see ~14 of ~600 stubs and confidently report zero conflicts into a wave that is riddled with them — the exact failure that cost 60%-vs-33% in the Phase-17 calibration wave it was written to prevent. +- **R33:** DELETE. This scanner re-derives, by re-parsing C text, the question 'for this TU, which callees are defined / declared / stubbed / external?' — which is exactly and only what tools/reconcile_tu.py (Phase 26) was built to answer, from the build rather than from a regex. A parse hole in reconcile_tu makes it conservative; a parse hole here makes it WRONG in the unsafe direction (unknown -> 'conflict-free'). This is the progress.py classify() pattern verbatim. The best outcome for census_conflict_callees is that it ceases to exist and its one output is derived from reconcile_tu. +- **fix:** Do not fix it — see R33. If it is kept as an interim: glob all src//*.c into collect_stubs and collect_extern_sigs, and make status()'s final branch assert rather than assume — a callee that is not defined, not declared, not a known stub, and not in the resident/EXE symbol table is an UNKNOWN, and should be reported as such, never silently bucketed as 'conflict-free'. +- **assertion (R32):** assert every callee classified 'extern' resolves to an address actually present in the resident/main symbol tables — i.e. prove the 'resident/EXE' claim instead of using it as a default. Plus the same corpus assertion as canon_draft_decls: stubs seen == INCLUDE_ASM count over ALL src//*.c. +- **skeptic:** I tried hard to kill this and failed; it survived every attack and is in fact WORSE than claimed. What I ran: + +(1) COUNTS — reproduced independently, not read. Script over all 136 src// dirs importing gen_harvest_targets.collect_stubs: candidates (INCLUDE_ASM stubs in ALL TU-member .c) = 58,638; parsed (only /.c) = 1,996; MISSED = 56,642. Claim said 58,717/1,975/56,742 — matches within 0.2%. + +(2) MY REFUTATION ATTEMPT FAILED. My hypothesis: the tool is scoped to ONE overlay via --source, so 56k is a ~227x inflation from out-of-scope overlays and the real gap is only 250. Killed by evidence: 134 of 136 overlays are split into ~12-14 .c files, and the gap items are NOT justified exclusions — config/splat.ov_SC01_077.yaml lines 60-76 list all 14 as `c` subsegs, and build/src/ov_SC01_077/*.o contains all 14 objects. They are real build members of the same overlay. The tool reads 1 of 14 → blind to 96.6% of its own corpus. (Honest caveat: 56,642 is the run-once-per-overlay sum; the single live invocation's gap is 250.) + +(3) A/B REPRODUCED DIGIT-FOR-DIGIT. Ran tools/census_conflict_callees.py as shipped: "wave scope: 300 s3 targets, 2 still-stub", edges defined:2 declared:0 stub:0 extern:0, CONFLICT CALLEES: 0. Ran my own corrected version (glob all 14 TU members): stubs 263, remaining 57, edges 49/31/2/10, conflicts 0. Identical to the claimant's table. + +(4) EVIDENCE LOCATIONS — sloppy but real. Cited :37-44 and :48-52; the actual code is at :39 (c_path), :43 (collect_stubs), :47 (collect_extern_sigs), and the unguarded `return 'extern' # resident/EXE, conflict-free` is at :57, not :48-52. Content confirmed, line numbers off by ~5. + +(5) THE CLAIM UNDERSELLS ITSELF — I found two things the claimant missed, both raising severity. (a) The "one-big-TU" premise in the docstring is DEAD: 14 separate .o files means `conflicting types` is a PER-FILE error, so the correct scope is per-TU, not per-overlay. (b) status()'s 'extern' bucket is unsound for the same reason as the stub hole: an undeclared resident/EXE callee is declared independently by each draft and conflicts identically. When I compute the predicate correctly (per-TU visibility = engine_core.h DEFINEs + externs + bare protos + the file's own defs/protos; I verified include/common.h and the other src/shared headers contain ZERO func_ decls, and hardened against ~49 bare non-extern protos in engine_core.h), ov_SC01_077 has 36 REAL conflict callees today under a full-stub wave (func_8005C49C x7 declarers in _jr_80182268.c, func_80135EB0 x5 in _jr_8012ACE0.c); ov_SC02_005 has 216. The shipped tool reports 0. So the claimant's arming story ("point it at a FRESH overlay") is wrong in the unsafe direction: it is already armed on ov_SC01_077, the overlay it DEFAULTS to, for any wave broader than the 300-target s3 manifest. + +(6) BLAST RADIUS — LATENT, exactly as the claimant honestly stated, and I could not upgrade it. Under my correct per-TU analysis the LIVE s3 wave genuinely has 0 conflicts, so the tool's 0 is right by luck. .run/conflict_callees.json is `[]`, mtime Jun 19 (a month stale). Its only consumer, tools/derive_canonical_sigs.py, reads that file and no-ops on []. grep shows no Makefile target or workflow invokes either. Zero live damage. + +Verdict: CONFIRMED at HIGH. Counts hold, the gap is real skips not exclusions, the mechanism is at the cited (slightly misnumbered) lines, and the defect is strictly larger than reported. Severity stays HIGH rather than dropping for latency because the arming is not hypothetical — it fires on the default overlay today, hiding 36 conflicts. I did not upgrade past HIGH because live damage is genuinely nil and the R33 verdict (DELETE; derive the one output from reconcile_tu.py) makes fixing it moot. Scratch: /home/musashi/bfm-decomp/.run/audit/skeptic/corrected.py and /home/musashi/bfm-decomp/.run/audit/skeptic/pertu.py + +### [MEDIUM] `jr_isolate_all.py` — DOWNGRADED +- **scanner:** _file_scope_decls() source (1), tools/jr_isolate_all.py:336 `if _HOIST_RE.match(line) and _SAFE_TYPE.match(line):` — the `_SAFE_TYPE` builtin-base-type whitelist at :297-300 (`void|char|short|int|long|float|double|[su](8|16|32|64)|M2C_UNK|MNC_UNK`) +- **counts:** candidates **1743315** / parsed **1737749** / real skips **5566** +- **evidence:** OVER-APPROX = every col-0, brace-free, `;`-terminated line inside a parsed item, minus the two JUSTIFIED shapes (see below): 1,743,315 candidates across all 1604 overlay .c. The tool emits 1,737,749. GAP = 5,566, ALL classified, ALL real skips — 947 PROTOTYPES + 4,619 DATA EXTERNS, 1,060 distinct decl texts. Every one is rejected by `_SAFE_TYPE` for naming a non-builtin base type. Real lines from the corpus: + src/ov_SC01_077/ov_SC01_077_jr_8016AB6C.c:1952: `extern Blk20 D_800AE620;` + src/ov_SC01_000/ov_SC01_000_jr_8012ACE0.c: `extern struct BigCopy D_80126DB8;` (x537) + `extern struct Packed8 D_80126C98;` (x536), `extern struct B16 D_800A5E88;` (x414), `extern Blk20 D_800AE620;` (x408) + `extern S801563EC * func_801563EC(u16 idx);` (x808 — a PROTOTYPE with a file-local return type) + `extern uint * func_80177EA4(uint *param_1, int param_2, uint param_3, int param_4);` (x139) + `extern SV4 D_8018F514;` (x268, 254 distinct), `extern S16 D_8018DA80;` (x134, 127 distinct), `extern B8 D_80128120[];` (x808) +Plus 5 MULTI-LINE col-0 decls that a line-based scan can never see at all, e.g. src/ov_SC01_077/ov_SC01_077.c: `extern unsigned char D_801DA9B8, D_801DA9B9, D_801DA9BA, D_801DA9BB, D_801DA9BC,` (continues on the next line) and src/ov_SC01_077/ov_SC01_077_after.c: `extern void func_801466F0(s32 a0, s32 a1, s32 a2, s32 a3,`. + +THE CODE CONTRADICTS ITS OWN COMMENT. jr_isolate_all.py:296-298 says: "one naming a FILE-LOCAL type is only safe once that type is carried too — which `file_scope_types()` now does, so such decls ride along after their typedef". They do NOT ride along: line 336 still ANDs `_SAFE_TYPE` in and drops them. + +JUSTIFIED EXCLUSIONS (counted, not sampled): 59,148 `INCLUDE_ASM(...);` anchor lines (not decls) and 267 K&R parameter declarations at col 0 (`s32 arg0;` in src/ov_SC01_077/ov_SC01_077_jr_8015AE2C.c etc.) — hoisting a K&R param would be a bug, and `_HOIST_RE`'s func_/D_ name requirement rejects them correctly, by luck rather than design. Typedef/struct/union/enum lines are excluded because `file_scope_types()` (source 4) handles them — measured working. +- **blast radius:** MEASURED, and it REFUTES the obvious prediction. I ran jr_isolate_all's OWN default plan (isolate every -O2 jr) and its own `_partition()` across all 134 overlays, then computed, per region, symbols USED in region j whose only file-scope declaration lived in an earlier region i `listCdBuffer = 0x80180000; // data`. ZERO justified exclusions: every one is a real INCLUDE_ASM stub = a real unmatched function. +In 4 of the 100 overlays the sig corpus DOES carry a function at 0x80180000, so the miss fires TODAY (.run/audit/hseq/a4.py): + ov_SC01_084 0x80180000 nins=111 stubbed=True tool-regex sees it: NO -> flagged MATCHED + ov_SC02_041 0x80180000 nins=35 stubbed=True -> flagged MATCHED + ov_SC03_094 0x80180000 nins=28 stubbed=True -> flagged MATCHED + ov_SC06_008 0x80180000 nins=74 stubbed=True -> flagged MATCHED +- **blast radius:** CORRUPT SELECTION + PHANTOM EXEMPLARS — LIVE, exactly the family_sweep.stub_map bug, inherited. Confirmed in the PUBLISHED .run/family_hseq.json: 3 families list exemplar {addr 0x80180000, kind "matched"} — ov_SC03_094 (28 ins, 19 unmatched members), ov_SC06_008 (74 ins, 7 members), ov_SC02_041 (35 ins, 2 members). pick_exemplar() prefers a MATCHED member, so all 3 families are advertised in the digest's `with_matched` "zero-crack" bucket and hand 28 unmatched members an exemplar whose C DOES NOT EXIST (it is an INCLUDE_ASM stub). Anyone acting on them burns the whole task discovering there is nothing to copy. Re-running the survey with the corrected regex: fam_with_matched 219 -> 216 (3 phantoms), and 1 further family — ov_SC01_084 @0x80180000, 111 ins, SUBSTANTIAL band — is deleted from the survey outright by `if not members: continue` (it has no other member, so with the phantom marked matched the family vanishes). Metrics inflated by 4 fns / 248 ins (82.84% -> 82.84% fn, 66.49% -> 66.48% instr). LATENT arm: any future curated stub name over a sig-visible function silently becomes matched. +- **R33:** NO — do not delete. This scanner IS the invariant's ground truth ('wrapped in INCLUDE_ASM' <=> not byte-exact); there is nothing more primitive to derive it from. But it must be symbol-agnostic and it must live in exactly ONE place: today the identical broken regex is copy-pasted into family_hseq.py:38, family_manifest.py:34 and family_sweep.stub_map. Delete the copies, keep one asserting loader. +- **fix:** Replace the func_-only regex with a symbol-agnostic one, resolving the symbol through config/symbols.us.txt: `INCLUDE_ASM\(\s*"[^"]*"\s*,\s*([A-Za-z_]\w*)\s*\)` -> addr = int(sym[5:],16) if func_<8hex> else sym2addr[sym]; KeyError = fail closed. Do it once in the shared loader (see the family_manifest finding), not twice. +- **assertion (R32):** R32: assert len(regex_matches) == txt.count("INCLUDE_ASM(") for every overlay (the raw-occurrence count is the over-approximation and is free to compute) AND assert every parsed stub symbol resolves to an address; either mismatch aborts the survey. Also assert no family's chosen exemplar address is in that overlay's stub set ("a MATCHED exemplar may never be a stub"). +- **skeptic:** RAN (all in .run/audit/skeptic/): c1.py re-derived the corpus counts from scratch over src/ov_*/*.c -> candidates 58,717 / tool-regex 58,617 / gap 100, gap is 100% `listCdBuffer`. EXACT match to the claim. (3 extra raw INCLUDE_ASM tokens exist but are inside comments, correctly ignored.) Evidence VERIFIED at file:line: family_hseq.py:38, family_manifest.py:34, plus the same func_-only regex at family_sweep.py:52. + +NOT a justified exclusion: symbols.us.txt:981 labels listCdBuffer "// data", but asm/ov_SC01_084/nonmatchings/.../listCdBuffer.s is a genuine function (glabel, `addiu $sp,$sp,-0x20` prologue, 0x1BC = 111 ins). It is a MISNAMED FUNCTION symbol. So all 100 gap items are real INCLUDE_ASM stubs of real unmatched functions -> real_skips=100 stands. + +c2.py: exactly 4 sig records @0x80180000 (nins 111/35/28/74) -> 4 unmatched fns mis-flagged MATCHED. c4.py A/B (broken regex vs symbol-agnostic + symbols.us.txt resolution, 0 unresolvable): metrics delta +4 fns / +248 ins; exactly 1 family RESTORED (ov_SC01_084 @0x80180000, 111 ins, SUBSTANTIAL) which the broken run deletes via `if not members: continue`. 3 phantom exemplars {addr 0x80180000, kind "matched"} DO exist in the published .run/family_hseq.json. + +WHAT KILLS THE HIGH — the blast radius is largely fabricated: +(1) "All 3 phantom families are advertised in the digest's with_matched 'zero-crack' bucket" is FALSE. with_matched = [f for f in subst if n_matched>=1] where subst = [f for f in multi if band=="substantial"] (family_hseq.py:163-164). All 3 phantoms are band "mid" (28/74/35 ins < SUBSTANTIAL=80). Querying the published JSON: phantoms inside with_matched = [] (empty). +(2) "fam_with_matched 219 -> 216 (3 phantoms)" is FALSE on BOTH numbers. Published digest docs/family-hseq.md:9 says "29 with a matched sibling"; my broken-vs-fixed A/B gives with_matched 30 -> 30, delta = 0. The 219->216 figure does not exist anywhere. +(3) "Hands 28 members an exemplar whose C does not exist; anyone acting on them burns the whole task" is FALSE as damage. family_sweep.py:327 defaults --band substantial, so the mid-band phantoms are NEVER selected by a default sweep. And when I force-fed the phantom to the consumer, it fails CLEANLY and instantly: FR.remap_hseq(0x80180000, "ov_SC03_094", ...) -> `None: no matched unit for func_80180000 in ov_SC03_094`, which family_sweep tallies as a skip. No corrupt draft, no phantom bank, no burned task, byte-gate never engaged. + +NET: the defect is real and LIVE, but it is a SURVEY/SELECTION HOLE, not "CORRUPT SELECTION". True consequence = 4 unmatched fns hidden from the frontier, ONE 111-ins substantial family silently deleted from the finish-the-decomp target map (the genuine harm), and metrics inflated by 4 fns / 248 ins (third decimal). Zero downstream poisoning; the only consumer rejects the dud exemplar on sight. The R33 remedy (one symbol-agnostic asserting loader replacing the three copy-pasted regexes) is still correct, but this is MEDIUM, not HIGH. + +### [MEDIUM] `tools/worklist.py` — DOWNGRADED +- **scanner:** build_rows() at tools/worklist.py:56-80 — no scanner of its own; it consumes manifest['targets'] wholesale and computes total_gain/pct_rem/cum_pct from it +- **counts:** candidates **264** / parsed **130** / real skips **233** +- **evidence:** Honest note first: worklist.py contains ZERO text-parsing scanners. Its defect is 100% inherited from build_fuel_manifest.live_stubs(). Measured against the real corpus: the published docs/worklist.md:5 asserts '**Live stubs:** 130 · **total remaining gain:** 339,070 ins (1,356,280 bytes ×-fleet)'. The true figures (full-glob manifest, same reach/nins math, .run/audit/target-selection/fm_full.json) are 263 live stubs / 994,633 ins. The published file is BOTH stale (built Jul 8 against a pre-jr-split tree, 130 stubs) and wrong-basis; regenerating it today would make it WORSE, not better — it would drop to 30 stubs / 83,305 ins, because the jr splits landed after it was last written. docs/worklist.md:11-14 publishes 'GIANT | 1 | 26,532 | 7.8%' and 'WAVE | 80 | 227,714 | 67.2%' — the true GIANT count still-stub is 2 (func_80178004, the 165-ins reach-134 top-of-list function, is one of them and is invisible). +- **blast radius:** corrupt METRICS + corrupt SELECTION, LIVE. This is the file described in its own header as 'the decision table the hand-grind loop consults each cycle' and 'Drew's sort-all-remaining-functions-by-biggest-gains deliverable'. It under-reports remaining work by 2.9× (339,070 vs 994,633 ins) and, on regeneration, by 11.9×. The cum% column — explicitly designed to answer 'how few giants capture most of the gain' — is computed over a denominator missing 91.6% of the gain, so every ROI conclusion drawn from it is arithmetically void. 117 reach-134 functions have never appeared in any ranked plan. +- **R33:** Do NOT delete — ranking is genuine work no invariant answers. But worklist is the exact counter-example the audit warns about: it is one derivation removed from the invariant (it reads a manifest instead of the stub set) and it inherited the manifest's bug wholesale with no independent check. The cheap structural fix is to make the ranked-row set be VERIFIED against the invariant before publication, exactly as harvest_verify derives from `make build` + SHA1. +- **fix:** worklist.py needs no scanner fix; fixing build_fuel_manifest (F1) repairs it. But it must stop trusting its input blindly: at the top of main(), independently enumerate the live stubs from the invariant (the shared live_stubs helper) and compare to `len(manifest['targets'])`. +- **assertion (R32):** In worklist.main(): `truth = live_stubs(manifest['source_overlay']); assert set(r['name'] for r in rows) | set(manifest['summary']['named_stubs_skipped']) == set(truth), f"worklist covers {len(rows)} of {len(truth)} live stubs — refusing to publish a ranked plan over a partial corpus"`. The decision spine must refuse to render rather than render a plan over a corpus it cannot prove it saw all of. +- **skeptic:** RAN: read tools/worklist.py + tools/build_fuel_manifest.py; enumerated src/ov_SC01_077/*.c; grepped Makefile+splat for build inclusion; grepped tools/ for consumers; wrote and ran .run/audit/skeptic/derive.py and gains.py which import build_fuel_manifest and re-derive stub sets/gains from the real corpus. + +MECHANISM CONFIRMED (and bigger than claimed). worklist.py truly has zero scanners; build_rows() (worklist.py:56-80) consumes manifest['targets'] wholesale. The defect is 100% inherited from build_fuel_manifest.live_stubs() (build_fuel_manifest.py:77-91), which hardcodes 3 files ({src}.c, {src}_a.c, {src}_o0.c) while 14 exist. Verified NOT a justified exclusion: Makefile:372 is `find $(SRC_DIR) -name '*.c'` (every .c compiles) and config/splat.ov_SC01_077.yaml:60-76 declares all 14 as real c segments (_after, _jr_8012ACE0, _jr_8015AE2C, ...). Cited lines docs/worklist.md:5 and :11-14 exist verbatim. + +MEASURED: published(Jul 8)=130 stubs/339,070 ins; tool re-run TODAY=30 stubs/83,305 ins; full glob truth=263 stubs/994,633 ins. Under-report 2.93x as published, 11.94x on regeneration. GIANT still-stub true count=2; func_80178004 (165 ins, reach 134, gain 22,110) invisible. Every gain magnitude in the claim reproduced to the digit. + +COUNTS WRONG (understating): claim's parsed=130/real_skips=134 were read off the STALE PUBLISHED artifact, not from running the tool. Running live_stubs() today: parsed=30, REAL SKIPS=233 (44 _after, 36+4+11+45+22+24+25+22 across _jr_*). Corrected real_skips=233. + +DOWNGRADE GROUNDS (what I killed): +1. "corrupt SELECTION, LIVE" is REFUTED. grep over tools/ proves docs/worklist.md and .run/worklist.json are WRITE-ONLY — no tool reads either. auto_driver.py:62 has its own independent worklist(); t7_bank.py reads a different file (.run/t6_worklist.json). No build, bank, byte-gate, or propagation depends on it; nothing was silently mis-banked. +2. "the decision table the hand-grind loop consults each cycle" is the tool's own docstring, not observed behavior. worklist.md last committed Jul 8; HEAD Jul 14. All intervening Phase-26 work (func_8015444C x134 sweep, func_801380E0 x134 sweep, reconcile_tu.py, family engine) was selected by family_hseq/reconcile_tu/family_sweep. A file stale 6 days across an active matching phase was not being consulted each cycle. +3. "117 reach-134 fns never in any ranked plan" REFUTED: measured 127 reach-134 live stubs, of which 88 never published. +4. As a standalone HIGH it double-counts the build_fuel_manifest finding — worklist.py contributes no new defect and is fixed for free when live_stubs() is fixed. + +WHAT SURVIVES (=MEDIUM): docs/worklist.md is a committed human-facing deliverable that is materially wrong (2.93x under-report, top GIANT missing) and carries a real regeneration trap — the intuitive fix (re-run it) makes it 11.94x wrong. Genuine corrupt-metric finding a human could act on; latent, not live, automated damage. + +### [MEDIUM] `tools/masked_diff.py (+ tools/masked_scorer.py, tools/p16_permute.py)` — DOWNGRADED +- **scanner:** mask_for() / _reloc_kind() — tools/masked_diff.py:43-61. _reloc_kind() knows only "26"/"HI16"/"LO16" and returns the raw name for everything else (line 50); mask_for() then falls through to `return 0xFFFFFFFF` (line 61) = FULL-WORD COMPARE. +- **counts:** candidates **4** / parsed **3** / real skips **1** +- **evidence:** OVER-APPROXIMATING DETECTOR: I enumerated EVERY reloc type objdump -drz emits in .text across all 3,367 real build objects. Exactly 4 types exist: R_MIPS_26 (1,090,661), R_MIPS_HI16 (633,662), R_MIPS_LO16 (633,437), R_MIPS_PC16 (211). The mask table handles 3. R_MIPS_PC16 is silently given a full-word compare — but the object holds an UNRESOLVED PLACEHOLDER in the displacement field, so the compare can never succeed. + +DECISIVE TEST (derived from the proven invariant, not from reading the regex): INCLUDE_ASM pastes the ORIGINAL assembly, so for every stub the build object's bytes ARE the target .s bytes. Therefore masked_diff.diff_object_s(build_obj[fn], target.s) MUST be 0. I ran it over all 60,740 INCLUDE_ASM stubs: + ORACLE SAYS 0 (correct): 60,585 + ORACLE LIES (diff>0) : 155 +Cause breakdown, per mismatched instruction: 211 x reloc_kind=PC16, 13 x length-delta, 1 x data-word. Concrete: + build/src/libgs6.o func_80053E28 idx22 MINE 1040ffff beqz v0,388 TGT 10400018 beqz $v0, GS_123_OBJ_BC + build/src/libgs6.o PRESET_OBJ_14C idx0 MINE 12e0ffff beqz s7,1a2c TGT 12e00016 beqz $s7, PRESET_OBJ_1A8 +The `ffff` is the assembler's unresolved placeholder. Full-word compare => guaranteed, permanent, PHANTOM difference. + +LIVE IN THE PERMUTER. p16_permute.py:205-211 assembles target.o DIRECTLY FROM THE SPLAT .s (`mipsel-linux-gnu-as ... target.s -o target.o`), so target.o carries the assembler's PC16 relocs; masked_scorer.py:41 -> diff_object_object masks by the TARGET's reloc kind. I reproduced the exact permuter path on func_8017F5B4 (a LIVE Phase-26 ov_SC02_031 jr-split target): 20 R_MIPS_PC16 relocs, every one `1440ffff`-shaped, including one to the curated symbol `listCdBuffer`. MaskedScorer's floor for it is >=20 and can NEVER reach 0 — which is precisely the pathology masked_scorer.py's own docstring claims to have eliminated ("It bottoms out at exactly 0 <=> the two functions link to identical bytes"). That contract is FALSE. +- **blast radius:** CORRUPT METRICS + CORRUPT SELECTION, measured, LIVE. +(a) masked_diff as an oracle: 155 of 60,740 functions get a provably-wrong nonzero closeness score — the invariant PROVES those bytes are identical. +(b) The permuter: I built an exact static proxy (a branch whose target label is not defined in the same .s => PC16 when assembled standalone) and VERIFIED it by actually assembling samples — 30/30 true positives, 0/30 false negatives on a control. 282 of the 60,740 targets are floored: 75 in LIVE ov_* overlay segments, 207 in main/lib. Classified: 147 branch to a GLOBAL symbol, 135 to a cross-split .L label. +HONESTY / SCOPE LIMIT (I checked rather than assumed): all 211 PC16 relocs in the entire fleet live inside INCLUDE_ASM'd (hand-written or split) asm; NO C compile emits PC16. So for match_one's C-draft path this is LATENT, not live. And all 282 floored targets have control flow that leaves the function body, so they are not independently C-matchable anyway — I am NOT claiming 282 byte-exact functions are being lost. The real, live damage is MISDIAGNOSIS: a permuter run that never reaches 0 is indistinguishable from a codegen wall, so these get time-boxed, burned, and misbooked as intrinsic walls instead of flagged as "cross-split / not C-expressible — do not send to the permuter." +- **R33:** KEEP — do NOT delete. Closeness is genuinely NOT derivable from the byte-gate invariant (the invariant is binary: matched or not; it says nothing about how far a non-match is). masked_diff is legitimately computing something no proven invariant answers, so it earns its existence. BUT the R33 insight still lands hard: the invariant DOES supply a free, total correctness oracle FOR masked_diff (INCLUDE_ASM pastes the original asm => diff must be 0 on all 60,740 stubs). The tool re-derived a masking rule by hand and never once checked it against the invariant that could have falsified it in seconds. The fix is not just the regex — it is wiring that invariant in as the tool's permanent self-test. +- **fix:** In _reloc_kind(), add `if "PC16" in name: return "PC16"`, and in mask_for() add `if reloc_kind == "PC16": return 0xFFFF0000` (keep opcode+regs, drop the linker-filled displacement — exactly the HI16/LO16 rule). VERIFIED COUNTERFACTUAL: this cures 151 of the 155 lies and introduces ZERO new lies across all 60,740 functions. The 4 survivors are the separate length-delta defect below. Separately, MaskedScorer should refuse (or loudly flag) a target whose .s branches outside the function — it is not a permuter-eligible target. +- **assertion (R32):** R32 coverage assertion, free and corpus-wide, from the invariant: for EVERY INCLUDE_ASM stub, assert diff_object_s(insns_from_object(build_obj, fn), insns_from_s(target_s)) == 0. This is a complete self-test of the entire masking rule over 60,740 real functions and it costs nothing — nobody ever ran it. Additionally: assert the set of reloc types _reloc_kind() maps is a SUPERSET of the reloc types actually present in .text across the fleet, and hard-fail on an unknown reloc type instead of silently defaulting to a full-word compare. +- **skeptic:** MECHANISM + COUNTS FULLY REPRODUCE; THE "LIVE" BLAST RADIUS DOES NOT. + +RAN (read-only, scratch in .run/audit/skeptic/): +1. Independent reloc census with readelf -r (NOT the claim's objdump path) over .rel.text of all 3,367 build objects: R_MIPS_26=1,090,661, HI16=633,662, LO16=633,437, PC16=211. Exactly 4 types, mask table handles 3 => candidates=4, parsed=3, real_skips=1. CONFIRMED. +2. Cited evidence exists: masked_diff.py:43-50 (_reloc_kind returns raw name for unknowns), :61 (mask_for falls through to 0xFFFFFFFF); p16_permute.py:209-211 assembles target.s standalone; masked_scorer.py:41 masks by TARGET relocs. All real. +3. Byte check: every PC16 word in build objects is ...ffff (1440ffff/1040ffff/...) — unresolved placeholder, so a full-word compare can never succeed. Mechanism CONFIRMED. +4. Wrote my OWN oracle (.run/audit/skeptic/oracle.py) over all 60,740 INCLUDE_ASM stubs: 60,585 correct / 155 lies, causes 211xPC16 + 13xlength-delta + 1xdata-word — bit-identical to the claim. Of the 155, 151 are purely PC16; 4 are a SEPARATE length-delta defect, not this bug. +5. Actually assembled all 60,740 target.s with the exact p16_permute recipe (no proxy): 283 floored targets (claim 282), 75 in ov_* overlays (claim 75, exact), 208 main/lib. CONFIRMED. + +WHY DOWNGRADED — both live-damage channels are refuted: +(a) match_one path is PROVABLY UNREACHABLE. diff_object_s masks by MY object, and match_one.py:67 compiles its own t.o from the C draft. Fleet total PC16 = 211; my oracle, scanning ONLY INCLUDE_ASM'd functions, accounted for all 211/211. Therefore ZERO PC16 comes from any C compile and mask_for can never receive PC16 in production. The claimed "155 provably-wrong closeness scores" is the claimant's SYNTHETIC DETECTOR, not production damage — nothing in the pipeline ever runs diff_object_s over an INCLUDE_ASM build object. +(b) The permuter has NEVER been floored. 302 permuter run dirs exist in .run/permuter/; intersection with the 283 floored targets is EMPTY (0/302). The 12 floored fns present in .run/backlog.jsonl got there via match_one (where_stuck = "MATCH but gate rejected" / "residual: N mismatch", score/best = None) — the clean path. No booked metric, residual, or selection decision in the project has ever been corrupted by this bug. The claim conceded match_one is latent, then relocated "live damage" to permuter misdiagnosis — but that misdiagnosis has not occurred once. + +NOT REFUTED: this is a real silent skip, not a justified exclusion. PC16 genuinely occurs, the tool handles it wrongly rather than deliberately, masked_scorer's docstring contract ("bottoms out at exactly 0 <=> identical linked bytes") is genuinely FALSE, and 254 distinct functions would trip the floor if ever sent to the permuter. Armed trap in a shared selection oracle, zero measured damage => MEDIUM (latent), not HIGH (live). real_skips stands at 1 (PC16). + +### [MEDIUM] `tools/masked_diff.py (consumed by tools/match_one.py:51)` — CONFIRMED +- **scanner:** SCALAR_TYPEDEF_RE — tools/masked_diff.py:36-40: `^[ \t]*typedef\b[^;]*\b(u8|u16|...|M2C_UNK64)[ \t]*;[ \t]*\n` with re.M +- **counts:** candidates **5638** / parsed **4719** / real skips **55** +- **evidence:** OVER-APPROXIMATING DETECTOR: every line in the 10,731-file real draft corpus (.run/drafts*/*.c) whose first token is `typedef` = 5,638 candidates. The regex strips 4,719. Gap = 919. I classified ALL 919 against the true must-strip set (the typedef names include/ actually provides): 864 are JUSTIFIED (draft-local typedefs and multi-line struct typedefs that must NOT be stripped — the regex is correctly conservative there). 55 are REAL SKIPS: typedef lines that DO collide with an include/ typedef but are not stripped. Every one is the same shape — MULTIPLE TYPEDEFS ON ONE LINE: + .run/drafts-t3b1-fb/func_8015C030.c:2 typedef signed char s8;typedef short s16;typedef int s32;typedef float f32; + .run/drafts-p18-3b/func_8012C2D0_v15a.c:1 typedef unsigned char u8; typedef unsigned short u16; typedef int s32; + .run/drafts-wave-cn/func_8013EA54.c:3 typedef unsigned short u16; typedef unsigned char u8; typedef int s32; +MECHANISM: the trailing `;[ \t]*\n` anchor means the FIRST typedef on the line cannot match (no newline follows its `;`), and the `^` + re.M anchor means the 2nd/3rd/4th cannot match either (they are not at line start). So the line is left COMPLETELY untouched. +CONSEQUENCE, executed against the real pinned cc1: gcc-2.7.2 rejects duplicate typedefs — `redefinition of \`u8'` / `include/common.h:16: \`u8' previously declared here`, cc1 exit=33. match_one.py:64 then prints CC1 FAIL and exits(1). The draft is DISCARDED WITHOUT EVER BEING SCORED. +CLEAN RESULT worth recording: the regex's hardcoded NAME list is COMPLETE — include/ defines exactly 15 scalar typedef names and the regex knows all 15 (latent name collisions: 0). The bug is purely the line-shape anchor, not the name set. +- **blast radius:** LOST CANDIDATES — LIVE, bounded: 42 draft .c files in the corpus carry the un-strippable multi-typedef-per-line shape, accounting for the 55 skipped typedef lines. Each one hard-fails cc1, so match_one returns FAIL rather than a score, and the draft is never compared to the target at all. In an interactive run an agent sees the CC1 FAIL text (loud). In an automated harvest/sweep, where a nonzero match_one exit is simply booked as "no match", a potentially BYTE-EXACT draft is silently thrown away for a cosmetic formatting reason. I checked the live default path honestly: 0 of the 42 currently sit in the three auto-searched dirs (.run/drafts3|2|), so today's damage is confined to explicit --c invocations (which the sweep pipelines do use). This is armed and has fired historically, but it is not currently eating the default queue. +- **R33:** KEEP but INVERT the control flow — and this is the honest R33 answer: the scanner is re-deriving "which typedefs does common.h already provide" by hardcoding a list AND re-deriving "which draft lines redefine them" by regexing C text. The compiler already knows both, authoritatively. The robust design is to stop guessing: preprocess+compile, and only if cc1 emits `redefinition of` do you strip and retry (and per the corpus fact, gcc-2.7.2 does NOT prefix with 'error:', so match on the diagnostic text `redefinition of`, never on the word 'error'). That makes a parse hole CONSERVATIVE (an extra retry) instead of WRONG (a discarded byte-exact draft) — the harvest_verify pattern applied to typedef stripping. +- **fix:** Drop both anchors. Use `re.compile(r'\btypedef\b[^;{}]*\b(u8|u16|u32|u64|s8|s16|s32|s64|f32|f64|M2C_UNK|M2C_UNK8|M2C_UNK16|M2C_UNK32|M2C_UNK64)[ \t]*;')` — no `^`, no trailing `\n`. The `[^;{}]*` (braces excluded) still guarantees it can never reach inside a struct/union body, so the docstring's promise to "never strip the draft's own custom struct typedefs" is preserved. Better still (R33): derive the name list from include/common.h's actual typedefs instead of hardcoding it, so the two cannot drift. +- **assertion (R32):** R32: after stripping, assert NO typedef of an include/-provided name survives anywhere in the source text — i.e. re-scan the stripped source and hard-fail if any of the 15 known names is still being typedef'd. That is a direct coverage assertion (candidates vs parsed) and it catches every future line-shape the regex has not anticipated, permanently. Separately, assert the hardcoded name set == the typedef names parsed out of include/common.h. +- **skeptic:** RAN: (a) my own scanner over .run/drafts*/*.c (10,731 files) -> candidates=5638, SCALAR_TYPEDEF_RE matches=4719, gap=919 — exact reproduction. (b) My own independent classifier of the 919 (split each line on ';', check whether any declarator's terminal identifier is one of the 15 names include/common.h really defines) -> 55 collide / 864 justified, across 42 files — exact reproduction; the 864 are genuinely justified (multi-line `typedef struct {` openers, draft-local names like Blob8/S801563EC). (c) Shape audit: 55/55 skip lines have >1 semicolon, 9 distinct shapes, all multi-typedef-per-line — the claimed anchor mechanism is the only one present. (d) Cited locations verified: masked_diff.py:36-40 (regex), match_one.py:51 (the .sub call), and all three example file:lines exist verbatim. (e) Consequence executed against the real pinned cc1 (tools/bin/gcc-2.7.2-psx/cc1, match_one's exact flags): exit=33, "redefinition of `u8'" / "include/common.h:16: `u8' previously declared here" -> match_one.py:64 prints CC1 FAIL, exit(1). (f) THE COUNTERFACTUAL that defeated my skepticism: .run/drafts-t3b1-fb/func_8015C030.c, with NEWLINES as the only edit (split the multi-typedef lines), goes from CC1 FAIL to "MATCH (23 ins)" — and func_8015C030 is STILL unbanked (134 INCLUDE_ASM stubs, e.g. src/ov_SC06_008/ov_SC06_008_jr_8015AE2C.c:1556). A masked-MATCH draft for a live 134-site family target is being discarded over whitespace. BOUNDS I VERIFIED (kept it at MEDIUM, not higher): 0 of the 42 files sit in the default auto-searched dirs (.run/drafts3|2|), so damage needs an explicit --c; silence is caller-dependent (auto_driver.py:93 books it ('fail',-1) and ab_score.py:56 books it 'compilefail' — truly silent; api_draft.py:199 feeds the CC1 FAIL text back to the drafter, self-healing; interactive is loud); and it is a false NEGATIVE only — no wrong bytes can pass through it, no false green. Counts, mechanism, evidence, and consequence all reproduce; I could not shrink it. + +### [MEDIUM] `derive_canonical_sigs.py` — DOWNGRADED +- **scanner:** tools/derive_canonical_sigs.py:100 asm_arity() via `asm_dir = asm/{source}/nonmatchings/{source}` (:130) — a hardcoded subdir; AND tools/derive_canonical_sigs.py:77 a_role() `src = set(a_in_order) - dest` +- **counts:** candidates **264** / parsed **13** / real skips **251** +- **evidence:** TWO independent holes in the arity oracle — the same class as the callee-signature hole that once made nine byte-exact functions look like a compiler wall. + +(1) PATH REGRESSION. asm_arity() opens `asm//nonmatchings//func_.s`. But asm/ov_SC01_077/nonmatchings/ now has TWELVE subdirs (ov_SC01_077, _a, _after, _o0, _jr_8012ACE0, _jr_801380E0, _jr_8015444C, _jr_8015AE2C, _jr_8016AB6C, _jr_801734BC, _jr_80178D40, _jr_80182268). The hardcoded dir holds 13 .s files; the true corpus is 264. 95% of lookups return 'no-asm'. This is a REGRESSION, not an original bug: all 20 rows in .run/canonical_sigs.json show asm='ok', so the files WERE in the flat dir when the tool ran at Phase 17 — the Phase-19/24/26 -O0 and jr splits fragmented the tree out from under it. + +(2) READ-BEFORE-WRITE BUG. a_role() computes `dest = {regs[0]}` then `src = set(a_in_order) - dest`. That subtraction DELETES the read whenever an a-reg is both the destination and a source of the same instruction — so `sll $a0, $a0, 2` (a0 = a0 << 2) marks a0 as SCRATCH, not a param. MEASURED over all 60,740 .s files in the fleet asm corpus against a corrected read-before-write reference: asm_arity() DISAGREES on 1,691 functions (2.8%), and it is an UNDER-count in 1,691 of 1,691 cases — never once an over-count. Distribution: 0->1 (875), 3->4 (272), 1->2 (195), 2->3 (147), 2->4 (137), 0->2 (21), 0->3 (17), 0->4 (17). Offending first-touch mnemonic: sll (778), lw (44), andi (38), addiu (27), lh (14), slti (7). 875 of these emit `extern s32 func_X(void);` for a function that actually takes an argument. + +A third, minor hole: GHIDRA_SIG_RE (:122) anchors the closing paren to end-of-line and so cannot read a wrapped signature — 9 of 861 files in the real .run/ghidra_c cache silently return None (e.g. .run/ghidra_c/func_801634D8.c, func_801777BC.c). +- **blast radius:** LATENT. No live damage today, and I checked rather than assumed: the tool's input .run/conflict_callees.json is `[]`, so it emits nothing. Its one persistent output, .run/canonical_sigs.json (20 rows, Jun 19), predates the asm split and I re-verified the four highest-risk rows — func_801759D8 / func_80175820 / func_801758FC all derive arity=0 from asm alone, and the asm confirms it (`lui $a2, %hi(D_800B9A02)` is a genuine dest-only scratch use). The 20 baked sigs are correct. The arming is severe though: the two holes COMPOUND. Arity is reconciled as max(ghidra, asm); with 95% of asm lookups now returning 'no-asm' and the ghidra_c cache covering only 861 functions, the next run on any split overlay would derive arity from nothing at all and emit `(void)` prototypes into engine_core.h — the shared header every drafting agent is fed. +- **R33:** DELETE THE ASM HEURISTIC. This scanner reads MIPS text to guess how many arguments a function takes — but the proven invariant answers it exactly: a function NOT wrapped in INCLUDE_ASM is byte-exact, so its C definition IS its signature, byte-verified, no guessing. The A/B I ran on ov_SC01_077 shows 49 of 57 live callee edges are now `defined` and 31 `declared` — the banked code already knows nearly every signature this tool is reverse-engineering. The heuristic should survive ONLY as a last resort for a callee that has never been matched in ANY of the 136 binaries, and every use of it should be logged as a guess. Today it is the primary path and the banked truth is the fallback; that is backwards. +- **fix:** (1) Resolve the .s by GLOB, not by construction: `glob(f'asm/{src}/nonmatchings/**/func_{A}.s', recursive=True)`, and make a zero-hit an explicit error, not a silent 'no-asm' fallback. (2) In a_role(), an a-reg that appears in ANY source position is a param even when it is also the dest — compute src from the operand positions that are reads, and only mark scratch on a dest-ONLY first touch. (3) Anchor GHIDRA_SIG_RE with re.S / allow the param list to wrap. +- **assertion (R32):** assert that every callee in the input resolves to a real .s file OR a real ghidra_c entry — zero silent 'no-asm' fallbacks, and abort with the list if any callee resolves to neither. Separately, a regression test pinning a_role() on the four idioms it currently gets wrong: `sll $a0,$a0,2`, `lw $a0,0x0($a0)`, `andi $a0,$a0,0xff`, `addiu $a0,$a0,8` — all four must yield arity>=1. +- **skeptic:** RAN: (1) Imported the tool's own asm_arity() and drove it over all 264 .s basenames under asm/ov_SC01_077/nonmatchings/** using the tool's own path construction (:130). Reproduced candidates=264 parsed=13 no-asm=251 (95.1%) EXACTLY. Confirmed 12 subdirs, 13 files in the flat dir, 264 unique basenames. Decisive regression proof: all 20 rows in .run/canonical_sigs.json recorded asm='ok', yet 0/20 of those .s files resolve in the flat dir today. Path regression CONFIRMED, count stands at 251. (2) Confirmed a_role('sll','$a0,$a0,2') -> (set(), {'a0'}) — the read IS deleted. Fleet-wide over 60,740 .s: 1,691 disagreements (2.78%), 0 over-counts, transition histogram matches item-for-item. BUT THAT IS CIRCULAR — it compares the tool to the claimant's own corrected reference, which cannot establish who is right. Built a real oracle from the proven invariant: banked byte-exact C definitions cross-checked against shared-engine .s that are byte-identical across ALL overlays (n=952). (My first attempt was wrong — I cross-matched func_.s across overlays where the same address is DIFFERENT code; discarded and re-ran with a byte-identity filter.) RESULT: the TOOL is 63.7% correct with under-counts 111 and OVER-counts 235; the 'corrected' version is 64.6%; the fix helps 11 and breaks 2. The tool's DOMINANT error mode against ground truth is OVER-counting, which the claimed bug neither explains nor fixes. The claim's 'under-count in 1,691 of 1,691, never once an over-count' is an artifact of the reference, not a measure of harm — bug (2) is ~1 point of a 36-point error rate. Magnitude REFUTED, mechanism real. (3) GHIDRA_SIG_RE: reproduced exactly, 9 of 861 return None including both named files. (4) BLAST RADIUS LATENT (claimant conceded): .run/conflict_callees.json is `[]` (2 bytes, unchanged since Jun 19) so the tool emits nothing; grep shows NO Makefile/shell/script invocation, only docs + PhaseEnd references; the 20 baked rows are correct. Latent + no live consumer + one of two bugs materially oversold => HIGH downgraded to MEDIUM. NOTE FOR THE PARENT: my ground-truth run STRENGTHENS the R33 'delete the asm heuristic' verdict while DESTROYING the evidence offered for it — the real case is not the read-before-write bug, it is that the heuristic is 36% wrong against byte-exact banked C, which was available the whole time. + +### [LOW] `gate_stage.py` — DOWNGRADED +- **scanner:** tools/gate_stage.py:215-232 — the backlog loop reads each non-verified draft from `d`, but `d` was rebound at line 185 to sig_unify's OUT dir, which contains ONLY the drafts sig_unify rewrote. `body = open(cpath).read() if os.path.exists(cpath) else ""` then silently yields `("fail", None)`. +- **counts:** candidates **12** / parsed **1** / real skips **1** +- **evidence:** MEASURED by running the real transform on a real draft dir: + $ .venv/bin/python tools/sig_unify.py --overlay ov_SC01_077 --in .run/drafts-giants --out + 'drafts unified: 1' -> in=12 out=1 +sig_unify emits only the drafts it rewrote. gate_stage line 185 does `d = _xform("sig_unify.py", ...)` and from that point `d` IS the 1-file dir. Line 216 then reads every stage-1 failure from it: + cpath = os.path.join(REPO, d, fn + ".c") + body = open(cpath).read() if os.path.exists(cpath) else "" + kind, close = match_one_closeness(fn, cpath, asm) if body else ("fail", None) + ... else: status, where = "failed", note or "won't compile standalone (loose-typing / missing decl)" + draft_path = backlog.save_draft(fn, body) if body else None +So for 11 of the 12, the record written is: status="failed", where_stuck="won't compile standalone (loose-typing / missing decl)" — a FABRICATED diagnosis; the draft was never compiled — and best_draft=None, which THROWS THE DRAFT TEXT AWAY. The draft still exists in d1 (.run/drafts-giants-cn-cast-rc) and in the original drafts dir; gate_stage just doesn't look there. This fires on essentially every run, because `d` is rebound whenever fails1 is non-empty. + +MEASURED in the real .run/backlog.jsonl (4255 records): + 709 status="failed" + 585 carry the fabricated "won't compile standalone (loose-typing / missing decl)" + 73 have best_draft = null (the draft text was discarded) + 63 of those 73 STILL have a draft .c sitting on disk — proof the text existed and the record says it didn't + 47 distinct fn+binary + 10 are STILL OPEN INCLUDE_ASM stubs today (e.g. func_801379FC, func_8012E138, func_8012E014, func_80129C40, func_80138DE0, func_80131D68 — all ov_SC01_077) +A further 165 of the fabricated-verdict records are for functions whose .s lives ONLY in a split subdir (e.g. func_80161208/ov_SC01_000 -> ov_SC01_000_jr_8015AE2C): match_one was handed the main asm-subdir, could not find the target, returned fail, and the same fabricated string was written. +- **blast radius:** CORRUPT SELECTION + destroyed drafts, live. The backlog is the work queue: grinder.py and wave_targets.py --class read `status` and `klass` to decide what to grind. A record saying 'failed / won't compile standalone' is triaged as garbage; the same function honestly recorded as 'near / 3 mismatch' is a prime permuter target. 585 records carry the fabricated verdict; 73 had their draft destroyed; 10 of those functions are open stubs today, i.e. 10 functions are sitting in the queue mislabelled as compile-junk right now. Zero risk of a wrong bank (the SHA1 gate is untouched) — this is purely a poisoned instrument, and it poisons exactly the decision 'is this a compiler wall or is this plumbing?' +- **R33:** PARTIAL DELETE. The 'did it bank?' half of this loop re-derives nothing — the byte-gate already answered it, and gate_stage correctly uses `verified`. The closeness number is genuine new information that no invariant supplies, so match_one_closeness earns its keep. What must be deleted is the FABRICATED FALLBACK: `else ("fail", None)` + the canned 'won't compile standalone' string. A tool that cannot observe an outcome must say 'not measured', never invent one. That single string is the audit's whole thesis in miniature — it is the mechanism by which plumbing failures get filed as compiler walls. +- **fix:** In the backlog loop, resolve the draft body with a fallback chain: `d` -> `d1` -> the original drafts dir; only then conclude there is no body. And pass the asm-subdir that actually holds fn's .s (derive it from asm//nonmatchings/*/.s) rather than the current call's --asm-subdir. Never emit 'won't compile standalone' unless a compile was actually attempted and actually failed — carry match_one's real first line into where_stuck instead. +- **assertion (R32):** R32: assert that every fn in `[f for f in draft_fns if f not in verified]` resolves to a NON-EMPTY body before a record is written; a body-less non-verified draft is a hard error. And assert `sum(near) + sum(failed) + len(verified) == len(draft_fns)` with `failed` only ever incremented after a real compile attempt. +- **skeptic:** RAN: (a) re-ran the claim's repro `.venv/bin/python tools/sig_unify.py --overlay ov_SC01_077 --in .run/drafts-giants --out ` -> reproduced in=12/out=1, THEN asked why. Cross-checked each of the 12 against src/ov_SC01_077/*.c: 10 of them are ALREADY BANKED (compiled definitions in the _jr_* split files, no longer INCLUDE_ASM stubs). sig_unify.py:182-183 `if fn not in cur_stubs: continue` is CORRECT to skip a non-stub -> JUSTIFIED EXCLUSION, not a silent skip. The other 2 are live stubs: func_801412A8 (main .c) and func_80178004 (a _jr_ split). Pointing sig_unify at the main .c emits exactly func_801412A8; re-running with `--src-file src/ov_SC01_077/ov_SC01_077_jr_801734BC.c` emits exactly func_80178004. So in=12->out=1 is what a CORRECT tool does; the claim ran a stale drafts dir against a tree where the work had since been banked and read the correct drop as a bug. Real skips in that repro = 1 (only under a --src-file misconfiguration, and gate_stage already carries --src-file for exactly this). Claimed 11 is refuted. +(b) Parsed the real .run/backlog.jsonl (4255 recs). Counts partially reproduce: 709 failed, 585 with the canned string, 73 best_draft=null. BUT 512 of the 585 HAVE a saved draft -> body was non-empty -> match_one genuinely RAN and returned fail. Those are a real measurement wearing a canned label, NOT the d-rebind bug. Only 73 carry the true fingerprint (failed + best_draft=null + closeness=null), 47 distinct fn+binary. The "585 fabricated verdicts" is inflated ~8x. +(c) BLAST RADIUS IS ZERO TODAY — this kills CRITICAL. Ran the real selectors. grinder.py:80 reads backlog.load_best(), which scores null-closeness at 10**9, so a poison record ALWAYS loses to any honest record for the same fn. All 10 still-open victims — including EVERY function the claim named by name (func_801379FC, func_8012E138, func_8012E014, func_80129C40, func_80138DE0, func_80131D68) — surface in the LIVE backlog as status=near, closeness=0, best_draft=yes. Zero are poison-only. The claim's headline ("10 functions sitting in the queue mislabelled as compile-junk right now") is FALSE on its own examples. wave_targets.backlog_walls() is the only consumer reading raw JSONL: 35 names are walled by a poison-only record, 10 are excluded anyway by plumbing_blocked|reserved_walls, and the 25 net-new are ALL already banked -> filtered by the live_stubs() gate at wave_targets.py:190 BEFORE walls is consulted. Net live exclusions: 0. +(d) "Destroys the draft" overstated: save_draft is skipped but the text survives in d1 and the original drafts dir (the claim concedes this). Lost pointer, not lost bytes. +SURVIVES: the fabricated fallback string is a genuine instrument defect — `else ("fail", None)` + the canned "won't compile standalone" invents a diagnosis it never measured, and 73 ledger records prove it fired. R33's "say not-measured, never invent" is the right fix. But the consequence is LATENT: every current consumer either prefers the honest record (load_best) or stub-gates the victim out. Severity LOW, not CRITICAL — a real cosmetic/instrument bug worth fixing, not corrupt live selection. + +### [LOW] `gate_stage.py` — DOWNGRADED +- **scanner:** tools/gate_stage.py:243 — `mm = re.search(r"byte-identical\s+:\s+\d+\s*/\s*\d+\s*=\s*([\d.]+)%", r.stdout)` against `progress.py --fleet` stdout +- **counts:** candidates **1** / parsed **0** / real skips **4** +- **evidence:** progress.py --fleet no longer prints that string. tools/progress.py:618 emits: + FLEET fn-count byte-ident: {BYTE:6d} / {MATCH} = {..:.2f}% (REAL+LINKED+empties; FUNCTION-count, x134-inflated ...) +The literal 'byte-identical' does not occur — it is 'byte-ident:'. Verified directly: + re.search(r"byte-identical\s+:\s+\d+\s*/\s*\d+\s*=\s*([\d.]+)%", "FLEET fn-count byte-ident: 12345 / 67890 = 64.86%") -> None +The label was renamed in commit commit:0512 (2026-07-11, 'progress.py --weighted — byte/instruction-weighted metrics'). gate_stage's regex was never updated. It is wrapped in `except Exception: pass` and a None-guard, so it fails in total silence. +The last gate_stage-authored commit is commit:0438 (2026-07-02) — BEFORE the rename — which is why `git log --grep 'fleet None%'` returns 0 today. The gun is loaded, not yet fired. +- **blast radius:** CORRUPT METRICS, LATENT-ARMED (fires on the very next gate run, and every one after). `fleet_pct` is now permanently None: (a) every future gate commit message reads 'feat(...): worker gate — +N fns xM propagated (fleet None%)'; (b) the JSON summary returned to orchestrator.py / grinder.py / bulk_harvest.py carries "fleet_pct": null — that is the number those loops use to judge whether a wave moved the needle. No lost matches, no wrong banks; a blind instrument on the steering wheel. +- **R33:** YES — DELETE THE SCANNER OUTRIGHT. This is the purest R33 case in the group: gate_stage shells out to progress.py and regex-scrapes a human-formatted report line to recover a float that progress.weighted_metrics() returns as a native Python float, derived from the sig/SHA1 invariant. Import the function. The regex should not exist, and its non-existence would have made this bug impossible. +- **fix:** Stop parsing stdout. `sys.path.insert(0,'tools'); from progress import weighted_metrics; wm = weighted_metrics(); fp = wm and wm['fleet_pct']` — an in-process call to the function that already computes it from the sig invariant, with no text in between. +- **assertion (R32):** R32: if progress.py exits 0 and fleet_pct is still None, that is a hard error, not a shrug — `assert fp is not None, 'progress.py output shape changed'`. Any tool that scrapes another tool's stdout must fail loudly when the scrape returns nothing. +- **skeptic:** RAN: (1) `.venv/bin/python tools/progress.py --fleet`, captured real stdout, applied the EXACT regex from gate_stage.py:243 -> match=None, fp=None. Real line is `FLEET fn-count byte-ident: 283995 / 343774 = 82.61%`; literal 'byte-identical' absent. Mechanism REPRODUCES. (2) `git log -S` confirms label was live until commit:0512 (2026-07-11) — genuine regression, regex was correct before. (3) File:line citations both exist and are accurate. + +COUNT IS WRONG — CLAIM UNDERCOUNTS 4x. `grep -rn` for the regex found FOUR identical stale copies, not one: gate_stage.py:243, bulk_harvest.py:182, auto_driver.py:107, lora_grind.py:242 — all scraping the same progress.py --fleet stdout, all now parsing to None. candidates=4, parsed=0, real_skips=4. + +BLAST RADIUS REFUTED (this is what kills HIGH). The claim's load-bearing assertion — "fleet_pct is the number those loops use to judge whether a wave moved the needle" — is FALSE. A branch-grep for any comparison/conditional on fleet_pct/fp across gate_stage, bulk_harvest, auto_driver, grinder, orchestrator, lora_grind returns EMPTY. Zero control flow observes it. Every consumer is display-only: a commit-message f-string, a heartbeat JSON field, a log line, and orchestrator.py:96's [-50:] progress list. The real progress predicates are auto_driver.py:211 `if a.once or pass_banked == 0` (banked counts from the byte-gate) and grinder.py:203 `if a.once` (+ gate-rejection blacklist) — both derived from the SHA1/byte-gate invariant, never from fleet_pct. + +LATENT — NEVER FIRED. `git log --grep 'fleet None'` = 0 commits. Every on-disk heartbeat holding a real value (64.7, 63.67) has mtime <= 2026-07-02, predating the 2026-07-11 rename. (lora_grind's None on 2026-06-30 ALSO predates the rename — that is its last_fp=None initializer under propagate_every, not this bug; not evidence.) + +SEVERITY-CUTTING NUANCE the claim inverts: this does not fail quietly-wrong, it fails to None. `(fleet None%)` in a commit message is conspicuously broken, not a plausible-but-wrong 64%. "CORRUPT METRICS" implies a believable lie; a visibly-NULL instrument is far less dangerous. + +Net: real defect, 4x wider than claimed, but latent, display-only, zero decisions affected, no lost matches, no wrong banks. LOW, not HIGH. The R33 remedy (delete all four scrapers; import progress.weighted_metrics(), which returns the float natively) remains correct and should now cover 4 sites, not 1. + +### [LOW] `harvest_verify.py + gate_stage.py` — DOWNGRADED +- **scanner:** The `func_[0-9A-Fa-f]+|DsMix` symbol allowlist, appearing three times: harvest_verify.py:59 (which drafts are gateable), gate_stage.py:154 (the Bug-B negative control), gate_stage.py:76 (`[w for w in ... if w.startswith("func_")]` — which banked fns are reported as verified) +- **counts:** candidates **60740** / parsed **59686** / real skips **0** +- **evidence:** Over-approximating detector = `INCLUDE_ASM\("([^"]*)",\s*([A-Za-z_]\w*)\)\s*;` over every src/**/*.c. 60740 stubs total; 59686 match func_; 1054 DO NOT (955 distinct symbols) — every one of them invisible to all three scanners: + listCdBuffer x100 — one per overlay, e.g. src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:5549: + INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40", listCdBuffer); + 954 in the main binary (src/libgs6.c x158, src/libmcrd1.c x76, src/800c.c x71, src/libcd1.c x66, src/snd2.c x59 ...): InitHeap, FlushCache, DeliverEvent, OpenEvent, ChangeClearPAD, GsTMDfastTNF3, RotNclip4, TransRotPers, ... +Two further consequences found while measuring: + * `DsMix` in the alternation is DEAD CODE — it has a real definition (src/resident/resident.c:1743 `s32 DsMix(void) {`) and no stub anywhere. + * The main binary is named 'us' (config/check.us.sha) and IS returned by lora_grind.binaries(). gate_stage.py:153 globs `src/us/us*.c` -> ZERO files -> bin_stubs is empty -> line 155 `if bin_stubs and not (...)` is FALSE -> the Bug-B negative control ("0/N drafts are INCLUDE_ASM stubs — banking will be 0") CANNOT FIRE for the main binary. The guard that exists to stop a 0 masquerading as 'nothing matched' is itself silently disabled on the one binary with 2002 open stubs. +- **blast radius:** LATENT (armed, not yet firing) for banking + LIVE for the negative control. Latent because no drafter currently emits a curated-name target: all 128 non-func_ draft filenames on disk are human scratch (_t2.c, probe5.c, func_80177EA4_v3.c). But the trap is fully armed: hand a listCdBuffer draft to harvest_verify and line 59 drops it without a word — a x100-reach function permanently unbankable; and if a curated name ever DID bank, gate_stage.py:76 would strip it from `verified`, so it would not be propagated, would be re-run through sig_unify, and would be logged to the backlog as a FAILURE despite being banked. Live today: the Bug-B guard is off for binary 'us' (2002 stubs, 954 of them non-func_). +- **R33:** DELETE. A name-SHAPE allowlist is re-deriving 'is this symbol a stub?' from a naming convention, when the INCLUDE_ASM line itself already declares it — the symbol is right there in the capture group. The convention is not even true (955 distinct counterexamples). And in harvest_verify specifically the whole filter is redundant with render()'s literal containment check, which is symbol-agnostic and already correct. Same deletion as finding 1. +- **fix:** Match any C identifier (`[A-Za-z_]\w*`) in all three places; drop the dead `|DsMix`; drop the `startswith("func_")` filter in _gate1 (harvest_verify only ever writes names it actually banked, so the filter can only lose). For the negative control, resolve the binary's source files from the build's own object list rather than assuming the src//*.c layout, so 'us' is not silently exempt. +- **assertion (R32):** R32: assert that the count of stubs the tool's regex finds in a file EQUALS the count of `INCLUDE_ASM` tokens in that file (excluding comments) — a fleet-wide invariant that is exactly true today (60740/60740) and would have caught this the day listCdBuffer was named. Additionally assert bin_stubs is non-empty for every binary in config/check.*.sha. +- **skeptic:** RAN: (1) Re-derived the corpus with the same INCLUDE_ASM detector over src/**/*.c — counts reproduce EXACTLY: candidates=60740, parsed=59686, gap=1054, distinct=955; main binary 2002 stubs / 1048 func_ / 954 non-func_; DsMix stubs on disk = 0 (dead alternation CONFIRMED; real def at src/resident/resident.c:1743). All cited lines exist verbatim (harvest_verify.py:59, gate_stage.py:76, gate_stage.py:154). Arithmetic is clean; the INTERPRETATION is not. (2) The 1054 splits into two buckets, both of which die. BUCKET A — 100 stubs = 1 symbol, listCdBuffer: it is NOT A FUNCTION. src/800.c:686 `extern s32 listCdBuffer;` — the 0xE40-byte buffer LIST.CD is read into (src/800.c:737 CdReadRequest(&D_80062C38, &listCdBuffer, 0xE40, 0)). splat mis-carved 0xC bytes at 0x80180000 (past the overlay end) into a fake function whose whole body is buffer bytes decoded as MIPS (addiu $sp,$sp,0x20 / jr $ra / nop). You cannot draft C for a CD read buffer and byte-gate it as a function. JUSTIFIED EXCLUSION — the scanner is right to drop it. The headline "x100-reach function permanently unbankable" is FALSE: not a function, no reach. BUCKET B — 954 main-binary symbols: real functions (GsTMDfastTNF3=99 lines) but MISATTRIBUTED — the allowlist is not what excludes them. src/us/ DOES NOT EXIST (main sources are flat: src/800c.c, src/libgs6.c). Ran lora_grind.open_stubs('us') -> 0 targets, because it globs src/{b}/{b}*.c -> src/us/us*.c -> zero files. Both drivers iterating binaries() (lora_grind.py:205, bulk_harvest.py:92) do `stubs=open_stubs(b); if not stubs: continue` -> 'us' skipped entirely. Grepped every run_gate/harvest_verify call site: nothing passes binary='us' or src=src/libgs6.c. Forced, it CRASHES LOUDLY: harvest_verify.py:57 `src0 = open(a.src).read()` is unconditional and precedes the build -> FileNotFoundError. Deleting the allowlist at all three sites changes NOTHING for these 954. (3) The 'LIVE' Bug-B claim is REFUTED: bin_stubs is indeed empty for 'us' so line 155 can't fire, but it is INERT — run_gate returns early at `if not draft_fns` (upstream of the guard) and nothing can ever produce drafts for 'us'. A guard disabled on a binary the pipeline cannot address is not live damage. (4) The claim MISSED the filter that actually binds: lora_grind.py:30 STUB_RE = INCLUDE_ASM\([^,]+,\s*(func_[0-9A-Fa-f]+)\) — the target-SELECTION layer, func_-only (not even DsMix). Ran open_stubs('ov_SC06_032') -> 554 targets, listCdBuffer NOT among them. So the proposed 3-site deletion surfaces nothing on its own; extended to STUB_RE it would surface only a data buffer that must not be drafted — actively harmful. WHAT SURVIVES (LOW): a code-hygiene point only — a name-shape allowlist re-deriving 'is this a stub?' from a convention the INCLUDE_ASM line already declares, redundant with render()'s containment check, with a dead DsMix arm; and gate_stage.py:76's startswith('func_') on the verified list is a genuine LATENT hazard only IF the main binary is ever wired in (src/us/ created), which would bring the 954 SDK functions into reach. Today it drops ZERO draftable functions. Real silent skips: 0. + +### [LOW] `bank_exemplar.py` — DOWNGRADED +- **scanner:** tools/bank_exemplar.py:68-70 — `errs = [l for l in (b.stdout+b.stderr).split('\n') if re.search(r'conflicting|undeclared|parse error|redefinition', l) and 'built-in' not in l]` ; `last_err = f"{name}: " + (errs[0] if errs else "byte-diff")` +- **counts:** candidates **3** / parsed **0** / real skips **0** +- **evidence:** The classifier is a 4-term allowlist over gcc-2.7.2's diagnostic vocabulary. When NO line matches, the failure is reported to the operator as "byte-diff" — i.e. 'the compiler produced different bytes'. But a build that never compiled also lands there, because bank_exemplar's success test is `b.returncode == 0 and "[ OK ]" in b.stdout`, and BOTH a compile failure and a SHA1 mismatch give returncode != 0. +Demonstrated with the real compiler (tools/bin/gcc-2.7.2-cdk/cc1 -O2 -G0) on a TU with an incomplete struct and a bad call — the two most common draft plumbing faults: + diag.c: In function `f': + diag.c:3: storage size of `x' isn't known + diag.c:3: too many arguments to function `g' + -> lines matching the classifier: 0 of 3 + -> bank_exemplar reports: "[recovered] FAIL — recovered: byte-diff" +The exemplar did not produce different bytes. It produced NO bytes. The operator is told the compiler won't reproduce the function; the truth is 'declare the struct and it banks'. +- **blast radius:** LATENT / diagnostic-only — no corrupt bank is possible (bank_exemplar reverts and exits 1 either way, and the SHA1 gate is untouched). But this is the exact mechanism the audit was convened to kill: a compile failure wearing the costume of a compiler wall. bank_exemplar is the tool a human runs on a HAND-CRACKED exemplar — the highest-value, hardest-won drafts in the project, and the ones whose 'byte-diff' verdict is most likely to be believed and to end the investigation. One mislabelled exemplar can cost its whole x134 family sweep. +- **R33:** DELETE THE CLASSIFIER, keep the reporting. 'Did it compile?' is answered by the build's exit status — a proven signal already in hand — so grepping the compiler's prose to re-derive it is exactly the R33 anti-pattern. The regex adds nothing but a chance to be wrong. Print the compiler's own words and the build's own exit code; classify nothing. +- **fix:** Do not classify by allowlist. Distinguish the two outcomes structurally: run the compile and the SHA1 check as separate observations. If cc1/as/ld exit non-zero -> status COMPILE-FAIL and echo the compiler's own last diagnostic lines verbatim, whatever they say. Only if the link succeeds AND the SHA1 differs may the word 'byte-diff' be used. Never let 'no pattern matched' collapse into a byte verdict. +- **assertion (R32):** R32: assert that 'byte-diff' is only ever printed when an output binary was actually produced (os.path.exists(build//) and the build's own compile steps all exited 0). If no binary exists, the verdict MUST be COMPILE-FAIL. Cover with a test that feeds bank_exemplar a body with an undeclared struct and asserts the reported class is COMPILE-FAIL, not byte-diff. +- **skeptic:** RAN: (a) read tools/bank_exemplar.py — cited code confirmed at :65 (success test) and :68-70 (4-term allowlist + "byte-diff" fallback). (b) Re-ran the repro with the PINNED compiler the build actually uses (Makefile:443 -> CC1_PSX = tools/bin/gcc-2.7.2-psx/cc1, not the cdk cc1 the claim used) on .run/audit/skeptic/diag2.c: "storage size of `x' isn't known" / "too many arguments to function `g'" / "dereferencing pointer to incomplete type" -> classifier matched 0/3 -> "recovered: byte-diff". Mechanism CONFIRMED. Note the claim's cdk evidence was off the real build path: cdk says "syntax error", psx says "parse error" — a HIT under the real compiler. (c) `strings` over the psx cc1 diagnostic vocabulary: 12/44 error-shaped strings matched; all 9 incomplete-type variants, all 4 arg-count variants, incompatible-types, invalid-lvalue, called-object-is-not-a-function escape. COUNTS REFUTED: candidates=3/parsed=0 are three lines of a SYNTHETIC file the claimant wrote, not corpus items. The real corpus is the 11 bank_exemplar run logs in .run/bank*.log: ~30 stage-FAIL lines, 27 classified CORRECTLY (conflicting types / undeclared / redefinition; ValueError paths bypass the classifier). "byte-diff" was emitted exactly ONCE, for one function (func_8013F350), triple-logged across bank9.log/bank_all.log/bank_func_8013F350.log — and phase-ends/CURRENT_PHASE.md:267 records the independent follow-up proving that verdict TRUE ("NOT a plumbing bug — it is a real class... D_8011511C must be struct-typed to force la+offset; no cast fixes it"). It compiled and produced different bytes. So real mislabels in the corpus = 0; the allowlist covers 100% of the fault vocabulary this project's staged ladder has actually produced. BLAST RADIUS latent as claimed: fb.revert()+sys.exit(1) on every failure path, success gated on the SHA1 "[ OK ]" string, and grep -rn "byte-diff" finds no programmatic consumer — operator-facing prose only. Real shape, reproducible on demand, zero occurrences: LOW/latent hardening, not a MEDIUM live bug. The R33 fix (print the compiler's words + exit code, classify nothing) is still correct and cheap. + +### [LOW] `jr_isolate_all.py` — DOWNGRADED +- **scanner:** jr_inventory() — the `banked` set: tools/jr_isolate_all.py:85-87 `for bj in glob.glob(REPO/.run/banked_func_*.json): cand.add(basename[len('banked_'):-len('.json')])`, then :94 `banked = {a: nm for a, nm in realc.items() if nm in cand}` +- **counts:** candidates **1202** / parsed **1199** / real skips **3** +- **evidence:** OVER-APPROX = every committed `.rodata` carve piece in the 134 overlay configs (1202). Each is, by construction, owned by a banked jr. REAL = carves whose owner jr_inventory+carve_owners can resolve = 1199. THE 3 ORPHANS, all confirmed against the real corpus: + +(1) config/splat.ov_SC01_000.yaml: ` - [0x7c21c, .rodata, ov_SC01_000_jr_8017BEBC] # Phase-26 §8 jtbl-rodata carve (jtbl_carve.py)` + src/ov_SC01_000/ov_SC01_000_jr_8017BEBC.c:2954: `void func_8017BEBC(s32 arg0)` <- real C, banked, owns that carve. + BUT `ls .run/banked_func_*.json` = {8012ACE0, 801380E0, 8013FFD8, 8015444C, 8015A3C8, 8015AE2C, 8016AB6C, 801734BC, 80178D40, 80182268} — NO banked_func_8017BEBC.json. It was banked without a family sweep (or the roster was cleaned), so `cand` never contains it. + +(2)+(3) config/splat.ov_SC02_000.yaml and config/splat.ov_SC02_003.yaml: ` - [0xbe944, .rodata, ov_SC02_000_jr_8017FCB0]` / `..._jr_8017FCB0]`. func_8017FCB0 is the CROSS-ADDRESS sibling of exemplar func_80182268 (ov_SC01_077 @0x80182268 -> ov_SC02_000/003 @0x8017FCB0 — jtbl_family_bank.py:107-113 documents exactly this remap). The roster file is named after the EXEMPLAR (`.run/banked_func_80182268.json`), so the sibling's OWN symbol name func_8017FCB0 never appears in `cand`. Cross-address banking is STRUCTURALLY invisible to this scanner. + +AND THE TIME BOMB: `git check-ignore -v .run/banked_func_8012ACE0.json` -> `.gitignore:22:/.run/`. The roster is EPHEMERAL, GITIGNORED scratch. A `rm -rf .run`, a fresh clone, or another agent's cleanup makes ALL 1200 banked jr invisible at once. +- **blast radius:** 3 LIVE ARMED overlays TODAY (ov_SC01_000, ov_SC02_000, ov_SC02_003). Mechanism, verbatim from the tool's own byte-proven docstring (jr_isolate_all.py:119-127 and :230-237): a banked jr missing from `banked` (a) is not added to `only_addrs` by the forced-cut rule, so no cut is made at its address, and (b) is missing from `banked_by_obj`, so `carve_moves` never repoints its carve. A new core isolated into the same code object therefore leaves the orphan inside the region, and ONE object must emit TWO far-apart jump tables into ONE contiguous `.rodata` — the exact byte-proven `0x1C -> 0x34` / image-+33B corruption the comment cites. The whole-binary SHA1 gate catches the corrupt image, so this is NOT a corrupt-binary risk: it is LOST MATCHES, silently mis-attributed. jtbl_family_bank.py:123-129 calls isolate() lazily on any NON-CONTIGUOUS carve collision, and its failure surfaces as a generic `"gate-fail"` / `"isolate-fail"` line — indistinguishable from a genuine compiler wall. ov_SC01_000 is the first sibling of every ×134 sweep, so the very next family that collides in that object silently loses it. +LATENT (armed, whole-fleet): if `.run/` is ever cleared, ALL 1202 carves orphan simultaneously — every subsequent isolation in every overlay corrupts. The tool has no assertion that would notice. +- **R33:** YES — DELETE THIS SCANNER. This is the purest R33 case in the group. The tool re-derives "which functions are banked" from an ephemeral, gitignored JSON roster written by a *different* tool, when two proven invariants already answer it exactly: (1) the committed splat config lists every `.rodata` carve, and (2) INCLUDE_ASM pastes original asm, so a function with real C in a `.c` and no stub IS matched — and `family_remap.reloc_targets` reads the ORIGINAL IMAGE BYTES to say which function owns which jtbl address. The roster adds nothing but a failure mode. `carve_owners()` is already the invariant-derived model sitting right next to it (jr_isolate_all.py:158-176, whose own docstring explains it must read the image because an asm scan misses matched functions) — it is simply being fed a roster-filtered input set. Remove the filter and the bug class is gone by construction. +- **fix:** Delete `cand` and the `.run/banked_func_*.json` glob entirely. `banked` must be derived, not re-parsed from scratch state: (a) the authoritative carve list is already in the config — `rodata_carves(cfg_lines)` returns all 1202; (b) the authoritative owner is already computable from the IMAGE — `carve_owners()` already calls `family_remap.reloc_targets(ov, addr)`, which reads the extracted bytes and needs no roster. So: enumerate EVERY real-C function in the overlay (parse_overlay_c `def`/`define` items — measured at 1604/1604 files, round-trip exact, zero misses), run reloc_targets over all of them, and let a function be `banked` iff it references a carve offset. No name whitelist, no scratch file, cross-address-immune, curated-name-immune. Then ASSERT every carve offset in the config resolves to exactly one owner — and sys.exit if not. +- **assertion (R32):** R32: in `plan()`, after computing `banked`, assert `set(rodata_carves(cfg_lines) offsets) == set(carve_owners(...).keys())`. Any carve with no resolvable owner is a hard sys.exit, not a silent pass. Today that assertion fires on 3 of 134 overlays — which is precisely why it must exist. +- **skeptic:** COUNTS REPRODUCE, CONSEQUENCE DOES NOT. Ran (read-only, scratch in .run/audit/skeptic/): + +(1) rederive.py — re-derived from the real corpus via the tool's own rodata_carves/jr_inventory/carve_owners over all 134 configs: 1202 carves / 1199 owned / 3 orphans, exactly the three claimed (ov_SC01_000 0x7c21c, ov_SC02_000 0xbe944, ov_SC02_003 0xbe944). All 1202 carves are jtbl carves (no confounding .rodata pieces). Cited evidence all exists: splat.ov_SC01_000.yaml:128; src/ov_SC01_000/ov_SC01_000_jr_8017BEBC.c:2954 is a real `void func_8017BEBC(s32 arg0)` def; roster = 10 files, no 8017BEBC; git check-ignore -> .gitignore:22:/.run/. So the scanner gap is REAL: the tool's model says 1199 when truth is 1202. real_skips=3 stands. + +(2) leader.py — tested the invariant the whole consequence depends on. 1196/1199 banked jr are the LEADER (start vram) of their own code object, AND ALL 3 ORPHANS ARE LEADERS TOO (vram 0x80128158 + 0x53d64 = 0x8017BEBC; + 0x57b58 = 0x8017FCB0; each is its own `- [off, c, ov_..._jr_]` object whose carve already names it). That defuses the mechanism by construction, per the tool's OWN code: jr_isolate_all.py:220 `sub = nm if lo is None else subseg_name(...)` — region 0 KEEPS the object name — and cuts are a subset of jr addrs in the object, hence all >= object start, so no cut can ever land below a leader. The orphan therefore always remains in the object its carve already points to: carve_moves has nothing to move. The "missing" forced cut is a no-op anyway — cutting at the leader empties region 0, which lines 211-218 EXPLICITLY skip. + +(3) proof.py/proof2.py — empirical kill shot. Monkeypatched jr_inventory to INJECT the missing orphan, then ran plan()+build_new_config() for EVERY remaining jr core in the 3 affected objects (1 + 11 + 11 = 23 isolations). Config lines, src file set, and carve_renames IDENTICAL in all 23. The claimed 0x1C->0x34 / image-+33B corruption is UNREACHABLE for a region leader. + +(4) hazard4.py — fleet-wide hazard scan (banked jr that is a NON-leader with an isolatable core below it, the only shape that can strand): HAZARD TODAY = 0. The "3 LIVE ARMED overlays" blast radius is FALSE. + +(5) proof3.py + hazard4.py — the `rm -rf .run` time bomb, tested by running build_new_config with an EMPTIED roster. Mechanism is real but grossly oversized: there are exactly 3 non-leader banked jr fleet-wide (ov_SC01_077/func_8013FFD8, ov_SC04_008/func_8015444C, ov_SC05_009/func_8015444C), ALL currently visible in the roster, and only ONE has a core above it. Wiped-roster hazard = 1 pair: isolating func_8013F350 strands banked func_8013FFD8 (confirmed divergence: the forced cut `- [0x17e80, c, ov_SC01_077_jr_8013FFD8]` disappears and the carve repoints to `ov_SC01_077`). Not "all 1202 orphan, every overlay corrupts" — 1 overlay, 1 core. + +VERDICT: 3 genuine silent skips (not justified exclusions — the tool's model is wrong), but ZERO consequence today; the live-damage story is refuted, and the latent radius is 1 reachable case, gated behind a scratch-dir wipe and caught by the SHA1 gate. LATENT, not CRITICAL. The R33 fix (drop the roster filter; feed carve_owners the full real-C def set) is still correct and cheap — the leader invariant that currently saves the tool is nowhere enforced, and the cross-address sibling naming (exemplar-named roster file) will keep re-manufacturing orphans — but this is hygiene, not a fire. + +### [LOW] `jtbl_family_bank.py` — DOWNGRADED +- **scanner:** bank():110 `to_func = "func_%08X" % to_addr` feeding stub_file():40-44 (`re.search(rf'INCLUDE_ASM\("[^"]*",\s*{func}\);', ...)`) and the identical re.search at :152 +- **counts:** candidates **58717** / parsed **58617** / real skips **0** +- **evidence:** OVER-APPROX = every `INCLUDE_ASM("...", )` stub in every overlay .c, whatever the symbol: 58,717. Symbols the tool can ever name (it CONSTRUCTS `func_%08X` from the member address, it never reads the symbol): 58,617. GAP = 100, all one shape — the curated name `listCdBuffer`: + src/ov_SC01_001/ov_SC01_001_jr_80178D40.c:4322: `INCLUDE_ASM("asm/ov_SC01_001/nonmatchings/ov_SC01_001_jr_80178D40", listCdBuffer);` + src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:5549: `INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40", listCdBuffer);` + ...100 overlays. +config/symbols.us.txt:981: `listCdBuffer = 0x80180000; // data` — a FUNCTION at 0x80180000 carrying a symbol annotated `// data`. It is real code: asm/ov_SC01_001/nonmatchings/ov_SC01_001_jr_80178D40/listCdBuffer.s exists in 100 overlays, and in 4 of them the .s references a `jtbl_` (i.e. it is a genuine jr/switch function). +If a family member sits at 0x80180000, bank() builds `func_80180000`, stub_file() finds nothing, and the sibling returns status `"no-stub"` — reverted and dropped. It is not even a loud failure: `no-stub` is printed as one line among 134 and looks like a legitimate skip. +I ALSO VERIFIED THE REGEX FORM IS FINE: 58,717/58,717 stubs match the strict `INCLUDE_ASM("...", sym);` form (0 misses), and the `src/{ov}/{ov}*.c` glob sees 1604/1604 overlay .c including every `_jr_*` split file (0 missed). The defect is purely the NAME CONSTRUCTOR, not the regex. +- **blast radius:** LATENT — 0 live losses today, and I checked rather than assumed: I indexed all 6,283 families in .run/family_hseq.json by member address; `0x80180000` appears in ZERO families, so no current sweep can reach it. But the reason it is absent is itself the same bug one layer up — the family engine cannot see a curated-named function either (that is task A4's territory, flagged here as a cross-tool corroboration). The moment 0x80180000 is enumerated as a family member, 100 siblings across 100 overlays are silently dropped as `no-stub` — and 4 of them are jr/switch functions, i.e. exactly this tool's target class. This is a 100-sibling armed loss sitting one upstream fix away. +- **R33:** PARTIAL. stub_file itself cannot be deleted — the tool genuinely must locate the stub to splice C over it. But the ADDRESS->NAME direction is being re-derived by string formatting when the symbol table (config/symbols.*.txt, already loaded by load_ov_syms and already proven complete) is the authority. That is the same anti-pattern in miniature: guessing a fact that a proven artifact already states. Note in contrast that this tool's GATE — bank():182 `if b.returncode == 0 and "[ OK ]" in b.stdout` — is exemplary R33: `make build` depends on `check` (Makefile:536 `build: check`), and check:571-577 prints `[ OK ]` only on a SHA1 match against config/check..sha. The verdict is derived from the invariant, not re-parsed. Keep that; fix the naming. +- **fix:** Never CONSTRUCT the symbol name from the address. Resolve it: the members.json entry gives (ov, addr); look the address up in the overlay's merged symbol table — `overlay_src_split.load_ov_syms(ov)` already builds it (measured: it resolves 1604/1604 files' items with zero unaddressed anchors) — and invert it to get the real symbol, falling back to `func_%08X` only when the address has no curated name. Then stub_file/re.search/jtbl_carve --func all receive the TRUE symbol. Correspondingly, jtbl_carve.func_subseg():79 `int(func[len("func_"):], 16)` must stop deriving the address from the name (it ValueErrors on all 100 curated stubs) and take the address as an argument. +- **assertion (R32):** R32: bank() must assert that the symbol it resolves for (to_ov, to_addr) actually has a stub in the TU, and must distinguish `no-stub-because-already-matched` (legitimate skip) from `no-stub-because-the-name-was-guessed-wrong` (a bug) — the latter is a sys.exit, never a tallied status. Coverage assertion for the group: for every overlay, `{symbol of every INCLUDE_ASM stub}` must be fully resolvable by the tool's naming path — 58717/58717, not 58617. +- **skeptic:** RAN: (a) own parser over the tool's own glob src/{ov}/{ov}*.c across the 134 ov_* dirs -> candidates=58717, func_%08X-nameable=58617, gap=100, single distinct symbol 'listCdBuffer'. Counts REPRODUCE EXACTLY. Loose-vs-strict INCLUDE_ASM( check: 58738==58738, so the regex/glob really are fine. (b) Verified all cited file:line evidence verbatim (src/ov_SC01_001/..._jr_80178D40.c:4322, src/ov_SC01_077/...:5549, config/symbols.us.txt:981) and that listCdBuffer is real MIPS code. The name-constructor defect SHAPE is real. + +BUT THE MAGNITUDE IS FABRICATED. (c) Parsed the size header of all 100 listCdBuffer.s: they are ~57 DIFFERENT functions (3 ins to 866 ins: 0xC, 0x10, 0x128, 0xD88...). 0x80180000 is just the overlay code base address, so one curated symbol lands on a different per-location function in each overlay. There is no '100-sibling family' and there never can be -- different sizes => different h_seq => different families. (d) This tool only banks jr/switch cores (jtbl_carve.py:227 sys.exits 'references no jtbl_'). grep of the 100 .s for jtbl_: only 4 -- and 2 of those 4 are '/* Handwritten function */' (ov_SC03_118, ov_SC03_119), permanently unbankable. Real ceiling = 2 functions (ov_SC01_084 111 ins, ov_SC03_108 184 ins), in 2 different families, each losing ONE member. Not 100. + +BLAST RADIUS ZERO TODAY AND DOUBLE-GATED. (e) Indexed all 6,283 families in .run/family_hseq.json: 0x80180000 appears in ZERO `members` lists; grep over every .run/members_*.json ever fed to the tool: 0 hits. (f) Stronger than the claim admits: only 4 of the 100 listCdBuffer instances exist in the sig corpus AT ALL (I diffed the 100 listCdBuffer overlays against .run/sig.ov_*.jsonl: 96 have no sig entry at 0x80180000), and NEITHER of the 2 bankable victims is among them. So arming this needs TWO upstream fixes (sig corpus must emit them, AND family_hseq.load()'s stub regex func_([0-9A-Fa-f]+) must stop misfiling them as `matched`) -- not 'one upstream fix away'. + +MECHANISM THAT DOES SURVIVE: jtbl_carve is purely address-driven (addr = int(func[len("func_"):],16)), so for a genuine jr fn at 0x80180000 the carve WOULD succeed under the bogus func_80180000 and stub_file() would then return None -> silent `no-stub`. Real path, 2 possible victims, currently unreachable. Also verified the praised gate is sound (Makefile:536 `build: check`; check:571-577 prints [ OK ] only on SHA1 match). + +CORRECTED: LOW, 0 live silent skips, latent ceiling 2 (not 100). Worth the 3-line fix (resolve the name from config/symbols.*.txt, already loaded by load_ov_syms) but not a MEDIUM finding and not a '100-sibling armed loss'. + +### [LOW] `jr_isolate_all.py` — DOWNGRADED +- **scanner:** jr_inventory():78-82 — `if re.fullmatch(r'func_[0-9A-Fa-f]{8}', fn): asm_jr[int(fn[5:], 16)] = fn` over glob(asm//nonmatchings/*/*.s) +- **counts:** candidates **4605** / parsed **4601** / real skips **4** +- **evidence:** OVER-APPROX = every .s under asm//nonmatchings/*/ whose text contains a `jtbl_` reference, WHATEVER its symbol name: 4,605 across the 134 overlays. REAL (the `func_[0-9A-Fa-f]{8}` fullmatch): 4,601. GAP = 4, all the same curated symbol: + asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40/listCdBuffer.s (contains jtbl_) + asm/ov_SC02_000/nonmatchings/ov_SC02_000_jr_8017FCB0/listCdBuffer.s + asm/ov_SC02_003/nonmatchings/ov_SC02_003_jr_8017FCB0/listCdBuffer.s + + 1 more. +These are real jr/switch functions at 0x80180000 (config/symbols.us.txt:981) that the isolator is structurally blind to. +SAME ROOT as the jtbl_family_bank finding: a func_-only name filter. Confirms the hypothesis in the audit brief. +- **blast radius:** LATENT, and I measured it rather than asserting harm. An UNBANKED jr contributes no `.rodata` carve, so leaving it un-isolated causes no same-subseg collision and no image shift today — the 4 misses cost nothing right now. The real cost is a CEILING: listCdBuffer can never be given its own region, so it can never be jtbl-banked, in the 4 overlays where it is a switch function. It also compounds the CRITICAL finding: if listCdBuffer were ever banked by hand, it would land in `realc` under a name the roster glob (`banked_func_*.json`) cannot express, joining the orphaned-carve set permanently. +- **R33:** NO on deletion — an overlay's jr set genuinely has to be discovered, and the .s-references-a-jtbl test is the honest discovery (splat emits a .s only for unmatched functions, which is exactly the invariant this scanner should lean on). But the NAME->ADDRESS step is re-derived by string slicing when config/symbols.*.txt already states it. Same fix as the family_bank finding: resolve, do not construct. +- **fix:** Drop the `re.fullmatch(r'func_...')` filter. Take the symbol name from the .s basename and resolve its address through `oss.load_ov_syms(ov)` (which already carries config/symbols.us.txt + symbols.resident.txt + symbols..txt and resolves listCdBuffer -> 0x80180000), falling back to the func_ hex only when the name is a func_ name. Identical one-line fix to the jtbl_family_bank naming path — do both together. +- **assertion (R32):** R32: assert that every .s in asm//nonmatchings/*/ containing `jtbl_` resolves to an address (4605/4605), and sys.exit on any that does not. An unresolvable jr symbol is a bug report, not a silent drop. +- **skeptic:** RAN: (1) Own glob/regex re-derivation of candidates+parsed over config/splat.*.yaml. First pass gave 4610/4606 (I wrongly included resident/us.exe/template); restricted to the 134 ov_ overlays it reproduces the claim EXACTLY: candidates=4605 parsed=4601 GAP=4. Counts CONFIRMED, real_skips=4 correct. (2) Verified evidence at tools/jr_isolate_all.py:78-82 — the func_[0-9A-Fa-f]{8} fullmatch is there as cited. (3) EVIDENCE PATHS ARE FABRICATED: grep -c 'jtbl_' on the three cited files (ov_SC01_077, ov_SC02_000, ov_SC02_003 listCdBuffer.s) returns 0,0,0 — none is a candidate. 103 listCdBuffer.s exist, only 4 contain jtbl_. Real gap set = ov_SC01_084, ov_SC03_108, ov_SC03_118, ov_SC03_119. Right conclusion, wrong exhibits. (4) NOT a justified exclusion: listCdBuffer@0x80180000 (symbols.us.txt:981 verified) is a genuine switch fn (lui %hi(jtbl_801C60DC) / lw / jr $v0). Ran jr_isolate_all.plan() on all 4 gap overlays: obj_of(0x80180000) returns an -O2 object (ov_SCxx_jr_80178D40) in every one, skipped_o0 is empty, and 0x80180000 is ABSENT from planned cuts — so no other path rescues it. The --only path (line 118) applies the same fullmatch and silently drops it too (2nd instance of the blindness). (5) BLAST RADIUS IS LATENT, NOT LIVE: in all 4 gap overlays listCdBuffer is INCLUDE_ASM (unmatched) -> no .rodata carve -> no same-subseg collision and no image shift today. The only overlay banking it (ov_SC03_031) holds a 22-char NON-switch variant with no jtbl. The hazard is conditional: those regions already carry a carve for banked func_80178D40, so banking listCdBuffer would emit two jump tables into one object's .rodata (the over-fill documented at lines 121-124) — but `make build` SHA1 would fail LOUDLY, so it is a blocked ceiling on 4 functions, not silent corruption. (6) The "CRITICAL compounding" (banked_func_*.json roster glob can't express listCdBuffer) is structurally true but unreachable: it requires first landing a bank the byte-gate already rejects. DOWNGRADE to LOW: real bug, count exactly right, but latent, gate-protected, and caps 4/4605 candidates (0.09%). R33 "resolve, don't construct" remains the correct fix. + +### [LOW] `jtbl_carve.py` — CONFIRMED +- **scanner:** all_data_labels():92-100 — `re.match(r"\s*(?:dlabel|glabel)\s+(?:jtbl_|D_)([0-9A-Fa-f]{8})", ln)` over asm//data/*.data.s +- **counts:** candidates **200183** / parsed **200150** / real skips **33** +- **evidence:** OVER-APPROX = every `dlabel|glabel|jlabel|ilabel ` directive in every asm//data/*.data.s, whatever the symbol: 200,183 distinct labels. REAL: 200,150. GAP = 33, all one shape — the curated data-label name that the `(?:jtbl_|D_)` prefix alternation cannot express: + asm/ov_SC01_000/data/tail.data.s:2380: `dlabel listCdBuffer` + asm/ov_SC01_004/data/tail.data.s:1589: `dlabel listCdBuffer` + asm/ov_SC02_004/data/tail.data.s:3958: `dlabel listCdBuffer` + ... 33 overlays, one each. +`labels` is the list from which jtbl_range():122-154 picks a jtbl's END (`nxt = next((a for a in labels if a > start), None)`). A missing label between a jtbl and the next D_/jtbl_ label means the naive end OVER-EXTENDS past it. +- **blast radius:** LATENT ONLY — and I MEASURED it rather than reasoning about it. I recomputed jtbl_range() for ALL 5,043 jtbls in all 134 overlays twice: once with the tool's label set, once with a fully-resolved over-approximating label set (every dlabel/glabel name resolved via config/symbols..txt). ENDS THAT DIFFER: 0 of 5043. Zero live damage, zero latent damage on the current corpus. The reason is structural, not luck: jtbl_range's trailing-`.align`-pad trim (:144-153) recomputes `end = start + n*4` from `jtbl_words()`, whose word scan is bounded by the `enddlabel` directive, and `trimmed <= end` always holds — so the word count DOMINATES the label lookup in every case where the jtbl has words, and I measured `jtbl_words` returning a non-empty list for 5043/5043 jtbls. The label list is effectively dead weight for the END computation; its only surviving role is the `if start not in labels: sys.exit(...)` staleness guard, which only ever sees jtbl_-prefixed names and so never touches the gap. +- **R33:** YES — the END-derivation half of this scanner SHOULD BE DELETED. This is a textbook case: the tool re-parses the data asm's label layout to answer "where does this jump table end", when the jump table's own contents (the `.word` run bounded by `enddlabel`, plus the function's `sltiu` range check the docstring already cites as ground truth) answer it exactly — and the code already computes that answer and already overrides the label-derived one with it. Two independent derivations of the same fact, one of them provably redundant on 5043/5043 cases, and nobody had compared them. Deleting the redundant one removes an entire silent-skip surface for free. +- **fix:** Two options, in preference order. (1) R33: DELETE the end-from-next-label logic. `end = start + len(nonzero_prefix(jtbl_words(ov, jtbl_hex))) * 4` is complete, is what the code already computes, and is what the code already prefers — measured identical on 5043/5043. Keep `all_data_labels` ONLY as the staleness guard (`start in labels`), where its 33-label gap is provably irrelevant. (2) If the label list is kept for the guard, widen the regex to `\s*(?:dlabel|glabel)\s+(\w+)` and resolve non-hex names through config/symbols..txt — one line, removes the class. +- **assertion (R32):** R32: assert `all_data_labels(ov)` count == the count of `^\s*(?:dlabel|glabel)\s+\S+` lines in asm//data/*.data.s (200183/200183, not 200150/200183) — and, more valuably, assert in jtbl_range that the next-label end and the word-count end AGREE, so the day they diverge is a loud failure rather than a silent 4-byte under/over-fill (the same +4 image-shift class as §41d that the docstring already warns about). +- **skeptic:** RAN: (1) .run/audit/skeptic/count.py — re-derived over-approx candidates (every dlabel|glabel|jlabel|ilabel line in all 134 asm/ov_*/data/*.data.s) vs the tool's own regex: candidates=200183, parsed=200150, gap=33, gap symbols = listCdBuffer x33 and nothing else (no jlabel/ilabel exists in data asm). Counts reproduce EXACTLY. (2) Went to the cited lines: asm/ov_SC01_000/data/tail.data.s:2380, ov_SC01_004:1589, ov_SC02_004:3958 are all literally `dlabel listCdBuffer` — a genuine tail-data label at vram 0x80180000 (config/symbols.us.txt:981), sitting between D_8017FFFC and D_80180008. Real skip, NOT a justified exclusion: it is the same kind of boundary the label list exists to capture, just a curated name the (?:jtbl_|D_) alternation can't express. (3) Blast radius measured twice, independently: .run/audit/skeptic/blast.py IMPORTS the real jtbl_carve and calls the real jtbl_range()/jtbl_words() for every jtbl in every overlay with the tool label set vs a fully-resolved label set -> jtbls 5043, naive-end diffs 0, final-end diffs 0, empty word lists 0; a clean-room reimplementation (blast2.py) agrees exactly. The missing 0x80180000 is always immediately preceded by D_8017FFFC, so no jtbl's next-label lookup ever steps over it — even the PRE-trim end never differs. (4) Reachability: all_data_labels has exactly ONE consumer repo-wide (jtbl_carve.py:223 -> jtbl_range:229); the label-derived end is unconditionally overridden by start+n*4 whenever jtbl_words() is non-empty, and I killed both ways that dominance could fail — 0/5043 empty word lists, and .run/audit/skeptic/mixed.py shows 0/5043 jtbls contain symbolic `.word func_…` entries that jtbl_words' 0x-only regex would silently drop. The `start not in labels` staleness guard only ever sees jtbl_-prefixed names, so the gap can never trip it. NET: the claim is fully reproduced — 33 real silent skips, LATENT-ONLY, zero live and zero latent damage, and the severity is a ceiling not a floor (this is a dead-code/redundant-derivation finding, not a correctness one; a case for NONE is defensible). Nothing in the claim was overstated in either direction. + +### [LOW] `tools/exemplar_miner.py` — DOWNGRADED +- **scanner:** the residual filter at tools/exemplar_miner.py:73 — `if ai in reg: continue` where reg = dedup_propagate.registered_addrs() (which parses config/dedup.us.yaml), used as the oracle for 'is this still a residual stub' +- **counts:** candidates **263** / parsed **263** / real skips **3** +- **evidence:** Coverage is COMPLETE in the skip direction — measured: all 263 live func_ stubs of ov_SC01_077 are present in the .run/wall_taxonomy.json census and all 263 receive a routing entry (0 fall through to the LEVER default). The defect is the INVERSE: exemplar_miner emits 655 'unregistered residual stubs' of which 395 (60%) are NOT live stubs at all — they were matched or split away since the Jun-19 census snapshot. Measured: census 915 rows − 260 registered = 655 emitted; intersecting with the live INCLUDE_ASM set leaves 263 real, 392 phantom. The published docs/exemplar_curriculum.md:2 states '835 unregistered residual stubs in ov_SC01_077'. The T6 fuel list — '## WAVE pool — reach-134, ranked' — lists 120 functions, of which 23 are PHANTOM (already banked, no longer stubs). The root cause is the oracle choice: registered_addrs() answers 'is this function SHARED via config/dedup.us.yaml', which is not the same question as 'is this function still an INCLUDE_ASM stub' — a locally-matched, un-shared function is never registered and therefore stays on the residual list forever. +- **blast radius:** corrupt METRICS, LIVE; no lost matches. Inflates the human-facing router doc by 2.5× (655 claimed vs 263 real residuals) and seeds 23 already-done functions into the reach-134 WAVE fuel list that agents pull from. It does NOT corrupt build_fuel_manifest's classify(), which only .get()s the routing map by addr for functions it already believes are stubs — so the 392 phantom entries are inert there. Also note registered_addrs() swallows every exception (`except Exception: return set()`, tools/dedup_propagate.py:77-78): a yaml/import failure would silently make EVERY census row a residual, with no diagnostic. It currently works (1807 addrs) but the failure mode is silent-open. +- **R33:** DELETE the scanner. Yes — this is a textbook case of re-deriving something the proven invariant already answers exactly. 'Is this function still work?' is answered, with zero ambiguity and zero parsing risk, by 'is it still wrapped in INCLUDE_ASM'. exemplar_miner instead asks a proxy question of config/dedup.us.yaml (a share registry) and gets a 60%-wrong answer. The registered_addrs() call should be removed from exemplar_miner entirely, not patched. +- **fix:** Delete the `if ai in reg: continue` line and the dedup_propagate.registered_addrs() dependency. Replace with `if ai not in live_stubs(source): continue` using the shared invariant-derived helper. Additionally, assert the census is not stale: `assert set(live) <= set(census_addrs)`, and warn loudly on `census_addrs - live` (the phantom count) so a stale wall_taxonomy.json is visible rather than silently inflating the pools. +- **assertion (R32):** `live = live_stub_addrs(source); phantom = {x['addr'] for x in rows} - live; assert not phantom, f'{len(phantom)} routed "residuals" are no longer INCLUDE_ASM stubs — census .run/wall_taxonomy.json is stale (snapshot {snap})'` — plus the reciprocal `assert live <= census_addrs` so a NEW stub missing from the census fails the run instead of vanishing from the curriculum. +- **skeptic:** RAN: .run/audit/skeptic/chk2.py+chk3.py (venv python) re-deriving from the real corpus — loaded .run/wall_taxonomy.json (915 rows), dedup_propagate.registered_addrs() (1807), and globbed live INCLUDE_ASM(func_XXXXXXXX) across ALL 14 src/ov_SC01_077/*.c (claimant's dp.overlay_files() misses the _jr_* splits). Also: git rev-list -1 --before=2026-06-20 -> commit:0139, counted INCLUDE_ASM at that commit; read build_fuel_manifest.py:95-160, wall_taxonomy.py:88-91, dedup_propagate.py:64-78. + +COUNTS mostly reproduce: census 915, reg∩census 260, emit-today 655, live stubs 263. But real=260/phantom=395, NOT 263/392: live∩reg = {801803B0,80182988,80183C50} — 3 LIVE stubs the filter wrongly drops (vaddr registered via another overlay's group; overlays share vaddr space). Those 3 are the only genuine silent skips (claim itself said real_skips=0, i.e. self-refuting as a silent-skip finding). + +WHY NOT HIGH: +(1) Root cause misattributed. wall_taxonomy.py:88 builds the census from LIVE INCLUDE_ASM. At commit:0139 (Jun 19 = the census's own snapshot date) there were 900 live stubs — the census was ACCURATE when generated. The 395 phantoms are functions matched Jun19→Jul14. Regenerating the census kills them regardless of the oracle. Worse: R33's prescribed 'delete registered_addrs()' against the current stale census raises emission 655→915. +(2) The '2.5x inflated doc' is artifact rot, not live tool output. docs/exemplar_curriculum.md: mtime Jun 20 23:54, single commit commit:0167, never regenerated; its 835 was ~right vs the 900 stubs live that day, and line 2 prints 'snapshot 2026-06-19'. +(3) Cited fuel-list evidence is fabricated/miscounted: the '## WAVE pool — reach-134' table has 60 rows (--top 60), not 120, and 28 of them are phantom, not 23 of 120. +(4) Blast radius inert: .run/exemplar_routing.json is gitignored scratch; its only consumer build_fuel_manifest.py:132-152 iterates live_stubs(src) and .get()s routing by addr with lever default 'WAVE' — all 572 phantom entries never render, and all 263 live stubs do get an entry. Nothing is currently steered wrong. +(5) dedup_propagate.py:77-78 bare 'except Exception: return set()' exists as cited but is silent-OPEN: it can only add phantoms, never lose work. Latent, LOW. + +Survives as LOW: the 3-stub over-filter + the oracle's blindness to locally-matched-unshared fns (only bites if a stale census is reused). SIDE FINDING (different tool, real): wall_taxonomy.py censuses only src/ov_SC01_077/ov_SC01_077.c — a fresh census today would see 13 stubs and miss the 250 in the _jr_*/_after/_a/_o0 splits. + +### [LOW] `tools/masked_diff.py (+ tools/match_one.py, tools/masked_scorer.py)` — DOWNGRADED +- **scanner:** mask_for() — tools/masked_diff.py:55-56: `if (word >> 26) in (2, 3): return 0`. Returns a ZERO mask for j/jal — masking the WHOLE 32-bit word, INCLUDING the 6-bit opcode. +- **counts:** candidates **335807** / parsed **0** / real skips **900** +- **evidence:** The docstring (masked_diff.py:10) says the rule is "ignore the whole 26-bit target field". The code ignores all 32 bits. A zero mask means `(c.word & 0) != (t.word & 0)` is `0 != 0` = False — the slot compares EQUAL TO ANYTHING. The opcode is invisible, so `j` (op 2) vs `jal` (op 3) compare equal, and a jal in my object compares equal to ANY instruction in the target. + +EXPOSURE, measured over the real stub corpus: 335,807 of 4,343,421 target instructions (7.73%) sit in a zero-mask slot and are entirely invisible to the oracle. + +REPLAY, measured: I replayed all 1,589 historical match_one objects (.run/match/*/t.o) that have an unambiguous target .s, comparing the current mask against a tight mask (0xFC000000 — keep the opcode, drop the 26-bit target): + instruction slots where the CURRENT mask HID A REAL DIFFERENCE: 900, across 534 functions + match_one says MATCH (current mask): 182 + match_one says MATCH (tight mask) : 182 + FALSE MATCHES: 0 +- **blast radius:** CORRUPT METRICS — LATENT for the verdict, LIVE for the score. I want to be precise and NOT over-claim: the mask genuinely hid 900 real instruction differences across 534 of the 1,589 replayed drafts, so every closeness score those agents were handed was UNDERSTATED and candidate ranking was corrupted by up to 900 slots. But in all 534 cases other unmasked diffs remained, so the verdict never flipped: ZERO false MATCH claims were produced on the historical corpus. The gun is loaded and pointed but has not fired. It would fire the moment a draft's ONLY remaining difference is a j/jal slot — at which point match_one prints MATCH, the agent banks it, and the whole-binary byte-gate rejects it (and per the brief, that rejection gets misbooked as a codegen wall). diff_object_object (the permuter) is largely PROTECTED from this by its extra reloc-operand equality check at masked slots (masked_diff.py:121-123); diff_object_s (match_one) has NO such check and is fully exposed. +- **R33:** KEEP the scanner, but this defect is pure self-inflicted re-derivation: a hand-written mask table drifted from its own docstring and nobody diffed the two. The invariant test above would not have caught THIS one (it produces no false positives on byte-identical input — that is exactly why it survived), so it needs the structural assertion (mask != 0) rather than the corpus oracle. Worth stating plainly: this is the one finding the invariant CANNOT police, which is precisely why it needs an explicit R32 guard. +- **fix:** mask_for(): return 0xFC000000 instead of 0 for both the `(word>>26) in (2,3)` case and the `reloc_kind == "26"` case — keep the opcode, drop only the 26-bit target field, which is what the docstring already promises. VERIFIED COUNTERFACTUAL: zero new lies across all 60,740 invariant-corpus functions, i.e. the tighter mask is provably free. +- **assertion (R32):** R32: assert that no mask returned by mask_for() is ever 0 — an all-zero mask makes an instruction slot unconditionally equal and is never a legitimate compare. Plus the same corpus-wide invariant assertion as above (diff must be 0 on all 60,740 stubs), which pins the tightened mask against regression. Additionally assert that mask_for's masked-out bits are exactly the documented field width (26-bit target / 16-bit immediate), so code and docstring can never silently diverge again. +- **skeptic:** RAN: (a) re-derived exposure with the tool's own insns_from_s over all 60,740 asm/**/nonmatchings/**/*.s -> 4,343,434 target ins, 335,807 with opcode in {2,3} = 7.73% (j=100,090, jal=235,717) — reproduces the claim exactly. (b) Re-ran the replay myself over .run/match/*/t.o: strict unambiguous-stub rule gave 1,315 objs / 666 hidden / 0 false matches; dedup'ing multi-path fns by identical instruction content reproduces their corpus to the digit — used=1589, hidden=900 slots across 534 fns, MATCH(current)=182, MATCH(tight 0xFC000000)=182, FALSE MATCHES=0. Tight mask causes zero regressions, so the fix is safe. (c) Evidence verified at file:line: masked_diff.py:55-56 is verbatim `if (word >> 26) in (2,3): return 0`; docstring:10 says "26-bit target field"; reloc guard at :121-123. Mechanism and every count CONFIRMED; the defect is real and not a justified exclusion (masking the opcode is never justified). + +DOWNGRADE grounds: (1) The "LIVE for the score / candidate ranking corrupted by up to 900 slots" half is refuted — match_one.py and rtu_match.py do NO ranking (single-draft verdict + diff list); the only ranking consumer is masked_scorer.py -> diff_object_object, which the claim concedes is guarded. (2) Category breakdown of the 900: 648 are "my jal/j vs a completely unrelated target instruction" in drafts already wildly misaligned (median draft length 11 ins, median 31 STILL-VISIBLE diffs); only 18 are the semantically meaningful j<->jal swap. Per-function understatement is a median of 1 slot = 3.8% of the true diff count. (3) The verdict flip is latent AND not close: min current-diff among the 534 affected fns is 4 — the gun never came within 4 visible diffs of firing. (4) Even if it fires, the whole-binary byte-gate (G3/P9, harvest_verify) is the sole arbiter and rejects it; worst case is wasted work, never a corrupted binary. + +Two holes the claim MISSED, neither closed by its proposed fix (synthetic probe with real instruction shapes from .run/match/func_800CEFD0/t.o): local `j` carries R_MIPS_26 against `.text` with the destination baked into the word, so diff_object_object([j .text+0x44],[j .text+0x130]) == 0 — two DIFFERENT jump destinations score identical, and j-vs-jal with the same reloc_op also scores 0. So masked_diff.py:109's "0 <=> same linked bytes" is FALSE and the permuter is NOT "largely protected". Separately, a jal to the WRONG symbol is invisible in diff_object_s under the current mask AND under the proposed 0xFC000000 mask — the scariest banking scenario is neither caused nor fixed by this finding. + +### [LOW] `tools/masked_diff.py (used by tools/match_one.py)` — DOWNGRADED +- **scanner:** insns_from_object() fn-label fallback — tools/masked_diff.py:87-89: `# fn given but nothing matched (unlabeled .text) -> fall back to the whole section` / `if fn is not None and not insns: return insns_from_object(obj, None)` +- **counts:** candidates **6180** / parsed **6163** / real skips **3** +- **evidence:** OVER-APPROXIMATING DETECTOR: for each of the 6,180 historical match_one objects (.run/match//t.o), I objdump'd it and asked whether a `` header actually exists. 6,163 resolve correctly. In 17 the requested symbol is ABSENT, so the fallback silently re-parses the WHOLE .text and match_one compares whatever it finds against the requested function's target .s. Real examples: + .run/match/func_8016A8FC/t.o want=func_8016A8FC got=['func_8013C98C'] + .run/match/func_80175820/t.o want=func_80175820 got=['func_8013C98C'] + .run/match/func_8012A094/t.o want=func_8012A094 got=['test'] + .run/match/exp_80155800/t.o want=exp_80155800 got=['func_exp_80155800'] +In each of these match_one silently diffed a DIFFERENT FUNCTION'S CODE against the requested target's .s and printed a confident per-instruction DIFF table. Note also: the fallback's own stated justification ("unlabeled .text") does not occur ANYWHERE in the 6,180-object corpus — every one of the 17 is a symbol-name mismatch, i.e. the fallback has never once done the job it was written for. +- **blast radius:** CORRUPT METRICS / misdirection — LIVE but small: 17 of 6,180 historical runs (0.3%), of which at least 3 (func_8016A8FC, func_80175820, func_8012A094) demonstrably compared the wrong function's bytes. The agent-visible symptom is a large, plausible-looking DIFF table, which reads as "my draft is far from matching" (grind at a codegen wall) when the truth is "your C file defines the wrong function name." It cannot produce a false MATCH in practice (an unrelated function will not accidentally be byte-equal), so this corrupts DIAGNOSIS, not the bank. The `mine` empty-guard at match_one.py:74 is defeated by the fallback: it only fires when .text is totally empty (1 of the 17, func_801330E0). +- **R33:** DELETE THE FALLBACK — this is the audit's cleanest "best outcome is a deleted scanner" case in my group. The fallback is a GUESS standing in for an ASSERTION. The tool already knows the ground truth (it asked for a specific symbol); when reality contradicts it, it silently substitutes different data instead of failing. That is the silent-skip pattern in its purest form. Removing it strictly increases correctness and removes code. +- **fix:** Delete the fallback (masked_diff.py:88-89). Replace with a hard error: `raise KeyError(f"symbol {fn} not found in {obj} (found: {sorted(seen)})")`. match_one should print that list — it turns a silent wrong-function diff into an instantly actionable "you named the function wrong." The fallback protects against a case that provably never happens in 6,180 real runs. +- **assertion (R32):** R32: assert the requested fn is present among the object's parsed headers; on failure list what WAS found. And a corpus assertion: over .run/match/*/t.o, assert 100% of directory names resolve to a same-named symbol in their own t.o — this exact check is what surfaced the 17. +- **skeptic:** RAN: (1) objdump -drz -j .text over all 6,180 .run/match/*/t.o asking whether a header matching the dir name exists (.run/audit/skeptic/scan.py); (2) re-implemented match_one's compare via masked_diff.insns_from_object/insns_from_s/diff_object_s against the real target .s for every gap item that still has one (.run/audit/skeptic/sim.py); (3) find asm -name ".s" per gap item; probed insns_from_s on a missing path; traced provenance in src/ and config/dedup.us.yaml. + +COUNTS REPRODUCE EXACTLY: candidates=6180, parsed=6163, gap=17. Cited code at masked_diff.py:87-89 exists verbatim; match_one.py:71 is the reachable caller (rtu_match.py defaults to .run/crack3/rtu, so .run/match is genuinely match_one's corpus). + +MECHANISM IS REAL — reproduced 1 live instance: .run/match/func_80175820/t.o contains only func_8013C98C (14 ins); the fallback parses it and against the real 55-ins target asm/ov_SC01_000/nonmatchings/ov_SC01_000_jr_801734BC/func_80175820.s yields 54 masked mismatches printed as a confident DIFF table. + +BUT 14 OF THE 17 ARE NOT SILENT SKIPS: (a) 3 are LOUD, not silent (func_801330E0, func_8017C974, func_80183324): .text is empty, mine==[], the `if not mine` guard at match_one.py:74 fires -> "FAIL: my object has no function", exit 1. Claim conceded 1; it is 3. (b) 10 are scratch compiler probes, not matching runs (_probe, _probe2, _probe3, _probe4, _probe_unused, probe_unused, dbg, dump, insp, tf). None has a .s ANYWHERE in asm/, so insns_from_s -- a bare open() -- raises an uncaught FileNotFoundError (verified by running it). Loud traceback; NO diff table is ever printed. Justified exclusions. (c) exp_80155800: no .s (loud crash) AND the substituted symbol is func_exp_80155800 -- the SAME function with a name prefix, not "a DIFFERENT FUNCTION'S CODE." The claim's own example refutes its characterization. + +3 OF THE 4 CITED "REAL EXAMPLES" DO NOT SURVIVE: func_8016A8FC, func_8012A094, exp_80155800 all have ZERO .s files today. The first two are BANKED (src/ov_SC03_099/ov_SC03_099_jr_8015AE2C.c:5142 defines func_8016A8FC; config/dedup.us.yaml has DEFINE_func_8012A094), so their .s plausibly existed at run time -- I grant those 2. exp_80155800 I do not. + +CORRECTED real_skips = 3 (1 confirmed live + 2 plausible-historical), not 17 -> 0.05% of corpus, not 0.3%. + +SEVERITY MEDIUM -> LOW: the "CORRUPT METRICS" framing is wrong -- nothing aggregates match_one's output; damage is one misleading interactive diff table per occurrence, trigger is an authoring error the agent discovers immediately, and it cannot reach the bank (whole-binary SHA gate G3/P9 is the arbiter). One correction AGAINST the claimant: they assert a false MATCH is impossible -- it is not. A correct body under a wrong symbol name (exactly the func_exp_80155800 shape) would print MATCH while the real build links the wrong symbol; they missed the fallback's sharpest edge. + +The R33 fix (delete lines 87-89) is still correct and cheap: it removes code and converts a guess into the assertion match_one.py:74 already has grounds to make. The finding was oversold ~6x on count and one severity tier, but it is not a false finding. + +### [LOW] `tools/masked_diff.py (used by tools/match_one.py)` — DOWNGRADED +- **scanner:** insns_from_s() — tools/masked_diff.py:93-103. Parses every `/* off vaddr LEHEX */ mnem` line in the .s with no notion of the function-boundary markers (`endlabel`, `alabel`, `dlabel`) that splat emits. +- **counts:** candidates **4343434** / parsed **4343434** / real skips **2** +- **evidence:** The LINE parser itself is at FULL COVERAGE (see verified_clean — 4,343,434/4,343,434, with all 410,534 unparsed lines exhaustively classified as labels/directives/comments). The defect is at the FUNCTION-BOUNDARY level: insns_from_s reads the WHOLE .s file, including inline DATA words that follow an `alabel`/`dlabel` marker, and counts them as instructions. insns_from_object, by contrast, correctly stops at the next objdump header. The two therefore disagree on length, producing a permanent phantom length-delta. Surfaced by the invariant test (build obj bytes == target .s bytes => diff MUST be 0) as the 4 survivors after the PC16 fix: + build/src/apicard4.o func_80062808 mine=26 tgt=32 (.s has `alabel D_80062870` + 6 trailing data words) + build/src/800c3.o func_8005CE38 mine=2 tgt=6 (.s has `dlabel func_8005CE38` — it is DATA, not code) + build/src/800b_6.o func_8004D16C mine=26 tgt=28 (`alabel D_8004D1D4`) + build/src/libgte1.o func_80047CAC mine=1 tgt=2 (`dlabel func_80047CAC`) +The boundary markers are rare and fully enumerated across the whole 60,740-file corpus: 3 x alabel, 2 x dlabel, 2 x enddlabel. +- **blast radius:** CORRUPT METRICS — LIVE but tiny and precisely bounded: exactly 4 functions, measured, out of 60,740. Each has a permanent, unfixable phantom length-delta, so match_one can never report MATCH for them no matter what an agent writes. All 4 are hand-written/data blobs (apicard4, 800c3, 800b_6, libgte1 — PsyQ/library territory), not live overlay decomp targets, so nobody is currently grinding against them. Reporting it because it is a REAL silent skip and because it is the residual that the invariant assertion will otherwise trip on forever: without fixing this, the R32 assertion proposed for finding #1 cannot be turned on at zero-tolerance. +- **R33:** KEEP the parser (target instruction bytes must come from somewhere), but STOP RE-DERIVING THE LENGTH. The .s file DECLARES its own function size on line 1 (`nonmatching func_8017F5B4, 0x...`), and the ELF symbol table in the build object declares it too. The tool re-derives length by counting lines it parsed, which is exactly the metric that a parse hole silently corrupts. Derive the length from the declared size and assert the parse against it — then a future parse hole becomes a LOUD assertion failure instead of a silent phantom diff. +- **fix:** Stop parsing at the function-boundary markers: in insns_from_s, break on `endlabel`, and stop collecting instructions once an `alabel`/`dlabel` line is seen (those introduce DATA, not code). Alternatively, take the authoritative length from the `nonmatching , 0x` header line that every one of the 60,740 .s files carries (it is the function's true byte size) and assert len(insns)*4 == that size — which is a far better idea than parsing at all. +- **assertion (R32):** R32: every nonmatchings .s begins with `nonmatching , 0x`. Assert `len(insns_from_s(s)) * 4 == SIZE` for ALL 60,740 files. That is a free, total, self-validating coverage check on insns_from_s that the corpus already hands you — it would have caught these 4 instantly and pins the parser against every future format change. Nobody ever compared the parsed count to the size the file literally declares. +- **skeptic:** RAN: (1) .run/audit/skeptic/count.py over all 60,740 asm/**/nonmatchings/**/*.s -> tool_parsed=4,343,434, EXACTLY the claimed number; total lines 4,814,708; only /*-leading unparsed lines are 1,194 x "/* Handwritten function */" (a comment, correctly ignored). Line-coverage claim holds. (2) Marker grep: my first pass showed 202,170 dlabel (5 orders of magnitude off) and I thought I had killed it -- but I had scanned all of asm/ including asm/*/data/*.data.s where dlabel is the normal data-section wrapper. Re-scoped to the corpus insns_from_s ACTUALLY consumes (nonmatchings/ only): exactly 3 alabel, 2 dlabel, 2 enddlabel, 60,738 endlabel, and precisely the 4 named files. The claim's enumeration was right; my grep was the error. (3) Read tools/masked_diff.py:93-103 -- insns_from_s does read the whole file with no boundary logic. Evidence exists at the cited lines. (4) .run/audit/skeptic/repro.py ran the real tool against the real build objects: all 4 deltas reproduce exactly (26/32, 2/6, 26/28, 1/2). For the two alabel cases body_mismatch=0 -- the real function bytes are PERFECT yet the tool reports diff=6 and diff=2. The .s declares 0x68 = 104 bytes = 26 instructions == mine, independently proving tgt=32 is the tool's own error. (5) Reachability: match_one.py:89 `if not diffs and len(mine)==len(tgt)` hard-blocks MATCH on any length delta; all 4 are INCLUDE_ASM (still undecompiled, open targets) and grinder.py:64 asm_subdir_for() derives the subdir generically with no exclusion. Path is real. + +DOWNGRADE (count 4 -> 2): the two dlabel entries (func_8005CE38, func_80047CAC) are NOT function targets. They are DATA blobs -- .word runs with "/* invalid instruction */" wrapped in dlabel/enddlabel. No C draft can ever be written for a data blob, so there is no MATCH to falsely deny; the bug cannot harm them. They are moot, not silent skips. Confirmed by a clean corpus self-partition: 60,738 files have BOTH endlabel AND a declared size (real functions); exactly 2 have NEITHER -- and they are precisely those 2 dlabel blobs. Also no .s has >1 endlabel, so there is no larger multi-function-per-file variant; my attempt to expand the finding failed. + +Severity stays LOW: it is a real, reachable false-NEGATIVE on 2 undecompiled functions (2 of 60,738 = 0.003%), in library/PsyQ TUs nobody is currently grinding (Phase 26 is overlay work). Direction of failure matters -- it yields a false NON-match, never a false MATCH, so it cannot bank a wrong function or turn the byte-gate falsely green; "CORRUPT METRICS" overstates it. Two points that STRENGTHEN the proposed R33 fix: the declared-size discriminator is implementable at zero tolerance across 60,738/60,740, with the 2 data blobs excluded by the principled absence of a size declaration (not a special case) -- so the R32 zero-tolerance assertion CAN be turned on. + +### [LOW] `canon_draft_decls.py` — DOWNGRADED +- **scanner:** tools/canon_draft_decls.py:86-87 `stub_re = re.compile(r'INCLUDE_ASM\([^,]*,\s*(func_[0-9A-Fa-f]+)\)')` + `cur_stubs = set(stub_re.findall(open(c_path).read()))` where c_path = `src/{overlay}/{overlay}.c` (:60), and the silent drop at :92 `if fn not in cur_stubs: continue` +- **counts:** candidates **58717** / parsed **1975** / real skips **250** +- **evidence:** c_path is hardcoded to the single file `src//.c`. But an overlay's source is SPLIT across ~12 .c files (src/ov_SC01_077/ has 14: .c, _a.c, _after.c, _o0.c, _o0b.c and the Phase-26 _jr_.c splits). MEASURED on ov_SC01_077: cur_stubs = 13 stubs. TRUE stub set across all 14 .c = 264. FLEET-WIDE: 58,717 INCLUDE_ASM stubs exist in src/ov_*/*.c; only 1,975 live in .c. The tool sees 3.4% of its own corpus. + +The consequence is not a bad rewrite — it is a SILENT DROP. Every draft whose function is stubbed in a split file hits `if fn not in cur_stubs: continue` and is never written to --out at all. + +MEASURED against the REAL draft corpus (every .run/drafts* dir, 10,731 drafts): + kept by the shipped filter : 62 + SILENTLY DROPPED : 10,669 +On the most recent wave dir .run/drafts-wave-cn (610 drafts): shipped keeps 6; a corrected all-.c filter keeps 166 still-stub drafts. + .run/drafts-full : 341 drafts -> 3 kept (corrected: 97) + .run/drafts-p16 : 393 drafts -> 3 kept (corrected: 123) + .run/drafts-recov : 505 drafts -> 2 kept (corrected: 106) + +Second hole, same file: the func_-only stub_re cannot see curated names. `listCdBuffer` appears as an INCLUDE_ASM stub in 100 src files fleet-wide and is invisible to it. Third hole: the canonical DATA decl corpus (DATA_DECL_RE at :36, harvested from [c_path, engine_core.h]) yields 174 decls from .c alone vs 1,021 available across all src/ov_SC01_077/*.c — so even the 13 drafts it DOES process are canonicalized against 17% of the available truth. +- **blast radius:** LOST MATCHES, LIVE. This is the tool that exists specifically to fix the dominant gate-failure (declaration conflicts in the one-big-TU build). It processes 6 of 166 eligible drafts on the current wave and throws away 10,669 of 10,731 drafts across the whole draft corpus. Every dropped draft goes to the byte-gate with UN-canonicalized externs and fails with `conflicting types` — which reads as a matching failure when it is a plumbing failure. This is the measured mechanism behind the standing project finding that 'matching is solved; integration is the bottleneck': the integration tool has been running at 0.6% coverage. +- **R33:** PARTIAL DELETE. The decl-rewriting step is real work and should stay. But the CORPUS RE-PARSE — 'go re-read C source text to discover what the canonical declaration is' — should be deleted and replaced by a derivation from what the TU already compiles. tools/reconcile_tu.py (Phase 26, 'ask what can THIS TU see') already answers exactly this question from the build, where a parse hole makes you CONSERVATIVE rather than WRONG. canon_draft_decls should call that, not re-implement a C-declaration scanner against a hand-picked subset of one overlay's files. +- **fix:** Three edits. (1) Replace the single c_path with `sorted(glob.glob(f'src/{ov}/*.c'))` everywhere it is used — cur_stubs, collect_inline_sigs, collect_extern_sigs, collect_data_decls. (2) Make stub_re name-agnostic: `INCLUDE_ASM\(\s*"[^"]*"\s*,\s*([A-Za-z_]\w*)\s*\)`, and make sym_of() accept any identifier, not just func_/D_. (3) Turn the `continue` at :92 into an explicit counted+logged skip so a drop can never again be silent. +- **assertion (R32):** assert len(cur_stubs) == the count of INCLUDE_ASM occurrences across ALL src//*.c (not just .c) — a mismatch aborts. And: every input draft must exit as either CANONICALIZED or SKIPPED-WITH-REASON; assert kept + skipped_with_reason == len(input_drafts), and print the skip histogram. A tool that consumes 10,731 files and emits 62 must be structurally incapable of doing so quietly. +- **skeptic:** RAN: (1) grep -rhoE INCLUDE_ASM regex over src/ov_*/*.c => 58,617 fleet stubs (claim said 58,717) and exactly 1,975 in /.c — parsed count CONFIRMED. ov_SC01_077: cur_stubs=13 vs 263 across all 14 .c — CONFIRMED. Sub-claims confirmed too: listCdBuffer is an INCLUDE_ASM stub in exactly 100 src files; DATA decls 174 (.c) vs 1,021 (all *.c). The code shape is real. BUT the cited file:lines are all wrong — stub_re/cur_stubs are at :83-84 not :86-87, the `continue` is at :88 not :92, c_path at :64 not :60, DATA_DECL_RE at :30 not :36. + +WHAT KILLS THE SEVERITY: +(a) `git ls-tree --name-only commit:0110 src/ov_SC01_077/` — the commit that CREATED AND RAN canon_draft_decls (2026-06-17) — returns exactly ONE file: ov_SC01_077.c. The hardcoded src//.c path was CORRECT AND COMPLETE at authorship. Per-file `git log --diff-filter=A`: _a.c/_o0.c arrived 2026-06-20 (Phase 19), _after.c 2026-07-07, all ten _jr_*.c on 2026-07-13/14 — days-to-weeks AFTER. +(b) Its real runs achieved ~100% coverage, not 0.6%. On-disk in/out pairs (tool mtime 00:15:13, output mtime 00:15:26 — 13s apart): drafts-T6-fail 145 in -> drafts-T6-recanon 144 out; drafts-recov 505 in -> drafts-recov-canon 505 out. Phase15.md confirms these are the only real runs (+19 recovered; 47 verified from the 505). +(c) ZERO CALLERS. No Makefile/script/.py/.js invokes it; every grep hit is a docstring or doc mention. Superseded TWICE: sig_unify.py (2026-06-25) says it "extends canon_draft_decls" and ADDED `--src-file` (:160-161) — the project already fixed the split-file problem in the successor — and canon_sig_reconcile.py (2026-07-10) added `--tu`, which preprocesses the real target TU for file-scope ambient. That IS the "derive from what the TU compiles" remedy the claim proposes as NEW work; it already exists and is live. Live chain is canon_resident_calls -> cast_call_sites -> sig_unify. +(d) The "10,669 silently dropped / 0.6% coverage" is SYNTHETIC: it runs today's filter over ~168 historical .run/drafts* dirs the tool was never pointed at, including OUTPUT dirs (.run/drafts-wave has 24 files but drafts-wave-cn has 610 and contains `_b.c`; 128 drafts corpus-wide aren't even func_XXXX.c-named and can never match any stub set), plus thousands of NOW-BANKED functions the filter is CORRECTLY supposed to skip (`# already banked elsewhere; skip`). My recount over all 10,731 drafts: shipped keeps 62, corrected all-.c filter keeps 2,523 — delta 2,461, not 10,669. The claim inflates the drop ~4.3x by counting justified exclusions as bugs. +(e) candidates=58,717 is a category error: the tool is --overlay-scoped and never scans the fleet. Its true per-run corpus is ONE overlay = 263 candidates, 13 parsed, 250 latent skips (my corrected_real_skips). + +VERDICT: real defect shape, but LATENT in DEAD, SUPERSEDED code — nothing calls it and nothing depends on it. "LOST MATCHES, LIVE" and "the integration tool has been running at 0.6% coverage" are FALSE: it has not been running at all since 2026-06-17. Correct R33 disposition is stronger than the claim's PARTIAL DELETE — since sig_unify/canon_sig_reconcile already cover it, this is a FULL DELETE candidate, not a fix-and-rewire. + +### [LOW] `difficulty.py` — DOWNGRADED +- **scanner:** tools/difficulty.py analyze() — `if mn == 'jal': ncalls += 1` and `leaf=(ncalls == 0)` +- **counts:** candidates **60738** / parsed **54802** / real skips **5936** +- **evidence:** ncalls counts only `jal`. `jalr` — the register-indirect CALL — is never counted, so a function that calls exclusively through function pointers scores ncalls=0 and is labelled leaf=Y. MEASURED over the whole fleet asm corpus: 5,936 of 60,738 functions contain a `jalr` and no `jal`, and every one of them is currently reported as a leaf. This matters more here than in most codebases: sig_image's own docstring records that 'overlays dispatch most code via function-pointer tables (not jal)', so indirect dispatch is the norm, not an edge case. (1,234 jalr instructions in the 6,000-file sample alone.) +- **blast radius:** Corrupt SELECTION, LIVE. docs/difficulty.md prints 'non-jtbl leaves : N (best harvest targets)' as its headline recommendation, and that number is inflated by up to 5,936 functions that are not leaves at all. The score (nins + 3*branches + 25*jtbl + 2*ncalls) also under-ranks them, so they sort EASIER than they are and get pulled to the front of the harvest queue. Nothing is lost from the queue — this mis-ranks it rather than truncating it, which is why it is MEDIUM and not HIGH. +- **R33:** KEEP. Same reasoning as the NON_MATCHING finding: this scores work that has not been attempted, so no proven invariant can supply it — the byte-gate cannot rank a function it has never seen compiled. The regex is fine; the opcode table is just incomplete. This is one of the few scanners in my group that legitimately must parse text. +- **fix:** Count `jalr` in ncalls (it is a call), or add a distinct `indirect` column and exclude jalr-callers from the 'best harvest targets' leaf count. The latter is more honest — an indirect call is a different (harder) matching problem than a direct one, not the same one. +- **assertion (R32):** assert that no function reported with leaf=Y contains a `jalr` in its .s. This is a one-line invariant over the same corpus the tool already reads, and it fails loudly on 5,936 functions today. +- **skeptic:** RAN: (1) imported tools/difficulty.py and drove unmatched_stubs()/analyze() over all 136 configured binaries -> 60,733 INCLUDE_ASM stubs, 60,731 analyzed (claim's candidates=60,738 is right). (2) Re-parsed every stub's .s with the tool's OWN INSTR regex counting jal vs jalr -> jalr-only (jalr>=1, jal==0) = 5,936, EXACTLY the claimed count; asserted r['leaf'] is True on all 5,936 and it passed, so the tool really does label every one a leaf. (3) Confirmed the gap is NOT a justified exclusion: `jalr $v0` encodes 09F84000 = 0x0040F809 = `jalr $ra,$v0` (rd=$31, it links), and func_80034DFC does `sw $ra,0x20($sp)` — a true leaf never saves $ra. Evidence exists at the cited lines: tools/difficulty.py:333 `if mn == 'jal': ncalls += 1`, :339 `leaf=(ncalls == 0)`. + +WHAT KILLS THE SEVERITY: (a) "parsed=54,802 / real_skips=5,936" is a FALSE FRAMING. The tool parses, scores, ranks and emits ALL 60,731 functions to the CSV and the queue; ZERO are skipped, dropped or truncated. Actual silent skips = 0. 5,936 is a count of rows with a wrong FIELD, not of work never attempted — this is a mislabel finding wearing a silent-skip costume. (b) The headline blast-radius claim ("score under-ranks them, they sort EASIER and get pulled to the front of the harvest queue") is REFUTED BY MEASUREMENT: I re-sorted every binary with score+2*njalr and diffed top-120 membership against the current sort -> only 87 functions swapped across ALL 136 binaries combined (0.6 per 120-row queue; 61 of 136 binaries have zero churn). The 2*ncalls term is negligible against nins. In the live committed docs/difficulty.md only 3 of 120 queue rows print a wrong leaf=Y. The queue is NOT meaningfully mis-ranked. The claimer's own stated rule was "MEDIUM because it mis-ranks the queue" — I measured the mis-ranking and it is ~nil, which falls below their own bar. + +WHAT SURVIVES (real + LIVE, but small): the summary statistic only. `make report` (Makefile:150) regenerates it; docs/difficulty.md is tracked and clean vs HEAD; it prints `non-jtbl leaves : 1115 (best harvest targets)` where the true value is 1,016 (9.7% inflated for main; fleet-wide 14,062 -> 8,127, i.e. 42%). Not latent — PROJECT_CONTEXT.md:234, docs/gen2-roadmap.md:77 ("harvest cheap non-jtbl game-code leaves") and matching-cookbook:1000 all name this list as a selector. But `leaf` is advisory: the drafter reads the .s (which shows the jalr) and the byte-gate is the oracle, so no work is lost and any bad pick self-corrects. Net: a one-line opcode-table fix (`mn in ('jal','jalr')`) worth doing, but it corrupts an advisory count, not the selection. LOW, not MEDIUM. + +## REFUTED by the skeptics (do NOT act on these) + +- **`tools/family_manifest.py`** — `stub077 = re.findall(r'INCLUDE_ASM\([^)]*,\s*func_([0-9A-Fa-f]+)\)', src077)` (tools/family_manifest.py:34) — the func_-only regex applied to ov_SC01_077, feeding BOTH matched_hex (line 36) and the ov077_ex draftable-exemplar picker (lines 64-69) + - killed: RAN: (1) Re-derived counts under the tool's own glob src/ov_SC01_077/*.c — 264 INCLUDE_ASM, 263 parsed by the func_-only regex, 1 unparsed (`listCdBuffer` @ src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:5549). Cited evidence exists. (2) Differential replication of family_manifest.py lines 20-69 in scratch, buggy regex vs corrected regex (resolving listCdBuffer -> 0x80180000 from the .s glabel): matched_hex 2252 vs 2252, families 44087 vs 44087, instances 110888 vs 110888, exemplars 260 vs 260 — ALL IDENTICAL. The claimant's own proposed fix changes nothing. (3) KILL SHOT — `listCdBuffer` is NOT a function; it is the tail fragment of func_8017FD14, split by a splat symbol collision (listCdBuffer is a DATA symbol at 0x80180000 in the main binary, src/800.c:686 `extern s32 listCdBuffer;`, whose address lands inside overlay code vram). Three proofs: (a) the split cuts through a %hi/%lo pair — func_8017FD14.s ends on a dangling `lui $at, (0x80190000 >> 16)`, listCdBuffer.s opens on its partner `addu $at,$at,$s1` / `lbu $v0,-0x5611($at)`; (b) listCdBuffer.s has NO prologue but HAS the epilogue `addiu $sp,$sp,0x80; jr $ra`, tearing down a frame it never set up; (c) 0x2EC/4=187 + 0xF8/4=62 = 249 = exactly the nins of sig077's single record at 0x8017fd14 spanning 0x8017fd14->0x801800f8. CONSEQUENCES: The claim's central arming mechanism is FABRICATED. There is no "sig hole" at 0x80180000 — sig_image.py bootstraps boundaries by return-detection from the raw image, found the true return at 0x801800F4, and CORRECTLY signed the whole 249-insn body as one function. 0x80180000 is mid-function; it cannot become a sig entry by "fixing the sig corpus," so the claim's one falsifiable prediction ("fixing the sig corpus WITHOUT fixing this regex would arm it") is refuted, and the "one bug masking the other" narrative is invented. The skip is inert BY CONSTRUCTION, not by luck: both consumers (matched_hex line 36, ov077_ex lines 64-69) iterate sig077.items() only, and I verified sig077-keys ∩ non-func_-stubs = empty set. The tool gets the RIGHT answer anyway: 0x8017fd14 IS in stub077 (verified True), so the merged 249-insn record covering both halves is correctly classified UNMATCHED. Finally, the R33 remedy is self-undermining: family_hseq.load() — the "correct" loader the claim says to delegate to — uses the BYTE-IDENTICAL regex at tools/family_hseq.py:38, so delegating changes zero behavior. The gap item is a JUSTIFIED EXCLUSION, not a silent skip: real_skips=0, severity NONE. (Unrelated residuals, not this finding: 3 copies of the regex is a mild DRY nit; and if func_8017FD14 is ever banked, the merged sig record flips to "matched" while listCdBuffer.s remains an INCLUDE_ASM stub — but that is a sig-MERGE property that occurs with a perfectly correct regex.) + +- **`tools/build_fuel_manifest.py`** — FUNC_RE at tools/build_fuel_manifest.py:29 — `^func_[0-9A-Fa-f]{6,8}$`, gating live_stubs() at :87-90; and addr_int() at :37-39 + - killed: RAN: (1) Re-derived both universes with STUB_RE/FUNC_RE. live_stubs() opens ONLY 3 files (ov_SC01_077.c/_a.c/_o0.c) -> candidates=30, parsed=30, FUNC_RE skips=0. The claimed 264/263/1 only reproduces over all 14 overlay src files, a counterfactual that exists only if the SEPARATE F1 bug is fixed. Ran the tool as shipped: total_stubs=30, named_stubs_skipped=[]. FUNC_RE drops nothing today. Real skips TODAY = 0. + +(2) KILLER: the one gap item is a JUSTIFIED EXCLUSION, not a function. config/symbols.us.txt:981 declares `listCdBuffer = 0x80180000; // data`. Parsed sig.ov_SC01_077.jsonl: func_8017fd14 has nins=249, and 0x8017FD14 + 249*4 = 0x801800F8 = exactly the next sig entry's addr. NO sig entry exists at 0x80180000 -- the authoritative signer says no function begins there. Counted instructions in the asm: func_8017FD14.s = 187 ins ending 8017FFFC with a DANGLING `lui $at, (0x80190000>>16)` and containing NO `jr $ra` anywhere (it never returns); listCdBuffer.s = 62 ins starting 80180000 with `addu $at,$at,$s1` / `lbu $v0,-0x5611($at)` -- the %lo continuation of that dangling %hi -- and ending with the real `jr $ra`. 187+62 = 249. Contiguous (0x8017FFFC+4 == 0x80180000). listCdBuffer IS the tail/epilogue of func_8017FD14, spuriously split because a bogus `// data` symbol sits mid-function and splat cut there. + +(3) The proposed R33 remedy is ACTIVELY HARMFUL: deleting the name-shape gate and resolving via address would inject a 62-ins PHANTOM target (0x80180000) into the fuel manifest -- a function that does not exist and can never match -- sending a drafting agent to grind another function's epilogue. FUNC_RE is the only thing currently preventing that. It excludes for the wrong reason (name shape) but reaches the right answer. + +(4) Claim's factual errors: "listCdBuffer has no address to derive" is FALSE (config/symbols.us.txt:981 = 0x80180000; the claim never looked). The addr_int ValueError DOES reproduce, but it is a LOUD crash on a symbol the tool correctly never reaches -- a silent-skip finding whose worst case is a crash is not a silent skip. Blast radius is neither live nor even latent-real: it is latent-and-wrong. + +Cited evidence at file:line DOES exist (src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:5549 and the .s on disk) -- but it evidences the opposite of the claim. The genuine defect is NOT in build_fuel_manifest.py: it is the bogus `listCdBuffer = 0x80180000; // data` entry in config/symbols.us.txt fracturing a real 249-ins function into two .s files. That is a symbol-table data-quality bug and belongs in a separate finding. + +- **`tools/build_fuel_manifest.py`** — nins_from_asm() at tools/build_fuel_manifest.py:94-98 — hardcoded `asm/{source}/nonmatchings/{source}/{name}.s` + - killed: RAN: (a) instrumented live_stubs()/load_src_h_exact() via .run/audit/skeptic/probe.py; (b) executed `.venv/bin/python tools/build_fuel_manifest.py --out .run/audit/skeptic/fuel_test.json`; (c) cross-checked every func_*.s in ALL 12 asm/ov_SC01_077/nonmatchings/* subdirs against .run/sig.ov_SC01_077.jsonl via probe2.py. + +COUNTS REFUTED. candidates=264 is wrong: that is `grep -c INCLUDE_ASM` over all 14 src/ov_SC01_077/*.c files, but live_stubs() (:78-79) only opens THREE ({src}.c, {src}_a.c, {src}_o0.c). Measured live_stubs = 30 (main 13 / a 10 / o0 7). nins_from_asm() is never invoked on the other 234 stubs — they are invisible to the tool. The auditor borrowed live_stubs()'s blind spot (a DIFFERENT defect in a DIFFERENT function) as this scanner's denominator. real_skips=251 is therefore fiction; the theoretical mislocation ceiling is 17 (_a=10, _o0=7). + +REAL SKIPS = 0. Line :146 is `nins = (s or {}).get('nins') or nins_from_asm(...)` — the fallback only runs when the sig lacks a non-zero nins. Measured: 30/30 live stubs have a sig row and 0 sig rows have falsy nins, so the `or` short-circuits every time. nins_from_asm() is called ZERO times. Tool run confirms: 30 targets, `nins is None` = 0, `leverage == 0` = 0. No target is de-ranked. + +BLAST RADIUS REFUTED, NOT MERELY LATENT. The claim's own trigger example is wrong twice: named_stubs_skipped == [] (no listCdBuffer stub exists in the 3 files live_stubs reads), and FUNC_RE drops named stubs INSIDE live_stubs() at :87-90, so they never enter `targets` and can never reach :146. The claim concedes this and still counts it as "fires exactly once" — it fires 0 times. Nor is it "armed": I matched all 263 func_*.s across every nonmatchings subdir against the sig — 0 have no sig row. The sig is derived from the original overlay bytes and covers every function in the overlay, so a live stub with no sig would require an INCLUDE_ASM for a function not in the binary. Unreachable by construction, not by accident. + +RESIDUE: the hardcoded path IS stale for _a/_o0 (17 stubs would mislocate IF the branch were reachable). That is dead-code hygiene, not a silent-skip bug. The R33 "DELETE the scanner" recommendation is fine as cleanup, but it fixes nothing that fires. + +- **`difficulty.py`** — tools/difficulty.py unmatched_stubs() — the `if s.startswith('#ifdef NON_MATCHING'):` skip loop that advances to the first line starting with `#endif` + - killed: RAN: (a) .run/audit/skeptic/repro.py — imported the real tools/difficulty.py, ran unmatched_stubs() over all 136 binaries vs my own over-approximating INCLUDE_ASM regex. Counts reproduce EXACTLY (candidates 60740, parsed 60733, gap 7) — but the claim's evidence table lists only 6 items; it omits src/800.c:328 func_80016714. All 7 are in src/800.c (binary `main`); zero gap in the other 135 binaries. (b) Read each cited line — the #ifdef NON_MATCHING / #else INCLUDE_ASM / #endif shape is real. (c) KILL SHOT — the 7 are a JUSTIFIED EXCLUSION, not a silent skip: tools/progress.py (header: "generated by tools/progress.py — authoritative") has the SAME skip loop at line 417 but uses it to EXTRACT the #else INCLUDE_ASM into its `nonmatching` bucket, and docs/progress.md:6 reads "NON_MATCHING (near-miss) : 7" with all seven named at line 17 (func_80016714 included). These functions are a first-class, named, tracked category under G4 (honest NON_MATCHING drafts, never linked in the default build). difficulty.py is the harvest queue for UNATTEMPTED work; these 7 are precisely the ATTEMPTED ones, each carrying an in-file residual note. The skip loop is not vestigial either — a NON_MATCHING C body never contains an INCLUDE_ASM, so dropping the #else stub is its ONLY possible effect, i.e. its purpose; the docstring says "and not inside NON_MATCHING". (d) .run/audit/skeptic/score.py — the tool sorts EASIEST-FIRST and prints top 120; score at rank 120 is 5. The 7 score 29–723 and would rank 1044/1529/1896/1964/1991/1994/1997 of 2000. Not one would ever enter the printed queue; three of the four "high-reach state machines" are in the bottom TEN of the binary. The claim's severity argument is inverted. (e) grep for difficulty.md|difficulty.csv across all .py/Makefile/.sh/.js: only tools/difficulty.py itself — no downstream consumer. Only artifact affected is docs/difficulty.md:3 headline (1993 vs 2000) for `main`, a closed byte-identical EXE; current Phase-26 overlay work has zero gap. (f) Robustness sweep over every #ifdef NON_MATCHING block in src/**/*.c for a nested #if (would end the skip early) or an unterminated block (would swallow the file): 0 defects, 0 unterminated. The latent fragility exists in the shape but nothing in the corpus triggers it. Real silent skips: 0. + +## MEASURED CLEAN — do not re-audit + +- `INCLUDE_ASM text-format uniformity (underpins harvest_verify.py:32/89 STUB literal substitution, and every stub regex in the group)` — 60744/60740 — MEASURED over every src/**/*.{c,h}. 60744 INCLUDE_ASM tokens; 60740 are real stubs; the 4 stragglers are prose in comments (src/resident/resident.c:861, src/ov_SC01_077/ov_SC01_077_after.c:1911-1912, one in ov_SC01_077_jr_801734BC.c). Every one of the 60740 is EXACTLY `INCLUDE_ASM("", );` on ONE line, with EXACTLY one space after the comma (60740/60740 — zero whitespace variants) and ALWAYS a trailing semicolon (0 exceptions). harvest_verify's exact-string STUB.format() substitution and its `if line not in s: return None` guard are therefore SAFE fleet-wide, and render() can never mis-splice. Nobody needs to re-audit the string format. +- `jtbl_family_bank.stub_file() + bank_exemplar.py:44 stub regex — `INCLUDE_ASM\("[^"]*",\s*{FUNC}\);` over glob src//*.c` — 58717/58717 — FULL COVERAGE, measured over all 134 overlays: every one of the 58717 distinct (overlay, symbol) stubs is located; 0 missed. Crucially this tool does NOT have the suffix-allowlist bug — the glob `src//*.c` picks up all 1200 _jr_*.c splits plus _after/_a/_o0 (1604 overlay .c in the tree today). Also verified: 0 symbols are stubbed in more than one file of the same overlay, so 'return the first match' is unambiguous. And the symbol is interpolated, not shape-matched, so curated names like listCdBuffer work here. bank_exemplar's stub location is sound — do not spend effort here. +- `bank_exemplar.py:65 build-success test — `b.returncode == 0 and "[ OK ]" in b.stdout`` — 1/1 — SOUND. Makefile:536 declares `build: check`, and the check target (Makefile:571-577) prints `[ OK ] $(OUT)` ONLY after `sha1sum $(OUT)` equals config/check..sha. So the marker is a genuine byte-identity assertion, not a build-completed message. bank_exemplar cannot false-positive a bank. (Its FAILURE-side reporting is a separate finding, but the accept/reject decision itself is correct.) +- `gate_stage.py:87 _dedup_group_count() — `re.findall(r"^\s*-\s*id:", ..., re.M)` over config/dedup.us.yaml` — 1806/1806 — FULL COVERAGE, measured: the regex counts 1806; yaml.safe_load(...)['groups'] yields 1806. Gap 0. Every group in the registry uses the verbose `- id:` form, so the `^[A-Za-z]` fallback branch is dead but harmless. The `propagated` delta gate_stage reports is trustworthy. +- `gate_stage.py:95 match_one_closeness() — `first.startswith("MATCH")` / `re.search(r"(\d+) mismatch", first)`` — 1/1 — VERIFIED against real match_one.py output, not by reading the regex. Ran tools/match_one.py on a live draft: first line is `DIFF func_801412A8 mine=172 ins, target=198 ins, 164 mismatched` -> the regex correctly extracts 164 ('mismatch' is a prefix of 'mismatched'). The MATCH path (match_one.py:90 `MATCH (%d ins) %s`) and every failure path (CPP FAIL / CC1 FAIL / MASPSX FAIL / AS FAIL / `FAIL: no C file`) are all classified correctly, and none of them can be mistaken for MATCH. This parser is fine — the bug in its vicinity is that it is sometimes never CALLED (finding 2), not that it parses wrong. +- `gate_stage.py:220-221 — `//\s*@class:\s*(.+)` and `//\s*@stuck:\s*(.+)` over draft bodies` — 2353/2353 — FULL COVERAGE, measured over all 10731 real drafts. Over-approximating detector = the bare substring '@class' / '@stuck' anywhere in the file: 2353 drafts contain each marker, and the regex parses 2353 of each. Gap 0 for both. The drafter's self-reported residual class always reaches the backlog (when the body reaches the loop at all — see finding 2). +- `gate_stage.py:55 _xform() draft passthrough — canon_resident_calls.py / cast_call_sites.py / reconcile_decls.py` — 12/12 — MEASURED by RUNNING all three on a real draft dir (.run/drafts-giants, 12 drafts, ov_SC01_077): canon 12->12, cast_call_sites 12->12 (6 recovered, 41 callees), reconcile_decls 12->12 (4 reconciled). All three copy every draft through unmodified. I initially suspected a drop from on-disk dir sizes (drafts-giants 12 vs drafts-giants-cn 6) and RETRACT that — those -cn dirs are stale artifacts of an older run, not evidence. Stage 1 of the chain loses nothing. (sig_unify, stage 2, DOES emit only rewritten drafts — that is correct in itself, and the damage is downstream in the backlog loop; see finding 2. _xform's missing returncode check remains a theoretical hole I could not make fire.) +- `overlay_src_split.parse_overlay_c() — the substrate parser that jr_isolate_all REWRITES SOURCE FILES from (_partition -> _render_region -> new_files)` — 1604/1604 — THE MOST IMPORTANT CLEAN RESULT IN THIS GROUP. Ran over every one of the 1604 overlay .c (including all the Phase-26 `_jr_.c` splits — no suffix allowlist here, it globs `src/{ov}/*.c`). Round-trip FAILURES: 0. Unaddressed non-footer items: 0. Non-monotonic address transitions: 0. Since jr_isolate_all reconstructs each region .c purely from these items, a round-trip hole would SILENTLY DELETE SOURCE. There is none. The K&R discriminator (the audit brief's specific worry) is handled correctly by the brace/paren/semicolon forward scan in scan_construct() — the 267 col-0 `s32 arg0;` K&R parameter lines are all inside `def` items, not misread as declarations. Do not re-audit this. +- `jtbl_carve.PIECE_RE + EOF_RE + parse_config() (the config lines apply() DESTRUCTIVELY REPLACES)` — 4409/4409 — Over-approx = every `- [` list line in all 134 overlay configs: 4409. PIECE_RE matches 4275, EOF_RE matches 134, unparsed = 0. Critically: apply() does `lines[:lo] + region_lines + lines[hi:]`, so any unparsed line INSIDE [lo,hi) would be silently deleted from the config — I measured 0 such lines across all 134. I also checked the docstring/code mismatch (parse_config takes data_pieces[0], the FIRST data piece anywhere, not 'the trailing run after the last c piece' as documented): in 0 of 134 overlays does the data region start before the last code piece, so the shortcut is safe on the current corpus. (It is a latent trap if a future overlay ever uses the commented-out leading `head` rodata slot the template offers — worth a one-line guard, but nothing to fix today.) +- `jtbl_carve.jtbl_words() — `.word\s+(0x[0-9A-Fa-f]+)`` — 64430/64430 — Over-approx = every `.word` line between a `dlabel jtbl_*` and its terminating dlabel/glabel/enddlabel, in ANY operand form (symbolic, decimal, hex). Full sweep of all 134 overlays: 64,430 candidate .word lines, 64,430 parsed, gap 0. Also: jtbl_words returned a NON-EMPTY word list for 5043/5043 jtbls — so the `if words:` trim path is always taken. Full coverage. +- `jtbl_carve.code_pieces() + func_subseg() (config-derived address->subseg ownership)` — 58617/58617 — Cross-validated the CONFIG-derived answer against on-disk ground truth: for every one of the 58,617 func_-named INCLUDE_ASM stubs in the fleet, func_subseg(ov, func) names the same subseg directory the function's .s actually lives in. DISAGREEMENTS: 0. The docstring's claim that the config is authoritative and stale-proof is CORRECT, and I confirmed the premise too: 0 stale asm//nonmatchings/ subseg dirs currently exist that are absent from their config. (The 100 curated `listCdBuffer` stubs ValueError here — that is loud, and is covered under the naming finding, not a silent skip.) +- `jr_isolate_all.code_objects() and rodata_carves()` — 2806/2806 — code_objects: 1604 parsed / 1604 `- [0x..., c, ...]` lines present. rodata_carves: 1202 parsed / 1202 `- [0x..., .rodata, ...]` lines present. Both at exactly full coverage across all 134 configs (the `.rodata` regex correctly tolerates the trailing `# Phase-26 §8 jtbl-rodata carve` comment jtbl_carve appends). The fail-loud ascending/unique code-subseg validator at :438-451 is also correct and does fire on the corrupt-config class it describes. +- `overlay_src_split.macro_table() / _invocation() / macro_externs() / macro_proto() — source (2)+(3) of the carried decl layer` — 222841/222841 — macro_table() builds 1804 macros and captures ALL 1801 `#define DEFINE_func_*` in src/shared/engine_core.h (plus ov_setters.h/clearTbl40.h). Across the fleet there are 222,841 `define`-kind items (the shared-body macro invocations the audit brief warns tools miss by only scanning .c) — and _invocation() resolves 222,841 of 222,841 to a macro in the table. ZERO unresolved. This is the one place in the group that correctly sees the engine_core.h shared bodies. +- `jtbl_carve.overlay_vram_base() / jr_isolate_all.oss_vram() — `vram:\s*(0x[0-9A-Fa-f]+)` first-match` — 134/134 — Each of the 134 overlay configs contains EXACTLY ONE `vram: 0x...` key (the template's prose comments mention 0x80128158 but never as a `vram:` key), so the first-match shortcut is unambiguous. 134/134. +- `jtbl_carve.set_overlays_var()/revert() and jr_isolate_all.repoint_overlays_mk() — the config/overlays.mk splice regexes` — 1202/1202 — All 134 overlays have both a `_JTBL_INTERLEAVE` var (so revert's committed-value restore path is exercised, not the drop path) and the `_SPLAT_YAML := config/splat..yaml` anchor set_overlays_var needs (0 missing, so its sys.exit can't fire). And every one of the 1202 `.rodata` carve subsegs in the configs is present as `.o` in its own overlay's `--order` list: 0 misses. Subseg names are `_`-prefixed, so repoint_overlays_mk's `(--order[^#\n]*?){old_sub}\.o` + count=1 cannot cross-hit another overlay. Clean. +- `jtbl_family_bank.stub_file() glob + the strict INCLUDE_ASM form; and the build gate `"[ OK ]" in b.stdout`` — 58717/58717 — TWO clean results here, both worth banking. (1) The `src/{ov}/{ov}*.c` glob sees 1604/1604 overlay .c — it does NOT have the suffix-allowlist bug the audit brief warns about, because every split file (`_a`, `_o0`, `_o0b`, `_after`, and the Phase-26 `_jr_`) is ``-prefixed. 0 files missed. (2) The strict regex form `INCLUDE_ASM("[^"]*",\s*{func}\);` matches 58,717/58,717 stubs — every stub in the fleet is on one line and ends in `);`, and there are 0 lowercase-hex func_ symbols anywhere in overlay src (so bank()'s `%08X` uppercase constructor is safe), and 0 functions with stubs in 2+ files (so stub_file's first-hit rule is unambiguous). The defect in this tool is ONLY the address->name construction, not the scanning. (3) The GATE is invariant-derived and correct: `make build` -> Makefile:536 `build: check` -> check:571-577 emits `[ OK ]` only when sha1(OUT) == config/check..sha. It reads the byte verdict, it does not re-derive it. This is the harvest_verify pattern and should be the model for the rest of the group. +- `tools/family_hseq.py:35-38 — overlay enumeration `glob.glob("src/ov_*")` + per-overlay `glob.glob(f"{ovdir}/*.c")`` — 1604/1604 — NO suffix allowlist. The plain `*.c` glob reads every overlay source file including the Phase-26 `_jr_.c` splits (which is where 100% of the curated listCdBuffer stubs live). Verified: 1,604 files under src/ov_*/*.c, all read. The '811 exist / 404 seen' allowlist bug does NOT affect either of these two tools. Same for family_manifest.py:33 (ov077's *.c glob). +- `tools/family_hseq.py:40 + tools/family_manifest.py:21 — sig-file <-> overlay-dir bijection and the `ov = f"ov_{p.split('sig.ov_')[1][:-6]}"` name derivation; includes the silent `if st is None: continue` at family_hseq.py:43` — 134/134 — MEASURED: 134 src/ov_* dirs, 134 .run/sig.ov_*.jsonl files, exact bijection — 0 src dirs without a sig, 0 sig files whose name fails to map back to a src dir. The `continue` at line 43 (which would silently drop an ENTIRE overlay's 2.5k functions from the survey) currently fires 0 times. Latent risk only; it should still be an assert rather than a continue, but there is nothing to find here today. +- `tools/family_manifest.py:37 — `re.findall(r'hash:\s*([0-9a-f]{40})', open("config/dedup.us.yaml").read())`` — 1806/1806 — MEASURED at FULL coverage. Over-approximation = every 40-hex token anywhere in the file (`grep -coP '\b[0-9a-fA-F]{40}\b'`) = 1,806; the tool's regex extracts 1,806. Gap = 0. Every 40-hex token in config/dedup.us.yaml sits behind a `hash:` key (verified: the only key holding a 40-hex value is `hash:`, 1,806 occurrences), there are no uppercase hashes and no alternate spellings. Do not re-audit. +- `the matched-vs-unmatched ORACLE DIRECTION in family_hseq.load() — `matched := (addr not in that overlay's stub set)`` — 283050/283050 — MEASURED SOUND. For all 283,050 sig functions the tool calls MATCHED (non-stub), a real body exists: a func_ definition/mention in that overlay's own .c, a DEFINE_func_() instantiation, or one of the 1,801 `#define DEFINE_func_*` macros in src/shared/engine_core.h. Orphans (claimed matched, no body anywhere) = 0 across all 134 overlays. So the invariant-derived direction is correct and the K&R/def-vs-decl and shared-macro traps do NOT bite here — the ONLY way to corrupt this oracle is UNDER-detecting stubs, which is precisely the reported finding. +- `tools/family_hseq.py:128/134 — the word-level backing of the survey via family_remap.stream_words / classify_member (overlay image lookup)` — 134/134 — MEASURED: 0 of 134 overlays return a None image from FR.img_path, so stream_words never silently returns None. Consistent with the published .run/family_hseq.json: 0 families with any LEN member, 0 LEN members total (a missing image would manifest as a family whose every member classes LEN and whose templatable weight silently collapses to 0). Not a text scanner and not a hole. +- `tools/build_fuel_manifest.py:28 STUB_RE — `INCLUDE_ASM\([^,]+,\s*(\w+)\)`` — 267/264 — FULL COVERAGE. Over-approximating detector = every line in every src/ov_SC01_077/*.c containing the literal 'INCLUDE_ASM' → 267 lines. STUB_RE extracts 264 unique symbols. The gap of 3 is 100% justified exclusions, all comment prose, individually classified: ov_SC01_077_after.c:1911 and :1912 (a '// ... no INCLUDE_ASM-side escape needed ...' explanatory comment) and ov_SC01_077_jr_801734BC.c:3202 ('// ... func_801777BC INCLUDE_ASM 7-arg ...'). Zero duplicate symbols. The regex itself is sound — the bug in this tool is entirely in WHICH FILES it is pointed at (see findings). Same regex is used verbatim in wave_targets.py:19; it is clean there too. +- `tools/build_fuel_manifest.py:27 INS_RE — `/\* [0-9A-Fa-f]+ [0-9A-Fa-f]{8} [0-9A-Fa-f]{8} \*/` (instruction counter)` — 263/262 — FULL COVERAGE. Cross-validated against an independent oracle: for all 263 func_ .s files under asm/ov_SC01_077/nonmatchings/*/, INS_RE's count was compared to the immutable sig nins (.run/sig.ov_SC01_077.jsonl, which signs the ORIGINAL bytes). 262/263 agree EXACTLY. The single disagreement — func_8017FD14, INS_RE=187 vs sig=249 — is NOT a parse hole: the .s header itself declares `nonmatching func_8017FD14, 0x2EC` (0x2EC bytes = 187 ins), so the file genuinely contains 187 instructions and the regex counted them correctly. That is a real sig-vs-asm corpus discrepancy (function-boundary drift, worth a separate look by whoever owns the splat/sig layer) surfaced BY this cross-check, not a defect in INS_RE. +- `tools/wave_targets.py:53-57 live_stubs() — `glob('src/ov_SC01_077/ov_SC01_077*.c')`` — 264/264 — FULL COVERAGE — 264/264, the only one of the three live-stub enumerators in this tool group that is correct. It globs the filesystem instead of consulting an allowlist, so the Phase-19 and Phase-26 splits widened its corpus automatically. It is tragically ineffective in practice because it is only ever used as a FILTER (`if t['name'] not in stubs`) against build_fuel_manifest's 30-target list — a correct scanner downstream of a broken one cannot rescue it. THIS is the function to promote into a shared tools/live_stubs.py helper; do not rewrite it. +- `tools/wave_targets.py:44-50 canon_class() — the 8-entry CLASS_KEYWORDS keyword matcher over backlog klass + where_stuck` — 1306/466 — ZERO REAL SKIPS — a clean result, though the headline (840 of 1306 near-misses land in OTHER, 64%) looks alarming. Over-approximating detector: every OTHER record whose where_stuck is NOT a bare machine-generated ': N mismatch' / 'residual: N mismatch' string, i.e. contains any human prose that could carry a class signal. Result: of the 840 OTHER records, 836 have literally NO class signal in the input (they read exactly 'WAVE: 17 mismatch', 'residual: 4 mismatch' — the automated batcher never wrote a residual description), and the remaining 4 with prose are all self-MATCH verdicts ('none — MATCH (60 ins)') which are correctly handled by plumbing_blocked() instead. Genuinely-unclassified prose: 0. The classifier's ceiling is the backlog's data quality, not its regex — the --class flywheel reaching only 36% of the backlog is an upstream logging gap, not a scanner hole. Do not spend effort widening CLASS_KEYWORDS. +- `tools/wave_targets.py:75-103 plumbing_blocked() — the self-MATCH-but-gate-rejected detector (`closeness==0` OR where_stuck startswith 'none —'/'none -' OR contains '— match'/'match_one match')` — 1723/1691 — ZERO REAL SKIPS, gap fully classified. Over-approximating detector: every 'near' record in the raw .run/backlog.jsonl (3542 of them) with closeness==0 OR whose where_stuck contains the word 'match' with 'mismatch' first stripped out → 1723 candidates. plumbing_blocked() catches 1691. All 32 in the gap were individually inspected and are JUSTIFIED exclusions: every one is a genuine near-miss with closeness>0 whose prose merely contains a PARTIAL-match phrase, e.g. func_8012B77C close=43 'register HOMES + mult/acc-build region + dy-in-delay-slot all byte-match; residual is sched.c load-region tie-…', func_80134A74 close=84 'structure + all 8 callee-saved regs (s0..s7) + block layout MATCH; residual is caller-saved-temp…'. Blocking those would be a false positive, and the tool correctly does not. The scanner does what its docstring says. +- `tools/wave_targets.py:60-72 backlog_walls() — status in ('failed','stub')` — 709/709 — FULL COVERAGE. Enumerated the complete status vocabulary of the real .run/backlog.jsonl (4255 records): {'near': 3542, 'failed': 709, 'capped': 1, None: 3}. The filter catches 709/709 'failed' records (476 distinct names). The alternative 'stub' status it also tests for never occurs in the corpus (harmless dead branch). 'capped' (1 record) is deliberately not a wall — it belongs to the CAPPED recovery pool. The 3 status-less records carry no name. No status value is silently missed. +- `tools/exemplar_miner.py:33-42 LEVER — bucket→lever routing map, with a silent `LEVER.get(bucket,'WAVE')` default at :74` — 18/18 — FULL COVERAGE. Enumerated every distinct bucket value in the real census .run/wall_taxonomy.json (915 records): 18 distinct buckets (STRUCTURAL_MISS 368, PERMUTER_CLASS 146, MCOMPILE_fnptr-call 85, ARITY_WALL 74, M2C_DECOMP_FAIL 59, MCOMPILE_stack-var 45, MCOMPILE_arg-arity 32, MCOMPILE_undeclared-other 21, VOID_VALUE_MISUSE 17, NONFAITHFUL_DEFER 16, MCOMPILE_m2c-incomplete-arg 14, MCOMPILE_bad-deref 14, OTHER_CONFLICT 9, MCOMPILE_other 8, SIG_FIXABLE_KR 4, LEAF_READY 1, MCOMPILE_bad-switch 1, DATA_CONFLICT 1). ALL 18 are explicit LEVER keys — the silent default-to-WAVE fallback fires ZERO times on the real corpus. (Two LEVER keys, NO_ASM and M2C_EMPTY, are unused.) Separately verified: all 263 live func_ stubs receive a routing entry in .run/exemplar_routing.json (levers: WAVE 152, STRUCT 73, PINS 24, STUB 14), so build_fuel_manifest.classify()'s `routing.get(ai,{}).get('lever') or 'WAVE'` default also never silently fires. The routing map is not where this tool is broken. +- `tools/dedup_propagate.py:40-43 onboarded_overlays() — `re.search(r'^OVERLAY_BINARIES\s*:=\s*(.*)$', mk, re.M)` (exemplar_miner's reach-computation dependency)` — 134/134 — FULL COVERAGE, checked because a `(.*)$` regex over a Makefile is a classic backslash-line-continuation trap — if OVERLAY_BINARIES were split across continued lines, reach would be silently undercounted for EVERY function in the project. It is not: config/overlays.mk:11 declares all 134 overlays on ONE physical line. Measured: onboarded_overlays() = 134, .run/sig.ov_*.jsonl on disk = 134, src/ov_*/ dirs = 134. Three-way agreement. dedup_propagate.registered_addrs() also resolves correctly (1807 addrs) — though note its bare `except Exception: return set()` is a silent-open failure mode that would make every function look unregistered without a single diagnostic. +- `masked_diff.py:32 _INS_RE — objdump instruction-line parser` — 15179364/12821393 — FULL COVERAGE. Over-approximating detector = ANY address-prefixed line (`^\s*[0-9a-f]+:`) in `objdump -drz -j .text` across all 3,367 real build objects. The 2,357,971-line gap is EXACTLY equal to the parsed reloc-line count (2,357,971) — i.e. every single address-prefixed line that _INS_RE rejects IS a relocation line (`\t\t\t12f8: R_MIPS_26\t.text`), which is correct and intended. Zero instruction lines missed, corpus-wide. Also confirmed: 0 instructions carry more than one reloc line, so the `insns[-1]` single-slot reloc attachment is sound. +- `masked_diff.py:33 _REL_RE — objdump relocation-line parser` — 2358076/2357971 — FULL COVERAGE. Over-approximating detector = ANY line containing `R_[A-Z0-9_]+` across all 3,367 build objects. Gap = 105, and I classified ALL 105: they are my detector's own false positives — PsyQ symbol NAMES that happen to contain the substring `R_` (SR_SV_OBJ_32C, INTR_OBJ_514, MEMCHR_OBJ_20, COR_05_OBJ_1AC, PUTCHAR_OBJ_3C, ...) appearing as branch targets, plus 3 objdump `file format elf32-tradlittlemips` banner lines from files named INTR_DMA.o / INTR_VB.o / COR_05.o. Not one is a real reloc line. _REL_RE misses nothing and its reloc-TYPE capture is correct — the PC16 bug reported above is downstream in mask_for(), not here. +- `masked_diff.py:31 _HDR_RE — objdump header parser` — 344506/344506 — FULL COVERAGE, exact. Over-approximating detector = `^[0-9a-f]+\s+<.*>:` over all 3,367 build objects. 344,506 candidates, 344,506 parsed, gap 0. Correctly handles the curated non-func_ symbol names the brief warns about (listCdBuffer, GsSortFastBg, MEMCHR_OBJ_20, ...) because it captures `<([^>]+)>` rather than a func_-only pattern. No func_-only-regex hole here. +- `masked_diff.py:93 insns_from_s — splat .s instruction-LINE parser (line-level coverage only)` — 4343434/4343434 — FULL LINE-LEVEL COVERAGE across all 60,740 nonmatchings .s. I exhaustively classified EVERY ONE of the 410,534 non-blank unparsed lines (not a sample): 227,117 `.L*:` local labels, 60,740 `nonmatching` headers, 60,738 `glabel`, 60,738 `endlabel`, 1,194 `/* Handwritten function */` comments, 3 `alabel`, 2 `dlabel`, 2 `enddlabel`. ZERO directive lines and ZERO instruction-bearing lines are missed — every unparsed line is a justified exclusion. The little-endian decode (struct.unpack('= len(mine) => me=None vs an int tg => counted; i >= len(tgt) => tg=None vs a masked int => counted, including the mask==0 case where me becomes 0 and tg None). Crucially the MATCH verdict at line 89 is correctly length-guarded (`if not diffs and len(mine) == len(tgt)`), so a short/long candidate can never be reported as a MATCH. Replayed across 1,589 historical objects with an unambiguous target: identical verdicts. It inherits the mask_for defects reported above but adds none of its own. +- `masked_scorer.py:33 MaskedScorer.__init__ — insns_from_object(target_o) with fn=None (the 'each object holds exactly ONE function' assumption)` — 200/200 — ASSUMPTION HOLDS — measured, not assumed. If either object held >1 function, the fn=None whole-.text parse would splice them together and permanently inflate the score. I checked the real permuter corpus (.run/permuter, 302 dirs): 200/200 target.o hold exactly ONE function header, and 296 of 300 base.c define exactly one function (the other 4 define zero — an empty/failed setup, which score() already guards via `if not cand: return PENALTY_INF`). No splicing risk. Also clean: the SHA-256 candidate dedup hash at masked_scorer.py:44-47 folds in word + reloc_kind + reloc_op, so it cannot collide two candidates that differ only in a relocation operand. +- `masked_diff.py:36 SCALAR_TYPEDEF_RE — the typedef NAME SET (as distinct from the line shape)` — 15/15 — NAME SET IS COMPLETE. I extracted every scalar typedef name that include/**/*.h actually provides (the true must-strip set): exactly 15. The regex's hardcoded list (u8/u16/u32/u64/s8/s16/s32/s64/f32/f64/M2C_UNK{,8,16,32,64}) covers all 15 — latent name collisions: 0. So there is NO missing-name hole here and no need to hunt for one; the 55 real skips reported above are caused purely by the line-shape anchors (`^` + `;[ \t]*\n`), not by an incomplete name list. Recording this so nobody re-audits the name set. +- `sig_image.py detect_code_end() (tools/sig_image.py:44) — the code->data boundary detector` — 134/134 — MEASURED, and this is a REFUTATION of a strong prior, not a shrug. The hypothesis was serious: sig_image runs with --bootstrap for all 134 overlays (Makefile:180), so boundaries are pure heuristic with no seeds, AND tools/new_overlay.sh:34 derives each overlay's splat text/data carve from 'the end of the last sig_image function' — so a truncating code_end would carve real CODE into a raw .bin data blob, losing functions permanently while the byte-gate stayed green. I built an over-approximating detector for exactly that: scan every byte from sig_image's code_end to EOF in all 134 raw 0.4.dec images for the `jr $ra` word (0x03E00008), the unforgeable signature of a function epilogue. RESULT: 0 occurrences. Zero, in all 134 overlays, across ~6.5 MB of data tail. Only 3 stray `addiu $sp,$sp,-N`-shaped words fleet-wide (data coincidences, no returns anywhere near them). There is no code beyond code_end. The hypothesis is dead; nobody needs to re-audit this. +- `sig_image.py func_end() (:76) + bootstrap_seeds() (:62) — function boundary detection` — 58621/58524 — MEASURED at full coverage via an INDEPENDENT oracle. Ground truth = the size header ('nonmatching func_X, 0xSIZE') and glabel address in every splat-emitted .s, which comes from spimdisasm's own algorithm — and I verified non-circularity: all 134 config/symbols.ov_*.txt files contain ZERO symbols, so splat's overlay function detection is in no way seeded by sig_image. Two independent codebases, two algorithms. 58,621 comparable function boundaries; 58,524 byte-exact agreement on BOTH start and size. The 97 disagreements are 100% attributable to the listCdBuffer symbol collision (CRITICAL finding #1) and in every one of them sig_image is the CORRECT party. So func_end's documented risks — the early-return `jr`, the trailing orphan `jr;nop` double-epilogue, the tail-call function ending in `j` that could merge two functions — are all real hazards in principle and all MEASURED ABSENT at fleet scale. h_exact rests on solid boundaries. +- `dup_report.py — the BINARIES allowlist, cross_report()'s named-union-glob ingestion, and _load_sig()` — 134/134 — MEASURED at full coverage. This was the prime suspect for the hardcoded-allowlist bug (the brief warned several tools hardcode a list and miss files). It does not have it: BINARIES holds exactly 134 ov_ entries, exactly 134 .run/sig.ov_*.jsonl exist, exactly 134 src/ov_* dirs exist, and all three sets are IDENTICAL — empty symmetric difference in both directions. cross_report()'s named-union-glob dedup (the R14 honesty fix) resolves all 134 with 0 missing files, and main + resident both resolve. The nins>=8 default filter drops 41,604 of 341,671 signed overlay functions (12.2%), but that is an exposed CLI argument and a documented design choice, not a silent skip. Note dup_report does no text parsing at all — it consumes sig JSONL — so its correctness is inherited wholesale from sig_image, which is verified above. +- `difficulty.py INSTR regex (:~185) and BRANCH regex (:~186), and find_s()` — 365665/365665 — MEASURED at full coverage. Over-approximating detector = any `*/ ` on any line of any .s. Over a 6,000-file sample of the real asm corpus: 365,665 instruction lines found by the over-approximator, 365,665 matched by INSTR. Gap ZERO — not one mnemonic in the entire corpus defeats it, including delay-slot lines with their extra leading space. BRANCH likewise: the corpus contains exactly 9 branch-like mnemonics (b, beq, beqz, bgez, bgtz, blez, bltz, bne, bnez) and the regex matches all 9, missing none. find_s() resolved all 60,733 stub names to a .s file with 0 failures — its single-level `*/{name}.s` glob happens to match the real asm//nonmatchings// depth exactly (unlike derive_canonical_sigs, which hardcodes the subdir NAME and thereby loses 95%). difficulty.py's src glob is `*.c` and so does NOT have the _jr_* suffix-allowlist bug. diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index a1b401e9c..899720cb1 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -59,7 +59,14 @@ The Phase-25 h_seq reframe: the "unique tail" is really per-location families **R33 applied:** the bank claim is now *derived* from the build invariant (`INCLUDE_ASM` pastes the ORIGINAL asm ⇒ a member not wrapped in it is byte-exact) rather than trusted from the registry. The C1 sig check is kept but its docstring **corrected** (P9): a sig signs the ROM, so it is immutable w.r.t. `src/` and **cannot** detect source drift, which it claimed to. **THE CAUSAL CHAIN (the audit's thesis in one example):** 3 of the 4 hidden fns are defined in `ov_SC01_077_jr_8012ACE0.c` — a `_jr_*` split file — and `dedup_propagate.overlay_files` allowlists only `("_a","_o0","_o0b","_after")`. The propagator **could not see them**; the group was registered anyway; `dedup_integrate` greenlit the lie. **Two silent-skip bugs compounding: one created the hole, the other hid it.** Fuel → `.run/audit/a1_harvest_fuel.json`, banked in **A6**. **Blast radius, MEASURED (R14):** headline metrics **unchanged to the decimal** (66.5% / 46.8%) — `weighted_metrics()` derives from the invariant and was *structurally immune*; `progress.py` had already been taught to distrust the registry (`commit:0574`). **A null result that CONFIRMS R33: the tool that refused to re-derive was the one that was right.** Registry 1813 → 1806 groups; `make report` green; negative controls all fail-closed. -- [ ] **A2 — THE FULL AUDIT: the remaining 18 gate/select tools** `[Ultracode — R27: STOP + prompt + WAIT]` — **RUNS NEXT, before any fixing.** `jtbl_family_bank` (3 bugs found by hand, never audited) · the SELECTION tools `family_hseq`/`family_manifest`/`wave_targets`/`exemplar_miner`/`worklist`/`build_fuel_manifest` · the closeness oracle `masked_diff`/`match_one` that every crack agent trusts · the bank/gate path `gate_stage`/`harvest_verify`/`bank_exemplar` · `dup_report`/`difficulty`/`sig_image`/`canon_draft_decls`/`derive_canonical_sigs`/`census_conflict_callees`. **METHOD (do NOT audit by reading regexes — that is the failure mode that WROTE these bugs):** build a deliberately **over-approximating** candidate detector, run both over the real corpus, compute `gap = candidates − parsed`, and **classify every item in the gap** as a real silent skip or a justified exclusion. Pair each auditor with an **adversarial skeptic** told to refute it (in the first audit several magnitudes were wrong in *both* directions and one whole class was refuted). **R33 before R32.** +- [x] **A2 — THE FULL AUDIT: the remaining 18 gate/select tools** `[Ultracode]` — **DONE. 38 agents · 2.24M tok · 0 err. 32 findings raised → 28 SURVIVED adversarial verification (4 REFUTED, 16 downgraded); 40 scanners measured CLEAN.** Full write-up appended to **`docs/tooling-audit.md` (ROUND 2)**. + **🔴 THE ROOT CAUSE — one bug, ~10 times:** *a hand-maintained model of the corpus layout (a file allowlist, a single-`.c` assumption, a `func_`-only regex, a `REGION_SUB` dict) sitting on top of a filesystem that already answers the question.* Every TU split silently widened it. **Decay proven:** `.run/fuel_manifest.json` (Jul 8) recorded **130** stubs; the same tool today returns **30** — the Phase-26 splits moved ~100 stubs out from under a dict literal last edited in Phase 22, and **nobody noticed, because an un-nominated target produces silence, not an error.** + **THE NUMBERS (measured, skeptic-confirmed):** **91.6% of ALL remaining project gain is invisible to target selection** (true 994,633 ins; the manifest sees 83,305) · **117 of 127 reach-134 functions never nominated** · `harvest_verify` cannot see **56,742 of 58,717 (96.6%)** open stubs · **`wave_targets` hands 78 of 87 targets an asm path that does not exist.** + **THE THREE THAT CHANGE WHAT WE BELIEVE:** + **(1) The Phase-22 verdict "the permuter's fuel is exhausted" is UNSAFE.** `grinder` banks through `harvest_verify`, which sees one TU — **1,290 of 1,298 (99%) of the grinder's own queue could never have banked**, however good the permuter's output was. "0 banks since Phase 21" is equally consistent with *the tool could not bank*. **Re-test before repeating it (A12).** + **(2) The Phase-25/26 endgame plan is majority-fiction.** `docs/family-manifest.md` advertises "2,758 multi-member families / 11.0 MB of hidden leverage." **1,071 of them / 6.80 MB are ALREADY FULLY MATCHED** — phantom targets, **62% of the advertised byte-weight** — because the matched-set oracle scans ONE overlay. The byte-weight *ranking*, the entire point of the file, is sorted mostly on dead work. True frontier: 1,475 families / 3.9 MB. + **(3) A corpus defect the byte-gate is structurally BLIND to.** `config/symbols.us.txt:981` declares `listCdBuffer = 0x80180000` (a main-EXE **data** symbol) and every overlay's splat config loads that file — but in overlay space 0x80180000 is **code**. splat cuts **97 real functions in half** and invents **96 phantom ones**: **193 slices that can never be matched by anyone**, in 97 of 134 overlays. They sit in the harvest queue as ordinary work, so agents burn on them forever and the failure reads as an intrinsic compiler wall — **and the build stays byte-identical and green throughout**, because the `.s` halves are pasted back verbatim. *A perfect correctness oracle and a null coverage oracle.* What saved us: **`sig_image` was RIGHT** (58,524/58,621 agreement with spimdisasm; correct on all 97 disagreements). **A second independent oracle is the only reason this was visible at all.** +- [ ] *(superseded — see A3/A4/A5 below, restructured around the root cause)* `jtbl_family_bank` (3 bugs found by hand, never audited) · the SELECTION tools `family_hseq`/`family_manifest`/`wave_targets`/`exemplar_miner`/`worklist`/`build_fuel_manifest` · the closeness oracle `masked_diff`/`match_one` that every crack agent trusts · the bank/gate path `gate_stage`/`harvest_verify`/`bank_exemplar` · `dup_report`/`difficulty`/`sig_image`/`canon_draft_decls`/`derive_canonical_sigs`/`census_conflict_callees`. **METHOD (do NOT audit by reading regexes — that is the failure mode that WROTE these bugs):** build a deliberately **over-approximating** candidate detector, run both over the real corpus, compute `gap = candidates − parsed`, and **classify every item in the gap** as a real silent skip or a justified exclusion. Pair each auditor with an **adversarial skeptic** told to refute it (in the first audit several magnitudes were wrong in *both* directions and one whole class was refuted). **R33 before R32.** - [ ] **A3 — `tools/cdecl.py`: ONE coverage-asserting C-decl parser** `[Max]` *(blocked on A2)* — the same char-class disease (`[\w\s\*]` cannot hold `(`, `,`, `[N]`) is **independently re-implemented in 6+ scanners**; two tools in ONE pipeline already disagree about what a data decl *is*. Patching 6 regexes = 6 chances to diverge again. **R33:** one statement-oriented splitter + a **TOTAL** classifier (scalar/ptr/`[N]`/fn-ptr/fn-ptr-array/multi-declarator/wrapped/trailing-comment) with a built-in coverage assertion, consumed by all of them. Migrate consumers one at a time, each byte-gated. Kills ~15 of the 40 findings at the root. - [ ] **A4 — Wire `reconcile_tu.py`; retire the fleet-majority oracle** `[Max]` *(blocked on A2)* — `reconcile_decls.canonical_data_map` asks *"what does the FLEET call this symbol"* when C's constraint is **per-TU**: 36.8% of symbols carry ≥2 conflicting spellings, so one fleet-wide answer is **wrong for some TU by construction** — it returns an **actively WRONG** canon for **3,717** symbols and disagrees with the TU's own decl on **21.7%** of pairs (**every one of 678 TUs** has ≥1 broken answer). `reconcile_tu.py` is **written + validated + NOT WIRED** (`commit:0580`). Wire → `bank_exemplar` / `jtbl_family_bank` / `gate_stage`; byte-gate. Unblocks `func_8017A4AC` (287 KB), `func_8013F350`, `func_80131340`. - [ ] **A5 — Family engine: 93 phantom exemplars, 1,834 free members** `[Max]` *(blocked on A2)* — `family_remap.extract_unit` globs only `src//*.c`, so it **cannot read a `DEFINE_func_*` body out of `engine_core.h`** → **93 of 218 (43%)** "matched" exemplars are PHANTOM and **1,834** still-stubbed, PURE/IMM-clean, unpinned member templates are **never attempted**. Plus `symbol_map` naming an address-taken **function** `D_` (3 families × 37 members silently no-op, booked as *byte* failures), `gather_externs`' multi-line externs (9), `family_sweep.stub_map`'s curated-name stubs (3 phantom families). Fix, assert, **re-run the sweep, harvest.**