Files
BFM-decomp/docs/tooling-audit.md
T
Drew T 4f7c3b64a3 docs(phase-33): commit-map + citations resolved to the rewritten history (C4–C7 — the tip commit)
- docs/commit-map.tsv: 4,032 rows (ordinal of the ORIGINAL main -> rewritten hash, author/committer dates, subject);
  1 pruned row of zeros (ordinal 1712, "session archive update"); 0 old hashes asserted; ordinal 1 unchanged by the
  rewrite (byte-identical)
- resolve_tokens: 1,238 commit:NNNN tokens -> shortest-unique new hashes in 98 files (docs, phase-ends, logs, tool
  docstrings, 2 C comments, the A5 evidence logs); residue left as tokens: commit:1712 x4 (the pruned commit),
  commit:orphan-24 x2, commit:orphan-26, commit:orphan-35 (cited commits that exist in no lineage)
- the rewrite (C4): filter-repo 2.47.0 on a bare clone of the C2 tip, 311 s, exactly 1 pruned, main 4,032 -> 4,031;
  the pre-rewrite history is mirrored in the private archive repo and in the local bundle
- the proof (C5): verify_rewrite 4,031 pairs / 0 failures; absent_scan 0 offenders; gate_scan 0 offenders on the clone
- adoption (C6): 100 text files differ at the tip, 0 purge paths, 0 added/deleted; leftover refs dropped; no gc yet
- resolver skips tools/public_rewrite/ (its self-test fixtures are the token grammar, not citations); repo-local
  identity is the GitHub noreply address from here on; CURRENT_PHASE: C4–C7 logged, checkpoint -> NEXT = C8
2026-09-06 23:28:39 -06:00

395 KiB
Raw Blame History

Tooling-Integrity Audit — measured findings (Phase 26 session 8, 2026-07-14)

STATUS: ✅ CLOSED (Phase 26-A, 2026-07-14 → 07-15). This began as DIAGNOSIS ONLY (session 8, below) and became the inserted half-phase Drew gated ahead of all further matching ("we should do T14 now"). The AUDIT-CLOSE LEDGER immediately below is the outcome; the per-finding sections carry inline ✅ FIXED marks. The individual "STATUS/priority/not-yet-audited" notes further down are the session-8 diagnosis, preserved as the historical input.

Method (do not audit by reading regexes — that is the failure mode that WROTE these bugs). For each scanner: 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. 6 auditor agents, each followed by an independent SKEPTIC told to refute (their corrections are recorded per group — several magnitudes were wrong in BOTH directions, and one whole class was refuted).

Coverage of this audit: 19 of 82 tools (23%), chosen by risk. NOT comprehensive. See "Not yet audited".

AUDIT-CLOSE LEDGER (Phase 26-A outcome)

Verdict on the question this audit set out to answer ("how many walls were lookup misses wearing a wall's clothes?"): the broken tools WERE the walls, and fixing the oracles banked the payoff — fleet instr-weighted 66.5 → 68.6% across A3f/g/h + A9a/b, at 136/136 byte-identical throughout (final R22 clean-fleet 2026-07-15: 136 passed, 0 failed). The residual walls, re-tested, are real (P9 — a re-confirmed wall is as valuable as a dissolved one).

# tool / defect outcome commit
A1 dedup_integrate — a fail-closed gate that printed false greens 3 paths closed + negative controls; 7 ghost groups purged bb65d3634
A2 THE FULL AUDIT (18 tools · 38 agents · 2.24M tok) 32 raised → 28 survived, 4 refuted, 40 scanners clean ffb6f1a40
A3 corpus.py (derived oracle) + cdecl.py (C-decl grammar oracle) + gate_stage/harvest_verify/cast_call_sites/sig_unify migrated; the fleet-majority oracle RETIRED for reconcile_tu targets 30→263 · reach-134 10→127 · gain 83k→994,633 ins · byte-gate reach 4.9%→100% · 3,717 actively-wrong canonicals removed f7b7399eb..b89fcc2ed
A4 the listCdBuffer corpus defect 193 unmatchable slices → 0; a banked phantom removed; 4 real fns un-hidden af2f40d15
A5 closeness oracle (masked_diff PC16) 150 lies → 4 (coverage-asserted over 2,741 fns) af2f40d15
➕ stale objects can produce a FALSE PASS extract invalidates them — structural, not advisory a2a507d07
A6/A7 family engine + build_engine_types un-blinded 96 phantom exemplars → 0; 17 fns banked ×134 free; type-lift now RUNS 82d79e7a3
A8 jr_isolate_all dropped 683 prototypes — a latent BYTE-CHANGER fixed + coverage-asserted 2086b15b4
A9a canon_sig_reconcile blind to fn-ptr dispatch tables cdecl-supplemented (0 regressions / 69,798 additions) abbedcee8
A9b wall re-test payoff func_8017A4AC (536×134) — a many-phase "plumbing wall" — banked ×134 by the fixed oracle 3509acf4b
A9c lint_symbol_refs (the only R22 rename-drift detector) RED + UNWIRED green on HEAD + wired into make report d7d2613ae
A9d dead Phase-17 canonical-sig chain (census_conflict_callees+derive_canonical_sigs) DELETED (R33 — the build answers it) 40477281c
A9e reconcile_tu into bank_exemplar already wired via A3d (fb.recover) — null result, documented 68d29ba8a
A9f overlay_src_split swallowed 2 real definitions; selftest was blind force_decl latch fixed + hidden_definitions() R32 coverage oracle 96e025a32
A9g jr_inventory read banked-roster from an EPHEMERAL gitignored file derive banked from the image (R33); 1:1 carve-ownership assert; fleet 134/134 ea20bdf9f
A10 re-test the 5 walls ① closeness-0 0/958 = REAL · ② arity 13/18 fell (A3c) · ③ def-side dissolved (A9b) · ④ type-heavy tool unblocked (A7) → Task-8 harvest · ⑤ h_seq +2,675 (A3h) b1c58d766,e9038a04e

REMAINING (handed forward, not audit-blockers): (1) the un-migrated cdecl consumers — canon_sig_reconcile's non-fn-ptr classifier is still on its own regex (A9a supplemented only the fn-ptr data class, deliberately — a full cdecl swap rippled 728k value-changes); migrate one-at-a-time, byte-gated, when a specific bank needs it. (2) #4 the type-heavy harvest (~1,200 members across the 9 Work8016/E4/… families) — build_engine_types now RUNS but is not wired into the family path (remap_hseq refuses STRUCT members); this is Phase-26 Task-8 integration, not a re-test. (3) the closeness-0 residual is confirmed genuine codegen — not actionable by tooling.

Why this gates the matching work

The whole-binary byte-gate is a perfect correctness oracle and a null coverage oracle. It has never once accepted a wrong match — and it is blind by construction to work never attempted (it has been green since Phase 5, when 0% was decompiled, because INCLUDE_ASM pastes the ORIGINAL assembly: a green byte-gate is compatible with any decomp %). Every silent skip is therefore invisible to the one instrument we trust absolutely.

The cost is not wrong answers. It is invisible work, and walls that aren't there. One 10% hole in the callee-signature oracle (SIG_IN_BODY_RE — a \s that cannot match a line-continuation backslash) made nine byte-exact functions look like an intrinsic compiler wall. We would have written them up as such.

THE QUESTION THIS AUDIT ACTUALLY ANSWERS: how many of the walls we have already "byte-proven" across 26 phases were lookup misses wearing a wall's clothes? The def-side loose-typing wall (§20/§41, "triple- confirmed" in Phase 23), the 159 arity/narrow-param conflicts (Phase-15 "documented dead-end"), the 3,098 type-heavy tail, and the 9 zero-bank type-using families were all diagnosed on top of that hole. Phase 16 byte-proved that genuinely contradictory typings DO exist, so the wall is real in part — but "some of it was our tooling" is now the prior, not the long shot. Re-test the cheap ones.

The three rules this produced (P10 candidates — Drew ratifies at the Phase-26 PhaseEnd)

  • R32 — Assert your COVERAGE. A tool that scans the corpus must compare what it found against an over-approximating candidate set and fail on the gap. A silent skip is a DEFECT, not a no-op — and a LOUD failure nobody counts is exactly as invisible as a silent one (build_engine_types fail-exited on 81% of its corpus for four phases and stayed invisible because the message read like an edge case). (Drew: "agreed", 2026-07-14.)
  • R33 — Derive, don't re-derive. Where a proven invariant answers the question, derive the answer from it rather than re-parsing the source. Apply R33 to each tool BEFORE R32: the best audit outcome is not a fixed regex — it is a DELETED SCANNER. (28 findings → one derived oracle + ~10 deleted scanners; the dead canonical-sig chain and the ephemeral-roster read both ceased to exist rather than being patched.) harvest_verify is the model (derives from make build + SHA1, so a parse hole makes it conservative, not wrong); progress.py classify() was the counter-example (re-parsed C, inherited a bug).
  • R34 — A second oracle, not a better assertion. When an oracle is structurally blind to a class of error, no assertion inside it can help — add an INDEPENDENT oracle that can disagree with it, and make them argue. The whole-binary byte-gate is a perfect correctness oracle and a null coverage oracle; sig_image disagreeing with splat is what exposed the 193 listCdBuffer slices, and make audit-corpus reproduced that number from an independently-written tool. We had both oracles all along and never compared them.

Priority order for the audit phase

  1. dedup_integrate.py — UNAUDITED, and the most dangerous thing in the toolchain. It is the FAIL-CLOSED byte-honesty validator; a silent skip there prints "1813 validated, 0 failed" — a false green from a gate. Nothing downstream can catch it.
  2. jtbl_family_bank.py — UNAUDITED. Three bugs found in it BY HAND this session.
  3. The SELECTION tools — family_hseq.py, wave_targets.py, exemplar_miner.py, worklist.py, build_fuel_manifest.py. A hole here makes work invisible to planning — the worst kind, because you never know to look.
  4. masked_diff.py / match_one.py — the closeness oracle every crack agent trusts.
  5. Then the findings below (the 23% already measured), highest severity first.

Already fixed this session (do not redo)

tool bug commit
gen_harvest_targets.SIG_IN_BODY_RE )\s*{ missed own-line braces → 186 of 1801 (10%) of the callee oracle 6f3441d26
family_remap.extract_unit read an m2c DECLARATION as a DEFINITION → 15 of 35 exemplars phantom cc6220eba
scope_data_externs (NEW, §8d) carried data externs at FILE scope established a global the TU never had 1ab990536
scope_data_externs._body_open_brace own-line-brace only → silently no-op'd on every ANSI draft a0e7ff7f6
progress.py classify() K&R defs invisible (; before {); phantom dedup members; + coverage assertion 329ab4cbe
jtbl_family_bank.revert() / jr_isolate_all config residue → duplicate subseg → "segments out of order"; + fail-loud validation 7e4165676
tools/reconcile_tu.py (NEW) TU-visible decl oracle + fn-ptr parsing + coverage assertion — WRITTEN, VALIDATED, NOT WIRED IN 5b1de7aca

Not yet audited (63 of 82 tools)

The filter is "does it PARSE something, and does it GATE or SELECT work?" — not all 82 (many are dead LLM-tier scripts: api_draft, export_pairs, format_finetune, eval_lora, ab_score, glm_reconcile, …). The ~15 that matter: dedup_integrate · jtbl_family_bank · bank_exemplar · gate_stage · harvest_verify · match_one · masked_diff · family_hseq · family_manifest · wave_targets · exemplar_miner · worklist · build_fuel_manifest · dup_report · difficulty · sig_image · canon_draft_decls · derive_canonical_sigs · census_conflict_callees.


THE 40 MEASURED FINDINGS

(candidates = the over-approximating detector's count; parsed = what the tool actually extracts; real skips = items in the gap the tool SHOULD have parsed. Skeptic corrections are recorded per group — read them: several auditor magnitudes were wrong, and one severity was correctly downgraded to "latent".)

GROUP: data-decls (tools/reconcile_decls.py, tools/canon_sig_reconcile.py)

Headline: The filed "fn-ptr-extern gap" is far bigger than filed: reconcile_decls' oracle is blind to 27 of engine_core.h's 497 data symbols and to 6,384 of the fleet's 39,440 — and worse than silently skipping, it returns an actively WRONG canonical decl for 3,717 symbols; one blind sized-array decl (engine_core.h:2114) is single-handedly blocking func_801387B8, which is still INCLUDE_ASM in 134 TUs.

Scanners measured: 14 Verified CLEAN: Four scanners measured FULL coverage and should NOT be spent on:

  1. canon_sig_reconcile._file_scope_statements (the cpp + brace-depth-0 scanner) — CLEAN. Emitted 949/949 file-scope statements for src/ov_SC01_077/ov_SC01_077.c and 497/497 for src/ov_SC01_000/ov_SC01_000.c with no truncation, no swallowed definitions, and correct handling of the def-vs-initializer brace ambiguity (the v2.1 fix holds). Every hole I found at this layer was in the CLASSIFIER regexes above it (tu_ambient/visible_above), not in the scanner — the fn-ptr statements ARE produced correctly and then dropped on the floor. Do not touch this function.

  2. canon_sig_reconcile def-locator (the re.finditer(r'... \bfn\s*\([^;{]*\)\s*\n?\{') in reconcile()) — CLEAN, and crucially it FAILS LOUD. 10,603 drafts contain a definition of their own fn; 10,600 located (99.97%). The 3 misses (.run/drafts-*-uni/func_80161208.c, all containing the syntactically garbage if ( func_80161208() == 0 a0)) are malformed drafts, and the tool raise ValueError(f'no definition of {fn} found in draft') rather than silently skipping. This is the one scanner in the group that already behaves the way the whole class should — it is the model for the assertions proposed above.

  3. canon_sig_reconcile.parse_sig / split_params / parse_param — CLEAN over the corpus. split_params' depth-tracking correctly handles nested parens (fn-ptr params) and brackets; parse_sig raised on 0 of the 10,600 located definitions.

  4. reconcile_decls.data_access_subs access-rewrite regex (&?)\bD_x\b(\s*\[)? — CLEAN for the three shapes it is reachable for (scalar, array, ptr). Probed directly: v = D_1[i]; -> v = ((u8 *)D_1)[i];, D_2 = 3; -> (*(s32 *)&D_2) = 3;, and the single-pass design does correctly prevent double-wrapping. CAVEAT, not a finding: it would mangle a fn-ptr call-through-array (D_1[i]() -> ((u8 *)D_1)[i]()), but that path is unreachable today because DATA_DECL_LINE_RE never parses a fn-ptr decl in the first place. Fixing DATA_DECL_LINE_RE (finding 3) WILL make it reachable — so the fnptr kind must be added to data_access_subs in the SAME change, or fixing the parser will introduce a new bug here.

[CRITICAL] tools/gen_harvest_targets.py :: DATA_DECL_RE (as consumed by reconcile_decls.canonical_data_map — BOTH the engine_core.h authoritative tier via collect_data_decls AND the fleet plurality tier)

  • candidates 39440 / parsed 33056 / real skips 6384
  • evidence: The regex extern\s+([A-Za-z_][\w\s\*]*?\bD_[0-9A-Fa-f]+\s*(?:\[\s*\])?)\s*; has no ( and no [N] in its character class, so three whole decl shapes are invisible.

AUTHORITATIVE TIER (src/shared/engine_core.h): 497 candidate D_ names, collect_data_decls sees 470, BLIND to 27 — 21 fnptr[], 4 fnptr, 2 sized array: src/shared/engine_core.h:2114 extern s32 D_80127530[4]; \ <- sized array, invisible src/shared/engine_core.h:2554 extern void (*D_80127088)(void); \ <- fn-ptr, invisible src/shared/engine_core.h:3517 extern void (*D_8011DB28)(s32 a0);
src/shared/engine_core.h:21319 extern s32 (*D_801274D0)(s32);
full blind set (27): D_8011DB10 D_8011DB28 D_80127088 D_801274D0 D_80127530 D_80127540 D_8018E208 D_8018E858 D_8018E8A0 D_8018EA0C D_8018EAD8 D_8018ED5C D_8018F2FC D_8018F31C D_8018F804 D_8018F824 D_8018FE80 D_801903DC D_80190CCC D_80190D1C D_8019155C D_801918E4 D_80191EE4 D_801B8A3C D_801E04F0 D_801E051C D_801E0554

FLEET TIER (src/ov_/.c + src/resident/resident.c): 287,801 candidate decl lines, 231,405 parsed, GAP 56,533 (19.6%). Shapes: 54,918 FNPTR, 1,077 SIZED-ARRAY, 404 MULTI-NAME, 134 ASM-ALIAS. 6,384 distinct D_ names get ZERO canonical decl. src/ov_SC01_000/ov_SC01_000.c:30 extern void (*D_8017EEA0[])(void); src/ov_SC01_000/ov_SC01_000.c:322 extern M2C_UNK (*D_8017EBDC)(); src/ov_SC01_000/ov_SC01_000.c:2632 extern s32 D_801A445C, D_801A4460, D_801A4464, D_801A4468; src/ov_SC01_000/ov_SC01_000_jr_8015AE2C.c:3239 extern unsigned char D_801A3E54[8];

END-TO-END, the x134 loss: engine_core.h:2114 declares extern s32 D_80127530[4]; .run/drafts-wave-cn/func_801387B8.c:4 declares extern s32 D_80127530[4]; (and .run/drafts-clean/func_801387B8.c: extern M2C_UNK D_80127530;) canonical_data_map()['D_80127530'] -> None (invisible in BOTH tiers) -> transform()'s if name not in canon: continue leaves the draft AS-IS -> conflicting types vs engine_core.h -> never banks. grep -rl 'INCLUDE_ASM("…", func_801387B8)' src/ | wc -l = 134

  • blast radius: LOST MATCHES + corrupt BUILD. 6 distinct draft functions are hard-blocked by the 27-symbol engine_core.h blind set (65 draft x symbol shape-conflicts the tool provably cannot repair, vs 1,034 it can). The live one is func_801387B8: still INCLUDE_ASM in 134 TUs => a x134 core bank is blocked by a single unparsed [4]. The other 5 (func_80144B9C, func_80145CEC, func_80153800, func_801726D0, func_8013E448, func_801536DC) were routed around by hand and are now banked, which is exactly why this read as a small lever. Forward-looking: 6,384 fleet symbols have no canon at all, so ANY future draft touching one is unreconcilable.
  • fix: In tools/gen_harvest_targets.py, replace DATA_DECL_RE with a shape-aware alternation instead of a single character-class: DATA_DECL_RE = re.compile( r'extern\s+(' r'[A-Za-z_][\w\s*]?(\s*\sD_[0-9A-Fa-f]+\s(?:^])?\s)\s*([^;])' # fn-ptr and fn-ptr array r'|[A-Za-z_][\w\s*]?\bD_[0-9A-Fa-f]+\s*(?:^])?' # scalar/ptr/array, ANY [N] r')\s;') and loosen the array bracket from \[\s*\] to \[[^\]]*\] so sized arrays parse. Then teach collect_data_decls to emit one entry per D_ name on MULTI-NAME lines (split m.group(1) on top-level commas and re-synthesize extern <base> <name><arr>; per name) — today a extern s32 D_a, D_b; line contributes nothing at all. reconcile_decls.parse_data_decl / DATA_DECL_LINE_RE must gain a matching fnptr kind so _norm_type can compare array-vs-fnptr-vs-scalar (a fnptr canon must never be 'repaired' into a scalar).
  • assertion to add: In collect_data_decls: run the permissive detector ^[ \t]*extern\b[^;]*\bD_[0-9A-Fa-f]+[^;]*; (comment-stripped) over the same paths; assert the set of D_ names it yields == the set collect_data_decls returns. On mismatch, fail loud with the unparsed lines: raise SystemExit(f'DATA_DECL_RE blind to {len(gap)} decls: {gap[:10]}'). This single assertion would have caught all 27 engine_core.h holes on day one.

[CRITICAL] tools/reconcile_decls.py :: canonical_data_map (the fleet-MAJORITY oracle design)

  • candidates 249615 / parsed 195438 / real skips 54177
  • evidence: This is not a regex hole — the oracle is asking the WRONG QUESTION, exactly as the decision-log suspected. C's constraint is PER-TRANSLATION-UNIT: each overlay .c is its own TU and may legally declare the same address differently. A fleet-wide plurality therefore cannot be right for every TU, by construction.

Measured with a permissive ground-truth extractor over src/ov_/.c + resident.c: distinct D_ symbols declared in the fleet : 39,447 symbols with >=2 CONFLICTING decl spellings : 14,521 (36.8%) <- one fleet-wide answer is wrong for SOME TU (TU, symbol) decl pairs : 249,615 oracle has NO canon (silent skip) : 19,768 (7.9%) oracle DISAGREES with the TU's own file-scope decl : 54,177 (21.7%) TUs with >=1 broken oracle answer : 678 / 678 (every single TU)

Worse than 'missing': for 3,717 symbols the oracle returns an actively WRONG canon, because the majority real-world form is invisible to DATA_DECL_RE so the vote is won by a MINORITY parseable spelling from an unrelated overlay: src/ov_SC01_000/ov_SC01_000.c:323 TU declares extern s16 (*D_8017EBE0)(); oracle says extern u8 D_8017EBE0[]; (invisible=2, visible=1) src/ov_SC01_000/ov_SC01_000.c TU declares extern void (*D_8017EF98[])(void); oracle says extern char *D_8017EF98; D_8011DB10: engine_core.h says extern void (*D_8011DB10)(s32); but the oracle, blind to it, falls through to the fleet vote and returns extern s32 D_8011DB10; — it CONFIRMS a decl that conflicts with the header the TU includes. (full list: .run/audit/data-decls/wrong_canon.txt)

  • blast radius: corrupt BUILD (and silently caps the recovery pipeline). reconcile_decls WRITES canon[name] into the draft. Where canon disagrees with the target TU, the tool converts a draft that would have compiled into one that cannot — a self-inflicted conflicting types. 21.7% of all (TU,symbol) pairs are exposed; every one of the 678 TUs has at least one. This is the mechanism behind the 780 h_seq rejections (task #12) and it silently bounds the whole family-remap endgame, because the byte-gate cannot distinguish 'reconcile poisoned the decl' from 'the body doesn't match'.
  • fix: Make the oracle TU-scoped, not fleet-scoped. canonical_data_map(extra_src) should become canonical_data_map(tu_path): resolve each D_ name against what THAT TU actually sees, in this precedence — (1) the TU's own file-scope decl (obtained the way canon_sig_reconcile already does it: cpp the pristine TU and brace-depth-0-scan, which correctly expands the DEFINE_ macros, so engine_core.h decls arrive automatically and the macro-body problem disappears); (2) only if the TU declares it nowhere, fall back to the fleet plurality as a guess. Concretely: reuse canon_sig_reconcile.tu_ambient(tu_path)['data'] as the primary source and demote canonical_data_map to the fallback. reconcile_decls' --src-file flag is already the hook — promote it from 'also votes' to 'is authoritative'. NOTE: tu_ambient must first be fixed per the fnptr finding below, or it will hand back the same blind spot.
  • assertion to add: After building canon for a TU, assert every symbol the draft declares that the TU ALSO declares resolves to the TU's spelling: for n in draft_syms & tu_syms: assert _norm(canon[n]) == _norm(tu_decl[n]), f'{n}: oracle {canon[n]!r} conflicts with TU {tu_decl[n]!r}'. Additionally emit a one-line stat per run — canon: N resolved from TU, M guessed from fleet, K UNRESOLVED — and make K>0 a hard warning, so a symbol with no canon can never again be a silent continue.

[HIGH] tools/reconcile_decls.py :: DATA_DECL_LINE_RE (draft-side, drives transform + parse_data_decl)

  • candidates 2500 / parsed 2385 / real skips 115
  • evidence: Same character-class disease as the oracle regex, measured over the live draft corpora (.run/drafts-recov, drafts-wave-cn, drafts-giants, drafts-t5, drafts-full): 2,500 candidate extern ... D_x ...; lines, 2,385 parsed, GAP 115 (4.6%) — 84 FNPTR, 25 MULTI-NAME, 4 SIZED-ARRAY, 2 other. .run/drafts-recov/func_8013E448.c:3 extern s32 (*D_801274D0)(s32); .run/drafts-recov/func_801536DC.c:5 extern void (*D_8011DB28)(s32); .run/drafts-wave-cn/func_80144B9C.c:29 extern s32 D_800A5E88, D_800A5E8C, D_800A5E90; .run/drafts-wave-cn/func_801387B8.c:4 extern s32 D_80127530[4]; .run/drafts-wave-cn/func_801502EC.c:17 extern Ent D_801202A0[96]; Note the skipped names are LITERALLY the same symbols the oracle is blind to (D_801274D0, D_8011DB28, D_80127530) — the two holes compound: the draft's decl is unparseable AND its canon is missing, so nothing can rescue it. MULTI-NAME is the nastiest sub-shape: DATA_DECL_LINE_RE matches nothing on extern s32 D_a, D_b, D_c; (the comma is outside [\w \t]), so ALL THREE symbols are skipped from a single line — and canon_sig_reconcile's _DATA_EXTERN_RE explicitly handles this shape (v2 note #9), so the two tools in the same pipeline disagree about what a data decl is.
  • blast radius: LOST MATCHES. 115 draft decl lines silently pass through untransformed; the draft keeps a decl that may conflict with the TU, and — critically — the ACCESS SITES are never cast either (transform only builds subs for symbols whose decl line matched), so even a correct decl rewrite by another tool would then byte-drift. Directly implicated in the 6 hard-blocked functions above.
  • fix: Rewrite DATA_DECL_LINE_RE as an alternation over the four real shapes and return a 4-valued kind: DATA_DECL_LINE_RE = re.compile( r'^([ \t])extern\s+(?:' r'(?P[A-Za-z_][\w \t*]?)(\s**\s*(?PD_[0-9A-Fa-f]+)\s*(?P^])?\s)\s*((?P[^;]))' r'|(?P[A-Za-z_][\w \t]?)\s*(?P*?)\s*\b(?PD_[0-9A-Fa-f]+)\b\s*(?P^])?' r')\s;[ \t](?:/*[^\n]*/)?[ \t]*$') and add a separate MULTI-NAME pre-pass that splits extern T a, b, c; into one synthetic single-name decl per symbol before the main scan (mirroring canon_sig_reconcile._DATA_EXTERN_RE's piece-splitting, so the two tools agree). parse_data_decl must return kind in {sca,ptr,arr,fnptr} and _norm_type must treat fnptr as its own kind so a fnptr is never 'reconciled' into a scalar.
  • assertion to add: In transform(): count candidate lines with the permissive ^[ \t]*extern\b[^;]*\bD_[0-9A-Fa-f]+[^;]*; detector and compare to the number DATA_DECL_LINE_RE matched. Assert equal; on mismatch print each unparsed line and exit non-zero (SystemExit(f'{path}: {n} data externs UNPARSED: {lines}')). Today the tool's only signal is the UNPARSEABLE decl, left as-is note — which is emitted ONLY when parse_data_decl fails on a line DATA_DECL_LINE_RE already matched, i.e. it is structurally incapable of reporting the lines that never matched at all. That is the whole silent-skip class in one line of code.

[HIGH] tools/canon_sig_reconcile.py :: _reconcile_data — the if not base or '(' in m.group(0): return m.group(0) guard

  • candidates 14332 / parsed 13809 / real skips 523
  • evidence: TWO separate holes, both measured over every .run/drafts* corpus (14,332 candidate data-extern lines, 13,809 fully processed, GAP 523 = 3.6%):

(F1) 436 lines: _DATA_EXTERN_RE does not match fn-ptr data externs AT ALL (its type group is [^;\n()]*? — parens excluded), so the entire data-reconcile contract (strip-if-identical / access-cast-if-different / block-scope-if-invisible) is skipped for 100% of fn-ptr data externs: .run/drafts/func_800CEDFC.c:12 extern void (*D_800D3430[])(void); .run/drafts/func_800CEE40.c:4 extern void (*D_800D3480[])(void); .run/drafts/func_800CEFD0.c:15 extern void (*D_800D3488[])(void);

(F2) 87 lines — the one that is pure accident: the line MATCHES, then the '(' in m.group(0) guard bails because there is a parenthesis IN A TRAILING COMMENT. m.group(0) spans the comment, which _DATA_EXTERN_RE deliberately allows. An ordinary scalar extern is silently abandoned because a human wrote '(' in prose: .run/drafts/func_800D27DC.c:64 extern s32 D_800AE7BC; /* ordering-table base (array of OT-entry pointers, stride 20) / .run/drafts-giants/func_8015126C.c:20 extern s32 D_800AE6A8; / +0x8 == D_800AE6B0 (s32) / .run/drafts-giants/func_8015126C.c:21 extern u8 D_80078E78[]; / +0x49 == D_80078EC1 (u8) / PROVED by calling the real function (.run/audit/data-decls/a7_proof.py): ambient = {'D_800AE7BC': ('u8', is_array=True)}, visible = {'D_800AE7BC'} draft 'extern s32 D_800AE7BC;\nvoid f(void){ g(D_800AE7BC); }' -> 'void f(void){ g(((s32*)D_800AE7BC)); }' (extern stripped, use access-cast: CORRECT) same draft + ' /* ordering-table base (array of pointers) */' -> UNTOUCHED, extern kept verbatim (-> conflicting types vs the u8[] ambient)

  • blast radius: LOST MATCHES + corrupt BUILD. 523 draft data externs are silently passed through unreconciled in the live recovery pipeline. F2's 87 are the cruellest: identical code banks or fails depending on whether a drafter's comment happened to contain a parenthesis, which is invisible to every downstream signal — the byte-gate just reports 'did not compile'.
  • fix: (F2) Compute the guard on the COMMENT-STRIPPED decl text, not on the raw match: change if not base or '(' in m.group(0): return m.group(0) to strip the trailing comment first — the regex already isolates the payload, so test the fields instead of the whole match: decl_txt = m.group(0).split('/*')[0] if not base or '(' in decl_txt: return m.group(0) (F1) Add a fn-ptr branch. Give _DATA_EXTERN_RE an alternation for extern <ret> (*NAME[opt])(args); and give _reconcile_data a 'fnptr' kind so it can do the same three-way decision (identical -> strip; different -> access-cast; not visible -> block-scope move). A fn-ptr extern must NEVER be silently emitted verbatim into a TU that declares the symbol differently.
  • assertion to add: In _reconcile_data, before returning: re-scan the ORIGINAL draft with the permissive ^[ \t]*extern\b[^;]*\bD_[0-9A-Fa-f]+[^;]*; detector and assert every candidate line was either consumed (stripped/moved/cast-registered) or explicitly recorded in a skipped=[] list with a REASON. Then assert not skipped, f'{fn}: {len(skipped)} data externs silently passed through: {skipped}'. The bail-out branch must never be reachable without leaving a trace.

[HIGH] tools/canon_sig_reconcile.py :: tu_ambient + visible_above (the file-scope statement CLASSIFIER)

  • candidates 25 / parsed 0 / real skips 25
  • evidence: file_scope_statements (the cpp + brace-depth-0 scanner) is CLEAN — it correctly emits every file-scope statement. The bug is one layer up: the two classifier regexes in tu_ambient and visible_above (^(?:extern\s+)?([A-Za-z_][\w \*]*?)\s*\b([A-Za-z_]\w*(\s*\[[^\]]*\])?...)\s*;$) have no ( in their type class, so a file-scope fn-ptr DATA decl falls into NO bucket — not funcs, not data, not typedefs. It is invisible to the ambient map AND absent from visible. src/ov_SC01_077/ov_SC01_077.c : 949 file-scope stmts, 569 names classified, 18 stmts naming a D/func_ landed in NO bucket — all fn-ptr: extern s32 (*D_801274D0)(s32); extern void (*D_80187ED0[])(void); extern void (*D_80187F10[])(void); (+15 more) src/ov_SC01_000/ov_SC01_000.c : 497 stmts, 279 classified, 7 unclassified: extern void (*D_8017EEA0[])(void); extern M2C_UNK (*D_8017EBDC)(); extern s16 (*D_8017EBE0)();

The CONSEQUENCE is not a no-op, it is an active miscompile-inducing branch. Because the name is missing from visible, _reconcile_data takes if name not in visible: moved.append(...) and MOVES the draft's (wrong-typed) extern to BLOCK SCOPE inside the function body — while the file-scope fn-ptr decl is still in scope. PROVED against the real cross-compiler (.run/audit/data-decls/blk.c): typedef int s32; extern s32 (*D_801274D0)(s32); void f(void){ extern s32 D_801274D0; g(D_801274D0); } -> blk.c:3:26: error: conflicting types for 'D_801274D0'; have 's32' The block-scope idiom, which is the tool's safety valve, is precisely what detonates here.

  • blast radius: corrupt BUILD. Every draft that touches any of the ~25 file-scope fn-ptr data symbols per TU gets a guaranteed-uncompilable block-scope redeclaration. Fleet-wide there are 54,918 fn-ptr extern lines across 678 TUs, so this branch is reachable from a large fraction of the recovery corpus. It also means visible_above UNDER-reports the TU's namespace, which biases every downstream 'not visible -> move to block scope' decision the tool makes.
  • fix: Add a fn-ptr branch to BOTH classifiers, ordered BEFORE the generic data-decl regex (a fn-ptr decl otherwise looks like a func decl to the func regex, or like nothing): m = re.match(r'^(?:extern\s+)?([A-Za-z_][\w *]?)(\s*\s*([A-Za-z_]\w*)\s*(^])?\s)\s*(([^;]))\s;$', stn) if m: data[m.group(2)] = (f'{m.group(1).strip()} (*)({m.group(4)})', m.group(3) is not None) continue and in visible_above add the same pattern to names.add(...). Give tu_ambient's data map a third element (kind='fnptr') so _reconcile_data can compare like with like rather than treating a fn-ptr as a scalar of type ''.
  • assertion to add: At the end of tu_ambient(), assert the classifier is TOTAL over the statements the scanner produced: for every file-scope statement that declares an identifier (permissive detector: mentions a D_/func_/named symbol and ends in ';' or '{'), assert that identifier landed in exactly one of funcs/data/typedefs. unbucketed = [st for st in stmts if names_in(st) and not (names_in(st) & classified)]; assert not unbucketed, f'{tu_path}: {len(unbucketed)} file-scope decls in NO bucket: {unbucketed[:5]}'. A classifier that silently drops a declaration is strictly worse than one that crashes.

[MEDIUM] tools/reconcile_decls.py :: _norm_type (the "already compatible -> return []" gate)

  • candidates 197 / parsed 171 / real skips 26
  • evidence: _norm_type collapses s32|u32|int|unsigned int|unsigned|long|unsigned long|u_long all to the single token 'int' (line 110), so it declares a draft/canon pair COMPATIBLE across a SIGNEDNESS change and returns [] — no decl rewrite, no cast. gcc does not agree. Verified with the real toolchain: typedef int s32; typedef unsigned int u32; extern s32 D_1; extern u32 D_1; -> error: conflicting types for 'D_1'; have 'u32' {aka 'unsigned int'} Measured over the draft corpora: 197 (draft, canon) pairs where _norm_type says 'compatible' but the spellings differ; 26 of them differ in SIGNEDNESS and are therefore hard gcc errors the tool refuses to repair: .run/drafts-ov077/func_8016F2C8.c draft 'extern u32 D_80126D50;' vs canon 'extern s32 D_80126D50;' .run/drafts-ov077/func_8016F30C.c draft 'extern u32 D_80126D50;' vs canon 'extern s32 D_80126D50;' .run/drafts-ov077/func_8016F350.c draft 'extern u32 D_80126D50;' vs canon 'extern s32 D_80126D50;' .run/drafts-T6-fail/func_801718AC.c draft 'extern u32 D_80126D50;' vs canon 'extern s32 D_80126D50;' The collapse is right about CODEGEN (same width, same load opcode, so the cast really is byte-neutral) and wrong about the C FRONT END (which rejects the redeclaration before codegen is ever reached). The tool skips the repair for exactly the reason the repair is unnecessary — and the compile then fails anyway.
  • blast radius: LOST MATCHES. 26 draft x symbol pairs currently in the corpus compile-fail with conflicting types and the tool, by design, declines to touch them. Note func_801718AC's draft is in a *-fail directory. The failure mode is maximally confusing to a human: the tool reports 0 reconciles, the build errors on a decl the tool looked at and consciously approved.
  • fix: Split the equivalence into two distinct predicates. Keep _norm_type's int-family collapse ONLY for the question 'is the access cast byte-neutral' (it is). Add a separate, STRICT predicate for the question 'will gcc accept the draft's decl alongside the canonical one' — no signedness collapse, no width collapse; s32!=u32, int!=unsigned. In transform(): rewrite the decl to canonical whenever the STRICT predicate says they conflict, and emit the access cast whenever _norm_type says the types differ in a codegen-relevant way. Today one predicate is doing both jobs and gets one of them wrong. Concretely, change the guard at line 133 from if _norm_type(...) == _norm_type(...): return [] to if _strict_same(idecl, cdecl): return [] and keep _norm_type only to decide whether subs needs to be non-empty.
  • assertion to add: Whenever data_access_subs returns [] ('already compatible'), assert the two decl strings are genuinely interchangeable to the C front end: assert _strict_same(idecl, cdecl), f'{name}: claimed compatible but {idecl!r} vs {cdecl!r} differ in signedness/width — gcc will reject'. Cheaper still, add a one-off CI check that compiles extern <canon>; extern <draft>; for every pair the tool declares compatible, and fails if gcc does.

[MEDIUM] tools/canon_sig_reconcile.py :: _uniquify_draft_types (the fn-ptr / alias typedef arm)

  • candidates 36 / parsed 0 / real skips 36
  • evidence: The 'simple/alias/fnptr/array typedefs' regex ^[ \t]*typedef\s+[^;{}\n]*?\b([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*; requires an IDENTIFIER immediately before the ; (modulo an array suffix). A fn-ptr typedef ends in ), so it never matches — despite the docstring explicitly claiming 'simple/alias/fnptr/array typedefs'. Verified by calling the real function: csr._uniquify_draft_types('typedef int (*Fn)(void);', {}, 'func_80000000') -> 'typedef int (*Fn)(void);' (NOT stripped, NOT uniquified — the name Fn is untouched) csr._uniquify_draft_types('typedef struct { int a; } S;', {}, 'func_80000000') -> 'typedef struct { int a; } S_80000000;' (the aggregate arm works correctly) Corpus: 36 fn-ptr typedef lines across the draft dirs, 7 distinct names — ActorFn, DispatchFn, FuncPtr, Handler, VoidFn, code, code_fn: .run/drafts-recover-a-cn-cast/func_80131CA8.c:4 typedef void (*code)(int); .run/drafts-recover-a-cn/func_80131CA8.c:4 typedef void (*code)(int); Because the name is neither stripped (when identical to an ambient type) nor uniquified (when colliding), a fn-ptr typedef is the one type-name class that can still collide — which is exactly the redefinition of ... failure the T7-M1 uniquify pass was built to eliminate. Generic names like code, Handler, FuncPtr are precisely the ones two independently-drafted exemplars landing in ONE split will both invent.
  • blast radius: corrupt BUILD, bounded. 36 draft lines / 7 names. Only bites when two drafts sharing a split both define the same fn-ptr typedef name with different signatures, or when one collides with an ambient engine_types.h name. Low frequency today, but it is a latent landmine that scales with the family-remap endgame (more exemplars per split = more collisions), and the docstring asserts a guarantee the code does not provide.
  • fix: Add a third re.sub arm to uniquify_draft_types, before the generic alias arm, matching the fn-ptr shape and extracting the name from inside the parens: draft = re.sub(r'^[ \t]typedef\s+[^;{}\n]?(\s**\s*([A-Za-z]\w*)\s*)\s*([^;])\s;[ \t]*\n?', strip_or_mark, draft, flags=re.M) strip_or_mark already does the right thing with the captured name (strip if identical to ambient, else add to the rename set). No other change needed — the trailing for name in rename: draft = re.sub(...) loop then uniquifies it everywhere.
  • assertion to add: After _uniquify_draft_types, assert no typedef NAME survives that is neither ambient-identical nor suffixed: leftover = [n for n in re.findall(r'typedef[^;{}]*?(?:\(\s*\*\s*)?([A-Za-z_]\w*)\)?\s*(?:\([^;]*\))?\s*;', draft) if not n.endswith(suffix) and n not in ambient_typedefs]; assert not leftover, f'{fn}: typedef names neither stripped nor uniquified: {leftover}'. This makes the docstring's guarantee machine-checked.

[LOW] tools/canon_sig_reconcile.py :: _FN_EXTERN_RE (callee externs)

  • candidates 22481 / parsed 22470 / real skips 11
  • evidence: Very nearly clean — 22,470 of 22,481 candidate callee-extern lines parse (99.95%). The only gap is a WRAPPED PARAMETER LIST: the regex's param group is ([^;\n]*), which forbids a newline, so an extern whose params wrap across lines never matches and is therefore never stripped, never block-scope-moved, and never call-site cast — it is emitted verbatim at TU file scope. .run/drafts-T6b/func_80157158.c:1 extern void func_801571C4(s32 a0, u16 a1, u16 a2, s32 a3, .run/drafts-recov/func_80156670.c:12 extern void func_80157158(s32 a0, u16 a1, u16 a2, s32 a3, .run/drafts-ov077/func_801466B4.c:1 extern void func_801466F0(s32 a0, s32 a1, s32 a2, s32 a3, .run/drafts-wave/func_8015F118.c:11 extern void func_801466F0(s32 a0, s32 a1, s32 a2, s32 a3, All 11 are 7+-arg callees, i.e. the ones a drafter is most likely to line-wrap. If the TU's canonical sig for that callee differs, the verbatim extern is a conflicting types.
  • blast radius: LOST MATCHES, small. 11 draft lines across ~6 functions (func_801571C4, func_80157158, func_801466F0). Real but rare — it only bites on wide-signature callees. Worth fixing in the same pass since it is a two-character change.
  • fix: Change the param group in _FN_EXTERN_RE from ([^;\n]*) to ([^;]*) and add re.S is NOT needed (the class already spans lines once \n is allowed); the trailing [ \t]*\n anchor still terminates the match at the real end of the decl: FN_EXTERN_RE = re.compile( r'^[ \t]extern\s+([^;\n]?)\b([A-Za-z]\w*)\s*(([^;]))\s;[ \t](?:/*[^\n]*/)?[ \t]*\n', re.M) (only the third group loses its \n exclusion — the return-type group must keep \n excluded so the match cannot start on a previous line).
  • assertion to add: In _reconcile_callees: count candidate lines with a permissive ^[ \t]*extern\b[^;]*\w+\s*\( detector (excluding fn-ptr DATA decls) and assert every one was visited. assert visited == candidates, f'{fn}: {candidates-visited} callee externs never parsed'.

[LOW] tools/canon_sig_reconcile.py :: SCALAR_TYPEDEF_RE

  • candidates 4774 / parsed 4719 / real skips 55
  • evidence: 4,774 candidate scalar-typedef lines in the draft corpus, 4,719 stripped, 55 skipped. The regex is anchored ^[ \t]*typedef\b[^;]*\b(u8|...)[ \t]*;[ \t]*\n — it requires the ; to be immediately followed by end-of-line, so MULTIPLE typedefs on one line are all missed (the [^;]* cannot cross the first ;, and the \n anchor then fails): .run/drafts-p18-3b/func_8012C2D0_v15a.c:1 typedef unsigned char u8; typedef unsigned short u16; typedef int s32; .run/drafts-p18-3b/func_8012C2D0_v15b.c:1 (same) .run/drafts-p18-3b/_tc.c:1 (same) All 55 are confined to one stale drafter batch (.run/drafts-p18-3b) whose emitter packed typedefs onto one line; current drafters emit one per line. Left unstripped, these are redefinition of typedef 'u8' errors against common.h (a hard error in C89/gcc-2.7.2).
  • blast radius: LOST MATCHES, contained. 55 lines, all in a single legacy corpus (.run/drafts-p18-3b) that is not on the live path. No current drafter produces this shape. Fix is cheap insurance against a future drafter regressing to one-line typedefs; it is not blocking anything today.
  • fix: Drop the end-of-line anchor and let the regex strip each typedef independently: SCALAR_TYPEDEF_RE = re.compile( r'[ \t]typedef\b[^;{}\n]\b(u8|u16|u32|u64|s8|s16|s32|s64|f32|f64|' r'M2C_UNK|M2C_UNK8|M2C_UNK16|M2C_UNK32|M2C_UNK64)[ \t];[ \t](?:\n)?') (the {} exclusion prevents it from eating an aggregate typedef body; the newline becomes optional so a mid-line typedef is stripped in place).
  • assertion to add: After the SCALAR_TYPEDEF_RE.sub pass, assert none survive: assert not re.search(r'typedef\b[^;{}\n]*\b(u8|u16|u32|s8|s16|s32|f32|M2C_UNK\w*)[ \t]*;', draft), f'{fn}: scalar typedef survived the strip pass'. gcc will reject it anyway; failing here names the cause instead of making a human read a redefinition error.

GROUP: callee-sigs

Headline: The DATA_DECL_RE char class [\w\s\*] cannot contain (, so the canonical-data oracle is 100% blind to function-pointer / jump-table extern void (*D_X[])(void); decls — and for 13 of the 25 that engine_core.h declares AUTHORITATIVELY it doesn't just miss them, it hands back a WRONG scalar canonical (extern s32 D_8018E858;) elected by a fleet plurality vote in which every correctly-shaped decl is invisible — guaranteeing conflicting types against the very macro it was supposed to conform to, on exactly the jtbl symbols the in-flight jtbl_family_bank is trying to bank.

Scanners measured: 10 Verified CLEAN: VERIFIED CLEAN — do not spend time here: (1) gen_harvest_targets :: SIG_IN_BODY_RE / collect_define_sigs — M=1801 #define DEFINE_func_ candidates, N=1801 parsed, GAP=0. The Phase-26 [\s\\]* fix is COMPLETE. I also audited sig QUALITY, not just count: all 1801 round-trip cleanly through split_sig_string→parse_sig, 0 failures, 0 control-keyword leaks (if (/while ( never captured as a signature). (2) cast_call_sites :: DECL_LINE_RE — 26,261 strict-prototype candidates across all 162 real draft dirs in .run/, 26,261 parsed, 0 real skips. Over src/**/*.c: 779,215 candidates, 2 non-matches, and both are DEFINITION lines it correctly declines to rewrite. Genuinely clean — the (?:/\*[^\n]*\*/)? trailing-comment allowance covers every form m2c and the drafters actually emit (no //-comment or multi-line prototype exists in the corpus). (3) cast_call_sites :: parse_sig / split_sig_string / norm_sig (+ sig_unify's split_top_commas / param_type) — 1801/1801 canonical sigs parse without exception, including function-pointer params. (4) gen_harvest_targets :: INCLUDE_ASM_RE — the regex itself is clean (13/13 on the file it is given; the fault in F4 is the CORPUS it is pointed at, not the pattern).

[CRITICAL] tools/gen_harvest_targets.py :: DATA_DECL_RE / collect_data_decls

  • candidates 38529 / parsed 33312 / real skips 5219
  • evidence: Candidate detector = every line containing extern + a D_[0-9A-Fa-f]+ token ending in ;. Corpus-wide (src/**/*.c + engine_core.h): M=38529 sites, N=33312 parsed, GAP=5219 (13.5%). I classified 100% of the gap — there are NO justified exclusions, all 5219 are real: (a) function-pointer / jump-table arrays, 4614 sites — extern void (*D_8018E858[])(void); src/shared/engine_core.h:24818 ; extern void (*D_8011DB10)(s32); engine_core.h:23629 ; extern void (*D_80187ED0[])(void); src/ov_SC01_077/ov_SC01_077.c:830 ; extern void (*D_800D3430[])(void); src/resident/resident.c:12. (b) sized arrays, 249 sites — extern s32 D_80127530[4]; engine_core.h:2114 (the regex's (?:\[\s*\])? only accepts EMPTY brackets). (c) multi-declarator lines, 356 sites — extern short D_800AE710, D_800AE712; src/800.c:1119 (the , is outside [\w\s\*], so the WHOLE line is dropped, not just the 2nd declarator). Root cause: the char class [\w\s\*] between extern and D_X cannot contain (, ), , or a non-empty [N]. THE KILLER: this is not merely a missing entry. reconcile_decls.canonical_data_map() is built entirely on this scanner (tools/reconcile_decls.py:67 _ght.collect_data_decls([ec]) = the AUTHORITATIVE tier; :78 _ght.DATA_DECL_RE.finditer = the fleet plurality tier). engine_core.h declares 25 D_ symbols as function pointers inside DEFINE macros; the oracle sees ZERO of them, so the authoritative tier is silently empty and all 25 fall through to the plurality vote. 12 come out MISSING; 13 come out WRONG: D_8018E858 truth extern void (*D_8018E858[])(void); (engine_core.h:24818, dereferenced as D_8018E858[*(u16*)(a0+2)](); at :24820) -> oracle returns extern s32 D_8018E858;. I verified the vote: 12 overlay files carry the scalar mis-decl (e.g. src/ov_SC03_126/ov_SC03_126_jr_80178D40.c:1245) and 0 carry the correct func-ptr form — the plurality is decided ENTIRELY by wrong-shaped decls because every right-shaped one is invisible to the regex. Also D_801903DC -> extern struct packed_word D_801903DC;, D_8018EAD8 -> extern char D_8018EAD8[];, D_8018F824 -> extern int D_8018F824;. Sized arrays D_80127530/D_80127540 -> oracle None.
  • blast radius: Corrupts the BUILD and loses MATCHES. reconcile_decls hands a draft extern s32 D_8018E858;; the same TU also instantiates the engine_core.h DEFINE macro that declares it void (*[])(void) -> in-TU conflicting types for D_8018E858 -> the draft is discarded as 'won't compile', and the reconcile pass reports nothing to fix (the identical signature of the SIG_IN_BODY_RE bug). Affected: 25 authoritative func-ptr symbols (13 actively mis-canonicalized, 12 absent) + 2 sized arrays + 5219 decl sites fleet-wide. These D_ symbols ARE the per-overlay jump-table dispatch arrays — i.e. precisely the class jtbl_family_bank (the in-flight recovery stage) exists to bank, and a strong candidate for a chunk of the open '780 h_seq rejections' (task #12). Worse than a silent skip: a silently WRONG oracle that looks authoritative.
  • fix: Rewrite DATA_DECL_RE to admit the fn-ptr and sized-array forms, and pre-split multi-declarator lines. TEXT (do not apply — fleet build in flight):

DATA_DECL_RE = re.compile( r'extern\s+(' r'[A-Za-z_][\w\s*]?(\s*\sD_[0-9A-Fa-f]+\s(?:^])?\s)\s*([^;])' # fn-ptr / jtbl array r'|[A-Za-z_][\w\s*]?\bD_[0-9A-Fa-f]+\s*(?:^])?' # scalar / sized array r')\s;')

and in collect_data_decls, before matching, expand extern T A, B, C; into one synthetic decl per declarator (split on top-level commas). MIRROR THE SAME FIX in tools/reconcile_decls.py:51 DATA_DECL_LINE_RE (see the separate spillover finding) or the repair is only half-done — canonical_data_map reads BOTH.

  • assertion to add: In collect_data_decls: build cand = {every D_[0-9A-Fa-f]+ on a line matching ^\s*extern\b.*;} and assert set(out) == cand, else raise with the unparsed lines printed. Additionally assert that every D_ symbol declared inside an engine_core.h DEFINE macro appears in the AUTHORITATIVE tier of canonical_data_map — an empty authoritative tier for a symbol that engine_core.h demonstrably declares is a hard failure, never a silent fall-through to the plurality vote.

[CRITICAL] tools/reconcile_decls.py :: DATA_DECL_LINE_RE (SPILLOVER — outside this group, but the F1 fix is void without it)

  • candidates 300617 / parsed 243283 / real skips 57334
  • evidence: tools/reconcile_decls.py:51-53 carries an INDEPENDENT copy of the same disease: r'^([ \t]*)extern\s+([A-Za-z_][\w \t]*?)\s*(\*?)\s*\b(D_[0-9A-Fa-f]+)\b\s*(\[\s*\])?\s*;' — the prefix class [\w \t] again has no (, and the bracket group again only accepts EMPTY []. Candidate = every line matching ^\s*extern\b.*D_XXXX.*;: M=300617 decl lines fleet-wide, N=243283 parsed, GAP=57334 (19.1%) — same three classes (fn-ptr/jtbl, sized array, multi-declarator). canonical_data_map reads BOTH scanners (_ght.collect_data_decls for the authoritative tier, and this one is used by the reconcile/rewrite side), so patching DATA_DECL_RE alone produces an oracle that KNOWS the correct func-ptr canonical but a rewriter that still cannot recognise the decl line it must replace.
  • blast radius: Corrupts the BUILD. This is the rewrite half of the F1 loop: even with a correct canonical in hand, reconcile_decls cannot locate/replace a draft's extern void (*D_X[])(void); line, so the reconcile silently no-ops on exactly the jtbl symbols. Gates the same jtbl_family_bank / 780-h_seq-rejection work as F1.
  • fix: Mirror the F1 fix here:

DATA_DECL_LINE_RE = re.compile( r'^([ \t])extern\s+(?:' r'([A-Za-z_][\w \t*]?)\s*(\s**\s*\b(D_[0-9A-Fa-f]+)\b\s*(^])?\s)\s*(([^;]))' # fn-ptr / jtbl r'|([A-Za-z_][\w \t]?)\s*(*?)\s*\b(D_[0-9A-Fa-f]+)\b\s*(^])?' # scalar / sized array r')\s;[ \t](?:/*[^\n]*/)?[ \t]*$')

and handle multi-declarator lines by expansion. Fix F1 and this in the SAME change — they are one loop.

  • assertion to add: assert the set of D_ symbols this regex extracts from a file == the set of D_ symbols on lines matching ^\s*extern\b.*; in that file, else fail loud. Cross-assert against canonical_data_map: every symbol in the canonical map must be RECOGNISABLE by DATA_DECL_LINE_RE, otherwise the reconciler can produce a canonical it can never apply.

[HIGH] tools/gen_harvest_targets.py :: EXTERN_DECL_RE / collect_extern_sigs

  • candidates 2723 / parsed 2706 / real skips 24
  • evidence: EXTERN_DECL_RE requires the literal keyword extern. But m2c and several hand-written/reconciled decls emit BARE prototypes — valid C, fully TU-visible, and completely invisible to the canonical-callee oracle. Measured PER-TU (the unit that matters: cast_call_sites takes --src-file) across the 9 ov_SC01_077 TUs = 24 distinct bare-proto decl sites skipped; 18 of those also escape the define/inline rescue in cast_call_sites.canonical_map and are therefore TRULY invisible to the oracle. Evidence: M2C_UNK func_80178D40(s32, s32); /* extern */ src/ov_SC01_077/ov_SC01_077_jr_801734BC.c:3508 (the x134 core just banked); void func_8012A418(void); /* extern (Phase-18 reconciled) */ src/ov_SC01_077/ov_SC01_077_jr_80178D40.c:3823; void func_8013B83C(s32 a0, s32 a1, s32 a2); and void func_8013BD74(void *a0, s32 a1); src/ov_SC01_077/ov_SC01_077_o0.c:22-23; void func_8012A048(void *a0, s32 a1, u8 a2); src/ov_SC01_077/ov_SC01_077_a.c:580; void func_80154C24(s32 a0, s32 *a1, s32 *a2); src/ov_SC01_077/ov_SC01_077_after.c:3057; s32 func_80019198(); src/ov_SC01_077/ov_SC01_077.c:46; s32 func_800D02D0(u8); src/ov_SC01_077/ov_SC01_077_after.c:3120. (Corpus-wide single-pass over engine_core.h + all 134 overlays: M=2723 addrs, N=2706, GAP=17 — that number UNDERSTATES the harm because it lets an extern in a DIFFERENT overlay's file rescue an addr that is bare in the TU actually being compiled; the TU-accurate figure is the 24/18 above.) Note ov_SC01_077 is the only overlay with a nonzero lost set — the other 133 are template-instantiated and carry only extern forms — so the whole hole sits squarely on the one overlay every draft is written against.
  • blast radius: Loses MATCHES. cast_call_sites.transform: if addr not in canon: continue # pure stub callee: draft's decl is the only one -> no conflict, leave it (don't cast). For these 18 the premise is FALSE — the TU does declare them — so a draft that declares the callee with its own byte-matching signature gets NO decl rewrite and NO call-site cast, hits in-TU conflicting types, and is thrown away as 'won't compile'. That is the EXACT §20 failure class cast_call_sites was built to eliminate, silently re-opened for 18 callees. The same collector also feeds gen_harvest_targets (emits status='stub', signature=None -> the drafting agent guesses), sig_unify, canon_draft_decls and census_conflict_callees — 5 consumers, one hole.
  • fix: Make extern optional and anchor at line start (the ^ anchor is what stops it eating call statements once extern no longer gates it):

EXTERN_DECL_RE = re.compile( r'^[ \t](?:extern[ \t]+)?([A-Za-z_][\w \t*]?\bfunc_[0-9A-Fa-f]+\s*([^;{]))\s;', re.M)

Keep the existing setdefault first-wins precedence; an explicit extern and a bare prototype for the same addr are the same declaration in C.

  • assertion to add: In collect_extern_sigs: cand = every line matching ^\s*(?:extern\s+)?[A-Za-z_][\w \t\*]*\bfunc_[0-9A-Fa-f]{8}\s*\([^;{]*\)\s*; (a prototype: a type run, then func_X, then a param list, then ;). assert set(cand_addrs) <= set(sigs), else fail loud listing the unparsed file:line — a declaration the TU can see but the oracle cannot is never acceptable.

[MEDIUM] tools/gen_harvest_targets.py :: collect_stubs + main() corpus scoping (main .c only, no split-file awareness)

  • candidates 270 / parsed 13 / real skips 257
  • evidence: collect_stubs (and every other collector called from main(), and the default asm_dir) is hardcoded to src/<overlay>/<overlay>.c and asm/<overlay>/nonmatchings/<overlay>/. ov_SC01_077 has 270 INCLUDE_ASM stubs spread over 8 .c files — ov_SC01_077.c:13, _a.c:51, _after.c:58, _jr_8015AE2C.c:68, _jr_801734BC.c:25, _jr_80178D40.c:26, _jr_80182268.c:22, _o0.c:7 — and the asm tree mirrors that split (asm/ov_SC01_077/nonmatchings/{ov_SC01_077:13, ov_SC01_077_a:51, ov_SC01_077_after:56, ov_SC01_077_jr_8015AE2C:68, ...}). The tool sees 13 of 270 = 4.8% of the overlay's remaining work and reports success. The --asm-dir flag re-points only the .s scan, NOT the .c corpus, so pointing it at a split subdir still yields an EMPTY manifest — those addrs are filtered out at if addr not in stubs: continue. cast_call_sites already fixed this class at the consumer end (it grew --src-file precisely because 'the TU sees THAT file's local decls, NOT the main .c's'); gen_harvest_targets never got the same treatment.
  • blast radius: Corrupts the METRICS / target selection, not the build. The leverage-ranked target manifest silently enumerates 4.8% of the candidate pool, so 'we've worked the high-leverage tail' is unfounded for 257 functions. Mitigating: the manifest path looks semi-dormant (.run/t6_targets.json last written Jun 21; no workflow or Makefile target invokes the CLI — the live surface of this module is its collect_* functions imported as a library by 5 tools). census_conflict_callees.py imports collect_stubs and inherits the same 13/270 blindness.
  • fix: Add --src-file (mirroring cast_call_sites) or, better, glob the whole TU family: c_paths = sorted(glob.glob(f'src/{ov}/{ov}*.c')), union collect_stubs over all of them, and derive each target's asm subdir from the .c file its INCLUDE_ASM lives in (the INCLUDE_ASM first argument already names it: INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80174684)) instead of assuming asm/<ov>/nonmatchings/<ov>.
  • assertion to add: assert len(stubs) == sum of INCLUDE_ASM occurrences over glob(f'src/{ov}/{ov}*.c'), else fail loud naming the .c files that were never scanned. And in the .s scan: assert os.path.exists(sp) for every target, else fail loud rather than silently emitting data=[] — a missing .s is the tool telling you it is pointed at the wrong subdir.

[MEDIUM] tools/gen_harvest_targets.py :: INLINE_DEF_RE / collect_inline_sigs

  • candidates 693 / parsed 690 / real skips 3
  • evidence: Candidate detector = brace-balanced scan for any func_XXXXXXXX(...) whose matching ) is followed (after whitespace/newlines) by {, preceded by a type token rather than a keyword/operator. Across the 9 ov_SC01_077 TUs: M=693, N=690, GAP=3 — and all 3 are REAL skips, no false positives. Two shapes defeat the ^([A-Za-z_][\w \t\*]*?... column-0 + type-only-prefix anchor: (a) an INDENTED file-scope definition — s32 func_80174650(s32 _arg0) followed by a column-0 {, src/ov_SC01_077/ov_SC01_077_jr_801734BC.c:2822. This is a template body replicated fleet-wide: 138 occurrences corpus-wide (src/ov_SC01_000/ov_SC01_000_jr_801734BC.c:2632, ov_SC01_001:2630, ov_SC01_004:2630, ov_SC01_005:2631, ...). (b) a definition preceded on the SAME LINE by extern decls — extern void func_80150170(void *a0); extern s32 func_8014FE60(void *a0); void func_8014FDF4(struct S8014FDF4 *a0) { ... } src/ov_SC01_077/ov_SC01_077_after.c:2151, and the same shape at :3299 for func_80155FF8. The ; and ( are outside the [\w \t\*] prefix class, so the line matches nowhere. 2 occurrences.
  • blast radius: Corrupts the sig PRECEDENCE (a latent match-loss). canonical_map ranks inline-def > DEFINE macro > extern, because the in-TU DEFINITION is the authoritative signature. A skipped inline def means a looser/stale extern (or nothing) silently wins. Today the damage is contained: func_80174650 and func_80155FF8 happen to also carry a matching extern, so they are rescued; func_8014FDF4 is not — no extern, no DEFINE — so its true signature void func_8014FDF4(struct S8014FDF4 *a0) is simply absent from the oracle, and any draft calling it guesses and conflicts. The 138-site indented form is one symbol replicated, so it is 1 symbol at risk, not 138 — but the shape will recur the moment a drafter indents a definition.
  • fix: Re-admit a leading-whitespace start and a start-after-a-semicolon, while keeping the exclusion of if (/= /, (which never sit directly before a type run):

INLINE_DEF_RE = re.compile( r'(?:^|;)[ \t]([A-Za-z_][\w \t*]?\bfunc_[0-9A-Fa-f]+\s*([^;{]))\s{', re.M)

(The (?:^|;) alternation is the whole fix: ^ + optional indent covers shape (a), ; covers shape (b). The Phase-19 bug this anchor was added to prevent — matching if (func_X(...) == 0x2000 && ...) { — stays excluded because if is followed by (, which the [\w \t\*] run cannot cross.)

  • assertion to add: In collect_inline_sigs: cand = the brace-balanced scan above (find every func_X( whose matching ) is followed by { and whose preceding non-space token is not one of {if,for,while,switch,return,else,do,sizeof} and does not end in an operator). assert set(cand) == set(sigs), else fail loud printing the unparsed file:line.

[LOW] tools/gen_harvest_targets.py :: ASM_DATA_REF_RE

  • candidates 3035 / parsed 2883 / real skips 24
  • evidence: Corpus = the 267 per-fn .s under asm/ov_SC01_077/nonmatchings/**. Candidate = every %hi(...) / %lo(...) operand (the only way a .s names a global). M=3035 operands, of which \b(D_[0-9A-Fa-f]+)\b parses N=2883. GAP=152, classified: 124 = jtbl_XXXXXXXX -> JUSTIFIED EXCLUSION (compiler-generated switch tables; a C draft regenerates its own, there is no C declaration to resolve — the tool is right to ignore them). 14 = func_XXXXXXXX -> REAL SKIP: an address-TAKEN function, not a jal, so it is in neither rec['calls'] (no callees entry) nor the D_ data list — the draft must declare it to take its address with ZERO signature guidance from the manifest. Evidence: asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC/func_80172C50.s -> %hi(func_80172CA0); .../func_8017F290.s -> %hi(func_8017E1D4) and %hi(func_8017E748); .../func_8017D98C.s -> %hi(func_8017DA08); .../func_80173A60.s -> %hi(func_80173B4C). 10 = renamed non-D_ globals -> REAL SKIP: currentLocationId x6, cdFileLocTable x4 — invisible to ASM_DATA_REF_RE (D_-prefix only) AND to DATA_DECL_RE, so as the project renames symbols away from D_ this hole grows monotonically.
  • blast radius: Loses MATCHES, small and bounded today. 14 address-taken callees + 2 renamed globals get decl: None / no callee entry -> the drafting agent guesses the declaration -> conflicting types at the whole-binary gate. Note this rides in the semi-dormant main() manifest path (see F4), so it is a latent rather than in-flight loss; the renamed-global class is the one that will grow.
  • fix: Scan only %hi/%lo operands rather than the whole .s text, broaden the symbol pattern, and route the classes:

ASM_SYM_RE = re.compile(r'%(?:hi|lo)(\s*([A-Za-z_]\w*)') ... for each sym: if sym.startswith('jtbl_'): continue # compiler-generated; no C decl (documented exclusion) elif sym.startswith('func_'): -> resolve through defined_sigs/extern_sigs and append to callees with status/signature (address-taken, not a jal) else: -> resolve through data_decls (covers D_XXXX and renamed globals alike)

  • assertion to add: assert every %hi/%lo operand in the target's .s is accounted for in exactly one of {callees, data, the explicit jtbl_ exclusion list}, else fail loud naming the operand — the exclusion set must be an explicit named list, never 'whatever the regex happened not to match'.

GROUP: recovery-rest

Headline: sig_unify's DATA_DECL_RE cannot parse function-pointer or sized-array data externs — 44 of the 646 D_ symbols in the ov_SC01_077 canon corpus (including the engine's callback slots D_80127088, D_8011DB28, D_801274D0) never enter data_canon, so 80 draft files keep an m2c-guessed decl that TYPE-CONFLICTS with the TU and are silently reverted by the gate as if they were codegen failures.

Scanners measured: 16 Verified CLEAN: MEASURED FULL COVERAGE (no action needed, do not spend time here): (1) sig_unify.rewrite_def def-header rx — 10600/10600 draft definitions matched; its [^()]* param class would break on a fn-ptr param but ZERO exist in 10731 drafts. (2) sig_unify.PROTO_DECL_RE — 5297/5297 bare m2c prototypes matched, GAP=0. (3) sig_unify.parse_canon_sig — 2341/2341 canonical func decls parsed. (4) recover_giant.canon_sig — 1801/1801 DEFINE_func_X() macros parsed; all 1801 DEFINE_* are zero-arg and ZERO have a { before the signature (the failure mode I hypothesised does not exist). (5) canon_resident_calls.SYM_RE — 991 parsed of 1039 assignment lines; the 48-line gap is 100% // data symbols = a JUSTIFIED exclusion, exactly as documented. (6) canon_resident_calls.FUNC_TOK — 69,876 func_ tokens across the draft corpus, every one exactly 8 hex; the {8} quantifier loses nothing. (7) inject_capped_externs.def_span — 57,472/57,472 top-level defs located across all 811 overlay TUs. (8) fix_arity_callers.draft_is_promotion_safe — 3998/4000 defs found; the 2 misses are corrupt drafts (if ( func_80161208() == 0 a0)), not a regex hole.

[CRITICAL] tools/sig_unify.py :: DATA_DECL_RE (line 36) -> collect_data_decls

  • candidates 646 / parsed 602 / real skips 44
  • evidence: Corpus = the tool's own canon set (src/ov_SC01_077/ov_SC01_077.c + src/shared/engine_core.h). The char class [A-Za-z_][\w\s\*]*? contains no (, [, ] or ,, and the array suffix is \[\s*\] (EMPTY brackets only). So three declarator forms are invisible: (a) fn-ptr / fn-ptr-array — 42 syms, e.g. src/shared/engine_core.h:2554 extern void (*D_80127088)(void); and src/ov_SC01_077/ov_SC01_077.c:830 extern void (*D_80187ED0[])(void);; (b) SIZED arrays — 2 syms, e.g. engine_core.h extern s32 D_80127530[4];; (c) multi-symbol lines, e.g. src/ov_SC01_000/ov_SC01_000_jr_8012ACE0.c extern s32 D_801A445C, D_801A4460, D_801A4464, D_801A4468; (only the 1st sym would even be a candidate, and the comma kills the whole match). NONE of the 44 is a justified exclusion — every one is a data symbol drafts reference. Counterfactual, type-normalized (u8==unsigned char etc.): 80 draft files declare a GAP symbol with a genuinely TYPE-CONFLICTING spelling. Concrete: .run/drafts-T6b-fail/func_801726D0.c:3 extern void *D_80127088; vs engine_core.h:2554 extern void (*D_80127088)(void); -> conflicting types for D_80127088. Also .run/drafts-T6c-fail/func_801387B8.c extern u8 D_80127530[]; vs extern s32 D_80127530[4];, and .run/drafts-T6c-fail/func_80153800.c extern s32 D_8011DB28; vs extern void (*D_8011DB28)(s32 a0);. Per-symbol draft counts: D_80127088 x45, D_8011DB28 x19, D_801274D0 x10, D_80127530 x9, D_80127540 x3.
  • blast radius: LOST MATCHES. sig_unify is the recovery pass whose entire job is to stop conflicting types from failing the whole-binary gate. For these 44 symbols it silently does nothing, so up to 80 drafts (each a candidate byte-exact bank) fail the TU compile and are reverted — and the failure reads as 'this draft is unrecoverable', identical in the logs to a real codegen wall. The affected symbols are the engine's fn-ptr dispatch/callback slots, i.e. the exact class scope_data_externs' own docstring already flags as unparseable by reconcile_decls — the same disease, still unfixed in sig_unify. Multiply by the family-sweep reach (x113-x134 per core) for the ones that gate a sibling family.
  • fix: Replace line 36-37 with a declarator-agnostic form: DATA_DECL_RE = re.compile(r'extern\s+([A-Za-z_][^;{}\n]?\bD_[0-9A-Fa-f]+[^;{}\n]?)\s*;') This accepts (*D_x[])(void *), D_x[4] and D_x[]. Additionally, in collect_data_decls, emit one entry PER symbol found on the line (for s in re.findall(r'\bD_[0-9A-Fa-f]+\b', decl)) so multi-symbol extern s32 D_a, D_b, D_c; lines are not dropped wholesale — or, safer, skip multi-symbol lines EXPLICITLY and count them as a known exclusion rather than a silent one.
  • assertion to add: In collect_data_decls, build an over-approximating candidate set first: cands = {s for line in <every line matching r'^[ \t]*extern\b.*;'> for s in re.findall(r'\bD_[0-9A-Fa-f]+\b', line)}. Then missing = cands - set(out); assert not missing, f'DATA_DECL_RE failed to parse {len(missing)} declared data symbols: {sorted(missing)[:10]}' (or print a LOUD warning listing the offending source lines). The tool must never report 'canonical decls: N data' without also reporting how many declared data symbols it could not parse.

[MEDIUM] tools/recover_giant.py :: recover() extern-gather (line 49: [l for l in lines if l.strip().startswith('extern')])

  • candidates 5297 / parsed 0 / real skips 2401
  • evidence: recover_giant's stated purpose is to canonicalize a giant draft's guessed callee decls against engine_core.h's DEFINE_ sigs and block-scope them. It collects ONLY lines starting with extern. But m2c also emits callee prototypes with NO extern keyword (<ret> func_X(<params>); — the very form sig_unify had to add PROTO_DECL_RE for in Phase 16). Across the draft corpus there are 5297 such col-0 bare prototypes in 1819 files; recover() neither canonicalizes nor block-scopes ANY of them, and still prints recovered <fn>: N externs canonicalized (success). Type-normalized, 2401 of them (in 979 files) carry a REAL type conflict with the DEFINE canonical sig, e.g. .run/drafts-T6w1-fail/func_80153C44.c:2 void func_80153C74(s32 a0, s32 a1); vs engine_core.h:1266 DEFINE_func_80153C74 -> void func_80153C74(s16, s16); .run/drafts-T6w1-fail/func_801653B8.c void func_801653B8(s32); vs canonical void func_801653B8(s32 *); .run/drafts-T6w1-fail/func_801699D0.c void func_80169A4C(s32,s32); vs canonical s32 func_80169A4C(s32,s32). HONEST CAVEAT: in recover_giant's ACTUAL corpus today (.run/drafts-giants*/) there are ZERO bare prototypes — all 426 callee decls there are extern-prefixed. So this hole is currently LATENT: the tool is clean by luck, not by construction. It bites the moment recover_giant is pointed at a mainstream m2c draft (which its CLI signature recover_giant.py <fn> <in> <out> invites).
  • blast radius: LOST MATCHES (latent). Zero cost today (0/426 in drafts-giants*). If the giant-recovery lever is reused on any mainstream draft — 1819 of 10731 drafts carry bare protos — recover_giant silently leaves the guessed sigs at FILE scope: the draft then fails the whole-binary gate with conflicting types (the exact wall recover_giant exists to remove) AND the file-scope leftovers break the find_site/compiles_standalone lift, so dedup_propagate's xN reach is lost too. It reports success either way.
  • fix: In recover(), gather BOTH decl forms: PROTO = re.compile(r'^[ \t](?!extern\b)(?!return\b|if\b|while\b|for\b|switch\b|do\b|else\b)[A-Za-z_][\w \t*]?\bfunc_[0-9A-Fa-f]+\s*([^;{])\s;.*$') is_decl = lambda l: l.strip().startswith('extern') or PROTO.match(l) and use is_decl in place of the startswith('extern') test for BOTH the externs and the rest partition. canon_extern already handles a missing extern prefix correctly (it rebuilds 'extern ' + cs + ';').
  • assertion to add: After building rest, assert no file-scope declaration of a func_/D_ symbol survives in it: leftovers = [l for l in rest[:di] if re.match(r'^[A-Za-z_][\w \t\*]*\b(func_|D_)[0-9A-Fa-f]+\s*[\(;]', l) and l.rstrip().endswith(';')]; assert not leftovers, f'recover_giant left {len(leftovers)} file-scope decls uncanonicalized/unmoved: {leftovers[:5]}'. Also print the count of decls it moved AND the count it could not parse, never just the former.

[MEDIUM] tools/inject_capped_externs.py :: file_scope_externs (line 60: r'^[ \t]*(extern[^\n;]*;)[ \t]*$')

  • candidates 292 / parsed 263 / real skips 29
  • evidence: Corpus = the default --src-file src/ov_SC01_077/ov_SC01_077.c. The [ \t]*$ anchor demands the ; be the last non-space char on the line, so every extern carrying a TRAILING COMMENT is invisible. All 29 gap symbols are that class, e.g. src/ov_SC01_077/ov_SC01_077.c:12 extern s32 func_8016EC0C(s32 a0, s32 a1); /* match-first, arity 2 */ and :14 extern s32 func_801670E4(s32 a0, s32 a1, s32 a2, s32 a3); /* derive-decl, arity 4 */. Two further sub-classes exist and are also silently dropped: multi-line externs (src/ov_SC01_077/ov_SC01_077.c:269 extern unsigned char D_801DA9B8, D_801DA9B9, ... wrapping to the next line) and multi-symbol lines (SYM_RE.search takes only the FIRST symbol, so extern s32 D_801A445C, D_801A4460, D_801A4464, D_801A4468; maps 1 of 4). Across the whole overlay TU corpus (which --src-file can target) the same regex loses 3667 trailing-comment (sym,decl) pairs + 1872 multi-symbol pairs. Measured downstream effect on the default file TODAY: 1 top-level def references a GAP symbol -> it lands in the missing extern: ... skip bucket instead of being freed.
  • blast radius: LOST MATCHES (small today, systemic by construction). A symbol absent from ext_map goes to missing, compiles_standalone fails, and the capped function is reported as skip 0x... (missing extern: func_X) — indistinguishable from a genuine type wall. Each such skip forfeits that function's dedup_propagate reach (x2..x134 overlays). Currently 1 def on the canonical overlay; the number scales directly with how many hand-annotated externs (the trailing /* match-first, arity 2 */ style, which is the project's own convention) exist in whichever --src-file is passed.
  • fix: Line 60: drop the end anchor and map every symbol on the line: for m in re.finditer(r'^[ \t](extern\b(?:[^;{}]|\n)?;)', text, re.M): line = ' '.join(m.group(1).split()) for s in SYM_RE.findall(line): out.setdefault(s, line) The (?:[^;{}]|\n)*? also picks up the wrapped multi-symbol externs, and SYM_RE.findall (instead of .search) fixes the multi-symbol case.
  • assertion to add: cands = {s for m in re.finditer(r'^[ \t]*extern\b.*$', text, re.M) for s in SYM_RE.findall(m.group(0))}; missing = cands - set(out); assert not missing, f'file_scope_externs failed to map {len(missing)} declared symbols: {sorted(missing)[:10]}'. Any function later skipped with reason 'missing extern: X' where X IS in cands must be a hard error, not a skip line.

[MEDIUM] tools/sig_unify.py :: DRAFT_EXTERN_LINE_RE (line 38: r'^[ \t]*extern\b[^;]*;[ \t]*$')

  • candidates 37022 / parsed 36777 / real skips 245
  • evidence: Corpus = 10731 draft .c files. The [ \t]*$ anchor means the ; must end the line. Gap breakdown: 206 externs with a trailing /* */ comment (e.g. .run/drafts-T6b-fail/func_8013E588.c:7 extern u8 D_800B9A15; /* sb */), 15 WRAPPED multi-line externs, 15 multi-decl lines (.run/drafts-o0/func_8013B598.c extern s32 D_801DAA08; extern u16 D_801DAA0C; extern u16 D_801DAA0E; — NONE of the three is rewritten), 9 with a trailing // comment. All 245 are real skips (the tool should have canonicalized them and did not) — but I checked the counterfactual honestly: after normalizing typedef aliases (u8==unsigned char) and param names, only 10 statements in 10 draft files carry a TYPE-level conflict that the rewrite would actually have resolved. The other 235 are cosmetic (the draft already agrees with canon; the comment merely blocked the no-op rewrite). The 10 real ones are the wrapped long signatures: .run/drafts-T6b/func_80157158.c extern void func_801571C4(s32 a0, u16 a1, u16 a2, s32 a3, (wraps) vs canon extern u32 func_801571C4(...) — return type void vs u32, a guaranteed conflicting types; and .run/drafts-T6c-fail/func_80156670.c full-proto vs canon no-proto extern void func_80157158();.
  • blast radius: LOST MATCHES. 10 drafts today whose canonicalization silently no-ops -> whole-binary gate revert. Structurally it is the same class as the SIG_IN_BODY bug: the moment any tool or human puts a comment on an extern line — which the project's own convention encourages — that decl becomes invisible to the recovery pass forever. Note the 15 wrapped externs are exactly the >4-arg engine signatures, i.e. the hardest-to-match call-heavy functions.
  • fix: Make it a STATEMENT matcher, not a line matcher, and stop requiring EOL after the ;: DRAFT_EXTERN_LINE_RE = re.compile(r'^[ \t]extern\b(?:[^;{}]|\n)?;', re.M) The sub() callback already preserves leading whitespace and replaces the matched span, so a multi-line match collapses correctly to the canonical one-liner. (Keep a guard so a { never enters the span.)
  • assertion to add: Per draft, before writing: cands = len(re.findall(r'(?m)^[ \t]*extern\b', txt_before)); parsed = len(DRAFT_EXTERN_LINE_RE.findall(txt_before)); assert parsed == cands, f'{p}: {cands-parsed} extern statements not parsed by DRAFT_EXTERN_LINE_RE' — and print the aggregate 'externs seen / externs parsed' in the final summary line so a regression shows up as a number, not as a silent zero.

[MEDIUM] tools/fix_arity_callers.py :: void_re / anyproto_re / noproto_re (lines 97-99)

  • candidates 128351 / parsed 128345 / real skips 6
  • evidence: All three regexes hard-require the literal extern keyword before the return type. Corpus = src/shared/engine_core.h + all 811 overlay .c (the files reachable via --binary). Of 128,351 (void)-prototype declarations of a func_X, 6 lack extern and are therefore invisible: src/ov_SC01_077/ov_SC01_077_jr_8015AE2C.c:2092 s32 func_80029AAC(void);, plus the same decl in ov_SC01_077_jr_801734BC.c, ov_SC01_077_jr_80178D40.c, ov_SC01_077_jr_80182268.c, and void func_8012A418(void); in the latter two. Two distinct callers (func_80029AAC, func_8012A418). Separately verified as NOT a problem: zero extern func decls in engine_core.h wrap across lines, so the newline-free return-type class [\w \t\*] costs nothing today. When this fires the tool prints [no caller (void) decl found] func_X — which is factually FALSE and reads to the operator as 'nothing to do here'.
  • blast radius: LOST MATCHES (bounded). If func_80029AAC or func_8012A418 is ever a match target that takes an argument, the no-prototype arity recovery cannot fire in the four jr carve TUs — and those are precisely the TUs the heavy-jr family sweep builds. --revert is equally blind, so a bad state there cannot be undone by the tool either. Bounded to 2 functions x 4 TUs today, but the shape means any future non-extern prototype in a carve TU is silently unrecoverable.
  • fix: Make extern optional in all three patterns and anchor the decl at line start: void_re = re.compile(rf'(?m)^([ \t](?:extern\s+)?[A-Za-z_][\w \t*]?\bfunc_{ad}\s*()\svoid\s()\s*;)', re.I) anyproto_re = re.compile(rf'(?m)^([ \t](?:extern\s+)?[A-Za-z_][\w \t*]?\bfunc_{ad}\s*()\s*[^;)]+?\s*()\s*;)', re.I) noproto_re = re.compile(rf'(?m)^([ \t](?:extern\s+)?[A-Za-z_][\w \t*]?\bfunc_{ad}\s*()\s*()\s*;)', re.I) (the (?m)^ anchor prevents a call site from matching now that extern is optional).
  • assertion to add: Before printing [no caller (void) decl found] fn, cross-check with an over-approximating detector: cand = re.search(rf'(?m)^[ \t\\]*(?:extern\s+)?[A-Za-z_][\w \t\*]*?\bfunc_{ad}\s*\(\s*void\s*\)\s*;', text). If cand fires but the rewrite did not, sys.exit(f'FAIL: a (void) decl of {fn} EXISTS but void_re could not match it: {cand.group(0)!r}'). A 'not found' message must be provable, not assumed.

[LOW] tools/scope_data_externs.py :: _body_open_brace (line 74)

  • candidates 10600 / parsed 10590 / real skips 10
  • evidence: The ANSI/own-line/K&R brace fix documented in the docstring is CORRECT (I verified 10590/10600 land exactly on the definition's opening brace, 0 no-matches). But the sig regex ^[^\n]*\b{func}\s*\( takes the FIRST line in the body that mentions func( — including a line inside a COMMENT. 10 drafts carry a header comment that names the function with a paren and thereby mis-anchor the search: .run/drafts/func_800D27DC.c:3 // void *func_800D27DC(u32 flags /*a0*/, u8 *out /*a1=t4*/, u16 *src /*a2*/, — body.find('{', sig.end()) then returns a brace that is NOT the def's, so the demoted externs are injected into whatever construct owns it. Same in .run/drafts-o0/func_8013BCDC.c, .run/drafts-t5-pilot/func_8017B490.c, .run/drafts2/func_800D2CA8.c, .run/drafts3/func_800D1B80.c, and 5 more.
  • blast radius: CORRUPT BUILD -> lost match (small, 0.09%). A misplaced block of extern lines yields a compile error, the sibling reverts, and the family sweep records it as a failure of the CORE rather than of the placement. Zero risk of a wrong match (the gate catches it), but it costs the bank silently. Risk grows with the exemplar bodies carried by family_remap, which are lifted from src/ where analysis-comment headers are the norm.
  • fix: Strip comments before locating the signature (do NOT strip them from the returned body — compute the index on a masked copy of equal length): masked = re.sub(r'/*.?*/', lambda m: ' 'len(m.group(0)), body, flags=re.S) masked = re.sub(r'//[^\n]', lambda m: ' 'len(m.group(0)), masked) sig = re.search(rf'^[^\n]\b{re.escape(func)}\s(', masked, re.M) i = masked.find('{', sig.end()) Offsets stay valid against body because the masks preserve length.
  • assertion to add: After computing at, prove the brace really closes the signature: walk back from the { at index i to the matching ) and assert the text between them is whitespace only, and that the paren at sig.end()-1 is balanced-closed before i. assert re.fullmatch(r'\s*', body[close_paren+1:i]), f'_body_open_brace anchored on a brace that is not {func}\'s body: {body[i-60:i+20]!r}' — a wrong anchor must raise, never silently inject.

[LOW] tools/scope_data_externs.py :: FILE_EXTERN_RE / _file_scope_data_syms (lines 58, 62)

  • candidates 1017508 / parsed 1017503 / real skips 5
  • evidence: ^extern\b[^;{}\n]*; cannot span lines, so a WRAPPED file-scope extern is invisible to the TU oracle. Only 5 statements out of 1,017,508 col-0 extern lines — but they are SYSTEMIC and multi-symbol: src/ov_SC01_077/ov_SC01_077.c:269 extern unsigned char D_801DA9B8, D_801DA9B9, D_801DA9BA, D_801DA9BB, D_801DA9BC, (wraps). Every one of the 134 overlay TUs carries the same shape, and each loses exactly 5 file-scope D_ symbols from the oracle (measured: 200 true -> 195 seen in ov_SC06_014.c, ov_SC07_000.c, ...; 306 -> 301 in the jr_8013FFD8 carves). A second, quieter hole in the same function: DATA_SYM_RE.search records only the FIRST D token of a decl, so extern s32 D_a, D_b, D_c; registers 1 of 3 even when it IS parsed.
  • blast radius: CORRUPT ORACLE, benign consequence TODAY. When fix() wrongly believes the TU has no file-scope decl for one of those 5 symbols, it DEMOTES the carried extern to block scope. By the tool's own (correct) 'never worse than the status quo' argument both branches are survivable — identical spelling is a legal duplicate at block scope, a differing spelling is an error at either scope (the §41 reconcile class). So I am NOT claiming lost banks here. What IS true: the oracle _file_scope_data_syms is silently wrong on 5 syms x 134 TUs, and anything built on it later (a stricter placement rule, a reconcile decision) inherits a false negative it has no way to detect.
  • fix: FILE_EXTERN_RE = re.compile(r'^extern\b(?:[^;{}]|\n)*?;', re.M) # statement, not line and in _file_scope_data_syms use out.update(DATA_SYM_RE.findall(m.group(0))) instead of taking only the first token. In fix(), keep the col-0 line test for the BODY partition (that half is correct — gather_externs always emits one-line col-0 decls) but guard it: if a body extern statement wraps, join it before classifying.
  • assertion to add: crude = set(re.findall(r'\bD_[0-9A-Fa-f]{6,8}\b', ' '.join(l for l in text.split(chr(10)) if l.startswith('extern') or _in_wrapped_extern(l)))) is awkward; simpler permanent guard: assert not re.search(r'(?m)^extern\b[^;{}]*$', text), 'FILE_EXTERN_RE: TU contains a WRAPPED file-scope extern this scanner cannot see' — i.e. fail loud on the existence of the unparseable shape rather than silently under-reporting the symbol set.

[LOW] tools/sig_unify.py :: stub_re (line 176) -> cur_stubs gate (line 184)

  • candidates 59201 / parsed 59101 / real skips 100
  • evidence: INCLUDE_ASM\([^,]*,\s*(func_[0-9A-Fa-f]+)\) only recognizes auto-named stubs. 100 INCLUDE_ASM stubs across src/ov_*/ name a CURATED symbol instead — all 100 are listCdBuffer, e.g. src/ov_SC01_077/ov_SC01_077_jr_80178D40.c INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40", listCdBuffer); (one per overlay's _jr_80178D40 carve). Because cur_stubs never contains such a symbol, the guard if fn not in cur_stubs: continue drops the draft — and note it drops it BEFORE the write, so the draft is not even COPIED to --out. It vanishes from the pipeline entirely with no message; the run summary just says 'drafts unified: N' with a smaller N.
  • blast radius: LOST MATCHES (1 function today, unbounded going forward). Any function that has been given a curated name in config/symbols.* becomes permanently un-recoverable by sig_unify — and curated naming is a thing the project does more of over time, so this hole WIDENS as RE quality improves. It also interacts badly with canon_resident_calls, whose whole job is to rewrite func_ to the curated name: a draft that tool just canonicalized is one sig_unify may then silently discard.
  • fix: Broaden the stub regex and reverse-map curated names to addresses: stub_re = re.compile(r'INCLUDE_ASM([^,],\s([A-Za-z_]\w*))') then build cur_stubs as a set of BOTH the raw stub names and, for curated names, func_{addr:08X} resolved via canon_resident_calls.load_name_map (inverted). Also, when a draft is skipped for not being a current stub, COPY it through to --out unchanged (or log it) so nothing silently disappears from the pipeline.
  • assertion to add: skipped = [p for p in drafts if basename(p)[:-2] not in cur_stubs]; assert not skipped or all(<addr has no INCLUDE_ASM stub at all in c_path>), f'sig_unify dropped {len(skipped)} drafts that have no matching stub: {skipped[:5]}' — and unconditionally print drafts in: X, unified: N, dropped (not a stub): X-N so a nonzero drop count is always visible rather than being folded into a smaller success number.

GROUP: metrics

Headline: progress.py's headline REAL count is wrong in BOTH directions: 532 function-instances are reported as banked matches while still INCLUDE_ASM stubs (4 dedup groups whose DEFINE_ macro exists nowhere in src), and 670 genuinely byte-exact banked instances (243k instructions — the hardest-won Phase-26 cracks) are counted as NOTHING because classify() reads a K&R-style definition as a forward declaration.

Scanners measured: 9 Verified CLEAN: MEASURED AND FULLY COVERED (do not spend time here): (1) classify()'s INCLUDE_ASM regex — 59,248 INCLUDE_ASM sites fleet-wide, all parsed; independently gated against the ".s oracle" (every asm//nonmatchings//.s = one un-banked symbol): 0 orphan .s files across all 136 binaries (main: 1034 stubs + 959 linked + 2 blobs + 7 NM = 2002 = exactly the .s count). This scanner is complete. (2) linked_subsegs() — the Makefile psyq_integrate + NAME := <comma,list> mvars parse: 49 candidate segments (every src/.c stem named lib/snd*/apicard* reachable from a psyq_integrate line, with $(LIBGTE_STUBS)/$(SND_STUBS)/$(APICARD_STUBS) expanded) vs 49 parsed. GAP = 0, EXTRA = 0. The Makefile-lockstep regex is currently correct. (3) BINARIES dict — 134 src/ov_* dirs = 134 ov_ entries = 134 sig.ov_*.jsonl. No overlay is silently missing from the fleet aggregate. (4) SRCS glob (src/<bin>/*.c, top-level only) — justified exclusion: the only .c files below the top level of main's src/ are the overlay subdirs, which are separate binaries with their own BINARIES entries. No orphan .c anywhere. (5) is_data_blob() — applied to all 2002 main .s files it returns exactly the 2 blobs classify() reports (func_8005CE38, func_80047CAC); self-consistent and complete over the .s corpus. (6) asm_is_trivial()'s REGEX — parses the real .s format correctly (0/2002 false "trivial"). The regex is fine; its CALLER is broken (see finding 5). (7) weighted_metrics() address alignment — resident's 21 src stub addresses all 21 land in .run/sig.resident.jsonl; the fun_ vs func_ name-prefix difference in the resident sig is a sig-generation artifact and does NOT affect the metric, which joins on ADDRESS. The instruction-weighted / distinct-code headline numbers are SOUND: they use "not an INCLUDE_ASM stub ⇒ matched", which independently gets both the K&R functions (correctly matched) and the 4 phantom-dedup functions (correctly unmatched) right. Only the fn-count metric is corrupted. (8) SIG's same-line definition form — classifying every uncounted sig function fleet-wide produced NO residue outside the K&R and macro-only classes: the ordinary s32 func_X(s32 a) { form is parsed with full coverage.

[CRITICAL] tools/progress.py :: dedup_members() / _DEDUP_CACHE (config/dedup.us.yaml via dedup_integrate.group_members)

  • candidates 223725 / parsed 223725 / real skips 532
  • evidence: DIRECTION NOTE: this gap is an OVER-claim, not a skip — the tool parses 532 registry claims it should have rejected. 7 registry groups name a func: token that occurs in ZERO files under src/: E_func_80128ED8, E_func_8012C098, E_func_8012C0EC, E_func_8012C750, E_func_8012F14C, E_func_8012F038, E_func_8012E5CC (grep -rl 'DEFINE_func_80128ED8' src/ -> 0 files). Four of them are still live INCLUDE_ASM stubs, so the same function is counted TWICE — once as a REAL dedup-shared match, once as a stub: config/dedup.us.yaml:11548 - id: E_func_80128ED8 / func: DEFINE_func_80128ED8 / binaries: [ ...134 overlays... ] src/ov_SC01_005/ov_SC01_005.c:401 INCLUDE_ASM("asm/ov_SC01_005/nonmatchings/ov_SC01_005", func_80128ED8); Measured over the fleet: func_80128ED8 x133, func_8012C098 x133, func_8012C0EC x133, func_8012C750 x133 = 532 (binary,fn) pairs that appear in BOTH dedup_members() and classify()'s stubs bucket. report() does shared = dedup_members(BINARY) - set(real) — it subtracts real but never stubs, so the registry claim wins unchallenged.
  • blast radius: Corrupt METRICS + lost MATCHES. (a) REAL is inflated by 532 function-instances that are NOT matched; every docs/progress.ov_*.md "REAL matches:" line names func_80128ED8/func_8012C098/func_8012C0EC/func_8012C750 as banked. (b) matchable is double-counted by the same 532 (measured: matchable - |union of buckets| = 532 across the fleet). (c) LOST WORK: these 4 distinct functions x133 overlays are REPORTED DONE, so no sweep or target-picker driven off progress.py will ever queue them — 532 free instances hidden behind a false green.
  • fix: In dedup_members(), verify the registry against reality instead of trusting it. Two changes, both fail-loud: (1) After building _DEDUP_CACHE, drop-and-report any claim that is still stubbed: stubbed = {m.group(1) for cf in (ROOT/f'src/{binary}').glob('.c') for m in re.finditer(r'INCLUDE_ASM([^)],\s*(\w+))', cf.read_text())} names = _DEDUP_CACHE.get(binary, set()) bogus = names & stubbed if bogus: sys.exit(f"progress.py: dedup registry claims {len(bogus)} fns MATCHED in {binary} that are still INCLUDE_ASM: {sorted(bogus)[:5]}") return names - bogus (2) Once, at load: assert every group's func: token occurs at least once in src/**/*.[ch]; the 7 groups listed above fail this today. (Separately, and outside progress.py's scope: those 7 registry entries should be removed or re-integrated — they are stale claims in the source of truth.)
  • assertion to add: assert dedup_members(b) & include_asm_symbols(b) == set(), f'dedup registry claims {n} matched fns that src still stubs' AND assert all(group['func'] in ALL_SRC_TEXT for group in registry), 'registry claims a match whose macro does not exist in src'

[HIGH] tools/progress.py :: classify() — the SIG definition-vs-declaration discriminator (progress.py:440-445)

  • candidates 534 / parsed 0 / real skips 534
  • evidence: K&R (old-style) function definitions are read as forward DECLARATIONS and skipped. classify() scans forward from the SIG line for the first { or ; and calls it a decl if the ; comes first — but in a K&R definition the parameter declarations sit BETWEEN the ) and the {: src/ov_SC01_005/ov_SC01_005_after.c:3778 s32 func_8015A3C8(arg0) src/ov_SC01_005/ov_SC01_005_after.c:3779 s32 arg0; <-- ; found first -> kind='decl' -> i = j + 1; continue src/ov_SC01_005/ov_SC01_005_after.c:3780 { Over-approximating candidate detector (col-0 <type> name(a, b) with an untyped arg list, followed by <type> arg; lines then {): 533 K&R heads across 399 files. Cross-checked against the sig oracle (every function that EXISTS in each binary): 534 (binary,fn) pairs, 243,296 instructions, are in NO bucket at all — not real, not empty, not stub, not shared. Distinct symbols: func_8015AE2C x134, func_80133CD4 x134, func_80166994 x134, func_8015A3C8 x132, func_80180000 x1. These are byte-exact BANKED matches (Phase-26's 562/493/399/369-instruction cracks). main has 0 K&R defs; this is entirely an overlay/m2c-output phenomenon.
  • blast radius: Corrupt METRICS. 534 function-instances / 243,296 instructions of genuinely banked, byte-exact code are invisible: REAL, byteident and matchable are each 534 too low. The per-binary REAL number gates the Gen1-exit ">=25 REAL" bar and any "which overlay next" decision. It also silently punishes exactly the highest-value work: the biggest hand-won functions in the project are the ones m2c emits in K&R form. LATENT: every future K&R draft vanishes the same way, and it reads as "the crack didn't land".
  • fix: Replace the def/decl discriminator so that a ; only means 'declaration' when it appears on or before the line that CLOSES the parameter list; K&R parameter declarations between ) and { must be skipped. Concretely, in classify() replace the while j < n: ... br/sm loop with: (a) walk forward tracking paren depth from the SIG line until the parameter list's ) closes -> line cl; (b) if a ; appears in strip_comments(lines[cl]) AFTER that closing ), kind='decl'; (c) otherwise walk forward from cl+1 skipping blank lines and lines matching ^[A-Za-z_][\w \t\*\[\],]*;$ (K&R param decls) — if the next non-such line contains {, kind='def', else kind='decl'.
  • assertion to add: assert set(real) | set(empty) | set(nonmatching) | set(stubs) | set(linked) | set(blobs) | dedup_members(b) >= {r['name'] for r in sig(b) if r['nins'] > 0}, f'{n} functions exist in {b} but are classified as NOTHING: {sorted(missing)[:5]}' — this single sig-reconciliation assertion catches findings 2 and 3 and the false side of finding 1.

[MEDIUM] tools/progress.py :: classify() (macro-emitted functions) + dedup_members() (registry binaries: list completeness)

  • candidates 136 / parsed 0 / real skips 136
  • evidence: Functions that exist in a binary, are matched (no INCLUDE_ASM anywhere, fleet builds byte-identical), and are counted in NO bucket — they are emitted by a shared macro in src/shared/engine_core.h, which classify() cannot parse, AND the dedup registry group that covers them omits those binaries from its binaries: list. Measured against the sig oracle: 136 (binary,fn) pairs, 1,632 instructions. Distinct symbols: func_80128EA8 x118, func_80132EC4 x118, func_80180000 x3. Evidence that they are macro-only: src/ov_SC01_004/ov_SC01_004_jr_8013FFD8.c:178 extern void func_80128EA8(s32 a0, s32 a1, s32 a2); <- declaration only src/ov_SC01_004/ov_SC01_004_jr_8013FFD8.c:1724 func_80128EA8(...); <- call only (no definition and no INCLUDE_ASM anywhere in src/ov_SC01_004/ — the body comes from an engine_core.h macro) Contrast: in ov_SC01_005 the same function IS accounted for, because the registry lists ov_SC01_005 for that group. So the registry's per-group binaries: list is short by ~118 overlays.
  • blast radius: Corrupt METRICS (undercount). REAL / byteident / matchable each 136 too low. Smaller than the K&R class but the same shape, and it means the dedup registry — the declared 'source of truth for code shares' — is not actually reconciled against the binaries it claims to describe.
  • fix: Do not try to teach classify() to expand macros. Instead make the sig reconciliation authoritative: after classify()+dedup_members(), any sig function with nins>0 that is not in any bucket and is not an INCLUDE_ASM stub is BY DEFINITION a byte-identical match (the fleet gate proves it) — fold it into real and report the count separately, e.g. (of which macro-emitted, sig-reconciled : N). Fail loud if that reconciliation count is nonzero for a binary that has no sig (i.e. main), where it cannot be verified.
  • assertion to add: assert (sig_fns(b) - accounted(b) - stub_syms(b)) == set(), f'{n} matched fns in {b} counted in no bucket (macro-emitted / registry binaries: list short): {sorted(...)[:5]}'

[MEDIUM] tools/progress.py :: classify() — preprocessor handling (only #ifdef NON_MATCHING is understood)

  • candidates 1 / parsed 1 / real skips 1
  • evidence: DIRECTION NOTE: over-claim — the tool COUNTS an item it should have excluded. classify() special-cases #ifdef NON_MATCHING but is otherwise preprocessor-blind, and it runs SIG.match on the RAW line. A function definition parked inside #if 0 ... #endif is therefore counted as a REAL match: src/resident/resident.c:861 /* func_800D00E4: left as INCLUDE_ASM — genuine structural mismatch. ... */ src/resident/resident.c:868 #if 0 src/resident/resident.c:903 void func_800D00E4(s32 arg0) { <- counted REAL src/resident/resident.c:925 #endif src/resident/resident.c:926 INCLUDE_ASM("asm/resident/nonmatchings/resident", func_800D00E4); <- also counted as a stub Measured fleet-wide: exactly 1 (binary,fn) pair today, and it is simultaneously in real and stubs (the resident row's matchable double-count of 1).
  • blast radius: Corrupt METRICS, small today (resident REAL +1, matchable +1) but it is a FALSE MATCH claim: a function the source explicitly documents as un-matched is reported as banked. LATENT and growing: #if 0 is the natural place to park a near-miss draft, and every one parked there will silently inflate REAL. Same class as finding 1 — the tool cannot tell 'code that ships' from 'code that is commented out'.
  • fix: Give classify()'s top-level loop a preprocessor skip that mirrors the existing NON_MATCHING block skip: on a line matching ^#if\s+0\b, consume forward tracking nested #if/#ifdef/#ifndef depth until the matching #endif, and continue — exactly as the #ifdef NON_MATCHING branch already does (progress.py:413-420). Do NOT harvest an INCLUDE_ASM out of the #if 0 body.
  • assertion to add: assert set(real).isdisjoint(set(stubs)) and set(real).isdisjoint(set(linked)) and set(empty).isdisjoint(set(stubs)), 'a function is counted in two buckets' — bucket disjointness; today it fires 533 times (532 phantom-dedup + this 1).

[MEDIUM] tools/progress.py :: --audit (report() lines 497-501 calling asm_is_trivial() over the empty bucket)

  • candidates 570 / parsed 0 / real skips 570
  • evidence: --audit is documented as 'verify every empty no-op's asm is exactly {jr,nop}'. It verifies ZERO of them and always prints 'all clean'. Mechanism: asm_is_trivial(name) does p = find_s(name); if p is None: return None, and the caller filters with bad = [n for n in empty if asm_is_trivial(n) is False] — None is not False, so a missing .s silently PASSES. And the .s files only exist for UN-banked symbols: an empty function is by definition already in C, so its .s is gone. Measured across all 136 binaries: 570 empty no-ops, of which 0 have a .s file to audit. Every one returns None. The report line then prints empties audit: 570/570 genuine jr;nop (all clean) having checked nothing. (The regex inside asm_is_trivial is NOT the bug — it parses the real .s format correctly, 0/2002 false trivials on main's stub .s files.)
  • blast radius: Corrupt METRICS (a check that cannot fail). The empty bucket feeds byteident (REAL+LINKED+empty), so a mis-emitted void f(void) {} over a non-trivial function would inflate the byte-identical headline. In practice the full-binary SHA1 byte-gate WOULD catch such a function, so no wrong match can ship — this is a redundant guard, not the last line of defence. But it is the purest instance of the class: a tool that no-ops on input it cannot find is indistinguishable from a tool that found nothing wrong.
  • fix: Audit the empties against the sig (which still lists them) instead of the deleted .s, and never let 'not found' read as 'clean': checked = [n for n in empty if find_s(n) is not None] unverif = [n for n in empty if find_s(n) is None] bad = [n for n in checked if asm_is_trivial(n) is False] out.append(f"empties audit: {len(checked)-len(bad)}/{len(checked)} verified jr;nop; {len(unverif)} UNVERIFIABLE (no .s)") Better: join empty to the sig by address and assert nins <= 2 — that is checkable for all 570.
  • assertion to add: assert len(checked) == len(empty), f'--audit verified only {len(checked)}/{len(empty)} empties; the rest had no .s and were silently passed' — a check must report its own coverage or fail.

[LOW] tools/progress.py :: weighted_metrics().src_stubs() — INCLUDE_ASM name regex (progress.py:533)

  • candidates 59248 / parsed 59148 / real skips 100
  • evidence: The regex is INCLUDE_ASM\([^)]*,\s*func_([0-9A-Fa-f]+)\) — it only recognises stubs whose symbol is literally func_<hex>, and it recovers the address by parsing hex out of the NAME. Any stub with a real (renamed) symbol is invisible to it, and a not in st then reads as MATCHED. Over-approximating candidate detector INCLUDE_ASM\([^)]*,\s*(\w+)\) over every src//*.c: 59,248 sites; src_stubs parses 59,148. GAP = 100, all the same symbol: src/ov_SC01_005/ov_SC01_005_jr_80178D40.c:4444 INCLUDE_ASM("asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_80178D40", listCdBuffer); (x100 overlays) I then checked whether any of the 100 is present in its binary's sig (which is what would actually miscredit it): 0 of 100. Total instructions wrongly credited TODAY: 0.
  • blast radius: Corrupt METRICS — LATENT, zero live impact. listCdBuffer is absent from the sigs, so the 100 skipped sites currently misattribute 0 instructions in the FLEET instr-weighted headline (the decomp.dev-display number). But the failure is silent and armed: the first renamed symbol that appears in both an INCLUDE_ASM and a sig is counted as MATCHED code it has not matched, inflating the project's most public number with no way to notice. Renaming symbols away from func_ is a normal, desirable decomp activity.
  • fix: Stop deriving the address from the symbol name. Match any symbol — INCLUDE_ASM\([^)]*,\s*(\w+)\) — and resolve it to an address via the sig's own name field (case-insensitively), falling back to int(name[5:],16) only for func_: stub_names = {n.lower() for n in RE_ANY.findall(text)} st = {int(r['addr'],16) for r in recs if r['name'].lower() in stub_names} Then assert every stub name resolved.
  • assertion to add: assert len(stub_names_unresolved) == 0, f'{n} INCLUDE_ASM symbols in src/{b} could not be resolved to a sig address (renamed stubs are being counted as MATCHED): {sorted(...)[:5]}'

GROUP: split-infra

Headline: build_engine_types hard-exits on 573 of 709 type-bearing overlay .c files (81%, all 134 overlays) because 1,929 tagged-struct typedefs trip its "handle manually" overlap guard — the documented unblocker for the type-heavy tail cannot run on the corpus the tail lives in.

Scanners measured: 18 Verified CLEAN: VERIFIED CLEAN (measured, full corpus — do NOT spend time here): • jtbl_carve.PIECE_RE + EOF_RE — 2,583/2,583 config - [...] piece lines parsed across all 134 overlay configs, gap 0. • jtbl_carve.parse_config data-region assumption ("data/.rodata are the trailing run after the last c") — 0/134 configs have a data/.rodata piece before the last c piece. The assumption holds. • jtbl_carve.jtbl_words — 5,671/5,671 jtbls return a non-empty word list; 17,013/17,013 entry lines are .word 0x<hex>. I specifically hunted the .word <symbol> form (which the 0x-only regex would miss, killing the trailing-.align-pad trim and re-creating the +4 image corruption): it occurs ONLY under D_/listCdBuffer pointer tables, never under a jtbl_ dlabel. The trim fires on 100% of jtbls. • jtbl_carve.all_data_labels (jtbl half) — 0 non-8-hex jtbl labels, 0 glabel'd jtbls; 5,653/5,653. • jr_isolate_all.code_objects — 910/910 code pieces. jr_isolate_all.rodata_carves — 722/722 .rodata carves. Gap 0 on both, all 134 configs. • overlay_src_split.macro_table / _MACRO_SIG — 1,804/1,804 function-like macros across engine_core.h + ov_setters.h + clearTbl40.h, all with a definition body parsed (def_lines != [] for every entry). This is the exact scanner shape that produced the SIG_IN_BODY_RE bug in a sibling tool; _MACRO_SIG is clean because it does NOT require )\s*{ between signature and brace. • overlay_src_split.INCLUDE_ASM / DEFINE_FUNC / MACRO_ARG_ANCHOR — 59,246 INCLUDE_ASM lines and 6 col-0 macro-arg anchors, gap 0. (I over-approximated MACRO_ARG_ANCHOR with a space-tolerant ^\w+\s*\(\s*func_ to catch SETTER (func_X,...); no such form exists.) • build_engine_types.find_defs / find_typedefs / typedef_name — the FINDERS are clean: 6,339/6,339 named struct|union defs, 14,701/14,701 typedefs. I hunted 4 hazards and all came back zero: 0 named enum X { in the corpus (so the struct|union-only regex loses nothing), 0 nested/overlapping def spans (the unchecked def-vs-def --strip corruption is not reachable), 0 multi-declarator typedefs (typedef struct{...} A, *PA;), 0 block-scope aggregates. The tool's blocker is PURELY the overlap guard (finding #1), not the parsing.

[CRITICAL] tools/build_engine_types.py :: main() tagged-struct-typedef overlap guard (lines 164-167), against find_defs + find_typedefs

  • candidates 709 / parsed 136 / real skips 573

  • evidence: Corpus = 875 overlay .c; 166 have no types; 709 are type-bearing. 573 of those 709 (81%) contain >=1 TAGGED-struct typedef and therefore hit sys.exit('[overlap] a tagged-struct typedef matched both finders — handle manually'). 1,929 tagged typedefs total; all 134/134 overlays have >=1 bailing file. Examples: src/ov_SC01_000/ov_SC01_000_jr_8012ACE0.c:609 typedef struct Entry_8012DDA4 { u16 active; unsigned char pad[0x10C-2]; } Entry_8012DDA4;; src/ov_SC01_000/ov_SC01_000_jr_8015AE2C.c:5077-5082 typedef struct SubB_8016A73C {...} SubB_8016A73C; / SubA_8016A73C / Mat_8016A73C / Obj_8016A73C; src/ov_SC01_000/ov_SC01_000_jr_80178D40.c (5 tagged typedefs). The tool's own comment asserts 'Our source has only ANONYMOUS-struct typedefs (no tag)' — that was true in Phase 20 and is now false: the harvest agents write their guessed overlay-local structs in the tagged form.

  • blast radius: Lost MATCHES, at the largest remaining scale. tools/inject_capped_externs.py:126 explicitly routes every type-bearing body away from extern-injection with reason 'inline type def -> build_engine_types.py (§28b)' — i.e. the type-heavy tail's ONLY sanctioned unblocker is this tool, and it hard-exits on 81% of the files that tail lives in. This is the '3,098 type-heavy tail' and the 9 zero-bank type-using families. NOTE (honest): this is FAIL-LOUD, not a silent skip — it prints '[overlap] ... handle manually' and exits. It went unfixed because the message reads like an edge case rather than an 81% coverage failure.

  • fix: The guard is over-conservative: a typedef struct Tag {...} Alias; is perfectly liftable — the typedef span ALREADY CONTAINS the whole struct body, so it just must not be double-counted. Replace the sys.exit with containment handling. In main(), after computing defs/tdefs:

    contained = [d for d in defs if any(ts <= d[2] and d[3] <= te for ,,ts,te in tdefs)] partial = [d for d in defs if any(ts < d[3] and d[2] < te for ,,ts,te in tdefs) and d not in contained] if partial: sys.exit('[overlap] PARTIAL def/typedef span overlap — handle manually') defs = [d for d in defs if d not in contained] # the typedef carries the body; don't lift or strip twice

Then add a struct <Tag>; forward decl for each contained tag alongside the existing forward-decl block (safe, and lets pointer-only refs resolve). The strip spans stay disjoint by construction, so --strip is safe. Keep the sys.exit ONLY for a genuine PARTIAL overlap, which is the malformed case the guard was actually written for.

  • assertion to add: assert every named-struct def span is either DISJOINT from every typedef span or FULLY CONTAINED in one; count the contained ones and print lifted N named + M typedefs (K tagged-struct typedefs folded into their typedef). Fail loud ONLY on a partial overlap. Additionally: assert (len(defs)+len(tdefs)) == n_candidates - n_contained where n_candidates is a crude re.findall(r'\b(struct|union|enum)\s+\w+\s*\{|\btypedef\b', blank_comments(text)) count — so a future type form that the finders cannot see fails the run instead of being quietly lifted-minus-one.

[HIGH] tools/jr_isolate_all.py :: _file_scope_decls() — the _HOIST_RE.match(line) and _SAFE_TYPE.match(line) predicate (line 336)

  • candidates 1126761 / parsed 1062837 / real skips 4040

  • evidence: Over-approximating candidate = every col-0, brace-free, ;-terminated line in src/ov_/.c (1,126,761). Hoisted into the carried decl layer: 1,062,837. 4,040 lines MATCH _HOIST_RE but are REJECTED by _SAFE_TYPE and silently dropped — 683 function PROTOTYPES + 3,357 DATA externs. The dropped-base-type histogram: struct 1313, S801563EC 678, B8 544, Blk20 274, S8 272, SV4 268, uint 139, S16 134, ImgRect8 134, Entry 134, DStruct 134, code_fn 21, volatile 3, +8 more. Concrete: src/ov_SC01_000/ov_SC01_000_jr_8015AE2C.c:969 extern S801563EC *func_801563EC(u16 idx); (PROTO — dropped); :990 extern B8 D_80128120[];; src/ov_SC01_000/ov_SC01_000_jr_80178D40.c:2662 extern uint * func_80177EA4(uint *param_1, int param_2, uint param_3, int param_4); (PROTO — dropped); src/ov_SC01_000/ov_SC01_000_jr_8012ACE0.c:224 extern struct BigCopy D_80126DB8;. TWO of the dropped base types are NOT file-local at all: uint (139 drops) and code_fn (21 drops) are BOTH DEFINED IN src/shared/engine_types.h, which engine_core.h pulls into every region — so _SAFE_TYPE rejects include-provided types it has no reason to reject. volatile (3 drops) falls off because the whitelist has const but not volatile. Most damning: the comment ON _SAFE_TYPE (lines 296-301) states these decls DO ride along — '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'. The predicate never implements it. The comment documents a fix that was never applied to the code.

  • blast radius: Corrupt BUILD, and specifically the SILENT half of it. The 3,357 dropped DATA externs are loud (undeclared identifier -> compile error, someone notices). The 683 dropped FUNCTION PROTOTYPES are not: in C89 an undeclared function is implicitly int f(), so the TU still COMPILES — with the wrong return type and lost pointer-ness. This project has already byte-proven that return type drives codegen (cookbook '@class: schedule (delay-slot fill via void return type)'; src/ov_SC01_077/ov_SC01_077_after.c:3301 carries an engine_core.h extern int->extern void flip described as byte-neutral precisely because the return type moves the delay slot). So a dropped prototype is a silent byte-changer on any future cut. Today's split is green only because the source redundantly re-declares externs per fn-group, so most items carry their own decl — this is a latent landmine that fires on the NEXT jr_isolate_all cut, not a live fire.

  • fix: Make the predicate accept a decl whose base type is carried in the SAME layer, which is what the comment already promises. In _file_scope_decls(), collect the carried types first, then widen the test:

    carried = set() for ,,kind,text in items: for block in oss.file_scope_types(text): carried |= set(re.findall(r'}\s*([A-Za-z_]\w*)\s*;|\b(?:struct|union|enum)\s+([A-Za-z_]\w*)', block)) ... base = re.match(r'^\s*(?:extern\s+)?(?:const\s+|volatile\s+)(?:struct\s+|union\s+|enum\s+)?([A-Za-z_]\w)', line) if _HOIST_RE.match(line) and (_SAFE_TYPE.match(line) or (base and base.group(1) in carried | ENGINE_TYPES)): out.append((line.rstrip(), False))

where ENGINE_TYPES is parsed once from src/shared/engine_types.h (that alone recovers uint and code_fn). Also add volatile to _SAFE_TYPE's optional-qualifier group alongside const (it is currently absent — a one-token omission costing 3 drops).

  • assertion to add: assert that EVERY col-0 line matching _HOIST_RE is either hoisted or its base type is provably unavailable; i.e. dropped = [l for l in hoist_matches if not emitted]; assert not dropped, f'{len(dropped)} file-scope decls dropped from the carried layer: {sorted({base_type(l) for l in dropped})}'. Print the base-type histogram of any drop and FAIL — a decl the tool recognised as hoistable but could not place is a bug, never a silent no-op. (This assertion alone would have surfaced all 4,040 today.)

[HIGH] tools/lint_symbol_refs.py :: main() glob src/**/*.c + load_symbols() (2 of 138 symbol files) + no __asm__-label model

  • candidates 967 / parsed 962 / real skips 5
  • evidence: THE TOOL IS CURRENTLY RED AND UNWIRED. python3 tools/lint_symbol_refs.py prints '43 STALE func_ ref(s) — clean-build will FAIL' and exits 1. grep -rn lint_symbol_refs Makefile config/*.mk tools/*.py => ZERO hits outside the file itself; make report (Makefile:148-156) runs progress.py/difficulty.py/dup_report.py and never calls it. Its own docstring says 'Run it after any symbols rename and in make report' — it is not there. All 43 hits are FALSE POSITIVES, from two distinct blind spots: (a) ASM-LABEL ALIASES — src/ov_SC01_000/ov_SC01_000_jr_8013FFD8.c:603 extern void func_8005C324(int dst, int src, int n) __asm__("memcpy"); /* Phase-24: ... keep the non-builtin C name here (else built-in codegen), emit via asm-label */. config/symbols.resident.txt:21-24 documents this as the SANCTIONED pattern ('func_8005C324 aliases the SAME address ... asm("memcpy") label so they emit this same 0x8005C324 call. One symbol resolves both.'). The lint has no model for asm labels. (b) PER-BINARY SYMBOL STACKS — src/libc2_1.c:10 INCLUDE_ASM("asm/nonmatchings/libc2_1", func_8005C324); is flagged because load_symbols() unions symbols.us.txt + symbols.resident.txt into ONE map, but memcpy = 0x8005C324 lives ONLY in symbols.resident.txt, which the main EXE deliberately does NOT stack (symbols.resident.txt:20: 'so main — where 0x8005C324 is DEFINED by MEMCPY.o — is untouched'). Proof it resolves: asm/nonmatchings/libc2_1/func_8005C324.s EXISTS on disk. COVERAGE GAPS on top: (c) the glob is src/**/*.c, so src/shared/.h is NEVER scanned — yet the docstring names 'func_(...) calls in shared macros (src/shared/engine_core.h)' as breakage source (b) of the very bug it exists to catch; engine_core.h holds 10,360 func_/D_ tokens, func_80144B9C.h 180, clearTbl40.h 2. (d) load_symbols reads 2 of 138 config/symbols..txt; the 136 ignored per-overlay files hold 1,598 curated names that can never be checked against.
  • blast radius: Corrupt BUILD. The Phase-24 T5b/T5c class (a symbols rename leaves committed src/ refs dangling; clean rebuilds fail, incremental builds mask it — the R22 failure mode) is presently UNGUARDED: the only detector is unwired, and if someone wired it today it would fail immediately on 43 false positives and be reverted. The header blindness is the sharper hole — engine_core.h is the single most-shared file in the project (1,801 macros ×134 overlays), so ONE dangling func_ in it breaks every overlay's clean build at once, and that file is exactly the one the lint cannot see.
  • fix: Four changes: (1) ASM-LABEL MODEL — before flagging, skip any token on a line carrying __asm__("<name>") where == curated[a]; strip_comments_strings currently blanks the label's contents, so capture it BEFORE blanking (scan the raw line for __asm__\s*\(\s*"([^"]+)"). (2) PER-BINARY SYMBOL STACK — build the curated map per source tree from that binary's splat config symbol_addrs_path list (overlay_src_split.load_ov_syms already implements exactly this walk); src/.c at the root -> splat.us.exe.yaml's stack (which excludes symbols.resident.txt), src//.c -> splat..yaml's stack. This kills the libc2_1 class of false positive AND makes the 136 per-overlay symbol files (1,598 curated names) actually checked. (3) CORPUS — change the glob to src/**/*.[ch] so src/shared/*.h is linted (10,542 tokens currently invisible). (4) WIRE IT — add $(VENV_PY) tools/lint_symbol_refs.py to the report: target in the Makefile (its docstring already claims it is there).
  • assertion to add: Two assertions. (a) COVERAGE: scanned = glob('src/**/*.[ch]'); assert set(scanned) == set(all_committed_sources), f'lint blind to {set(all_committed_sources)-set(scanned)}' — a source file the lint cannot see must fail the run, not be silently omitted. (b) GREEN-BY-DEFAULT: once the false positives are fixed the tool must exit 0 on HEAD, and make report must invoke it — so add a CI-ish guard assert lint_symbol_refs.main() == 0 to the report target. A guard that is allowed to sit red is a guard that does not exist.

[MEDIUM] tools/overlay_src_split.py :: scan_construct() force_decl short-circuit (lines 157-158, 188-194) — and the selftest that cannot see it

  • ✅ FIXED (A9f, §26-A): scan_construct no longer lets force_decl survive a same-line depth-0 ; that is followed by more code — it re-classifies from the remaining text and keeps scanning, so a following definition anchors (audit's "equivalently…" option). NOT the "split into 3 constructs" option — round-trip reconstruction is "\n".join(item_texts) (whole-line chunks), so splitting a physical line into fragments would insert a newline that was a space and BREAK round-trip; the def item instead keeps the leading externs in its (whole-line) text, byte-identical on round-trip, anchored at its own address. def_name was made robust in tandem (name = the identifier before the LAST top-level header ( before {, so it names the DEFINITION, not the first same-line extern; byte-identical on all single-def constructs). R32 coverage oracle added (hidden_definitions(), wired into selftest) — an INDEPENDENT detector of func_XXXX(...){ bodies not anchored, closing the serialisation-blind hole. The audit's line refs were STALE (src rewritten by banking): the two real cases are now ov_SC01_077_after.c:2020 (func_8014FDF4) and ov_SC01_077_jr_8015444C.c:1495 (func_80155FF8). Verified: 2 swallowed → 0; full-fleet regression over 1738 overlay .c = 0 round-trip fails, 0 non-monotonic, 0 non-additive changes (nothing removed/reordered), exactly +2 anchored defs; byte-safe (tool not in the build path; ov_SC01_077 rebuilds d19c9580; neither def straddles a committed subseg boundary).
  • candidates 1021298 / parsed 1021296 / real skips 2
  • evidence: Candidate = every top-level construct parse_overlay_c classifies as a non-anchor DECL (1,021,298 across 811 overlay .c). I classified all of them: 1,003,781 are genuine prototypes, 17,515 are type defs (both justified). TWO contain a { body and are NOT type keywords — i.e. real function DEFINITIONS that were never anchored: src/ov_SC01_077/ov_SC01_077_after.c:2151 extern void func_80150170(void *a0); extern s32 func_8014FE60(void *a0); void func_8014FDF4(struct S8014FDF4 *a0) { ... } and src/ov_SC01_077/ov_SC01_077_after.c:3299 extern void func_80156044(int arg, int a1); int func_80155FF8(int arg, int a1) { __asm__ __volatile__( ... ); }. Both put externs and a DEFINITION on ONE physical line; scan_construct's force_decl fires on the leading extern keyword and returns at the FIRST depth-0 ;, so the definition that follows on the same line is never seen. VERIFIED: parse_overlay_c on that file yields 731 items and neither 0x8014FDF4 nor 0x80155FF8 is among them. AND THE SELFTEST PASSES GREEN ANYWAY: tools/overlay_src_split.py selftest src/ov_SC01_077/ov_SC01_077_after.c exits 0 with 'round-trip exact: True / addressed: 730 / unresolved(non-footer): 0 / non-monotonic transitions: 0'. This is the direct answer to the audit's question about the 404/404, 341,902-item selftest: it is a SERIALISATION check, not a coverage check. Unrecognised text is absorbed into the PREAMBLE of the next anchor, so the round-trip is exact BY CONSTRUCTION even when an anchor is missed. The selftest is structurally incapable of detecting this bug class.
  • blast radius: Corrupt BUILD (latent), plus a metrics hole in the EXEMPLAR overlay. ov_SC01_077 is the family-template source (build_engine_types' default --source; the ×134 remap exemplar), so its item stream is the one everything else is derived from — and it is short two definitions. Two consequences: (1) partition()/jr_isolate_all — the swallowed definition rides in the preamble of the NEXT anchor, so if a cut is ever placed between the swallowed def and its host anchor, the function BODY moves into the wrong object -> .text address shift -> SHA1 break. Not reachable today (neither 0x8014FE60 nor 0x80156044 is a jr vram), so this is a landmine, not a live fire. (2) jr_inventory's realc map is built from parse_overlay_c's def/define items — a definition missing from that map is a banked function the isolator cannot see, which is EXACTLY the mechanism that stranded func_801734BC's .rodata carve (jr_isolate_all lines 228-237 document that incident). Neither of these two is a jr today, so no carve is stranded now.
  • fix: force_decl must not survive a top-level ;. Today it is computed ONCE from the first token of the construct and then latches for the whole scan. Make the top-level ; a construct BOUNDARY and re-classify from the next token. Minimal surgical change in parse_overlay_c's loop, before calling scan_construct: split the physical line on depth-0 ; (comment/string/paren/brace aware — _strip already gives the machinery) and treat each resulting fragment as its own top-level construct. Equivalently, inside scan_construct, on hitting a depth-0 ; while force_decl is set and there is REMAINING CODE on the line, recompute force_decl from the remaining text and continue the scan rather than returning. Then extern A; extern B; void f(){...} yields three constructs: two decls and one anchored def.
  • assertion to add: Two, because the current selftest cannot see this. (a) In parse_overlay_c, after building items: assert not [it for it in items if it[2] not in REAL_KINDS and _has_definition_header(it[3])] — i.e. NO item's preamble may contain a {-bodied function-definition header that is not that item's own anchor; fail loud with file:line. (b) Add a real COVERAGE gate to selftest() alongside the round-trip: crudely count candidate anchors (INCLUDE_ASM lines + col-0 ^\w+\s*\(\s*func_ macro invocations + col-0 } lines) and assert n_anchors >= n_col0_close_braces - n_type_blocks, printing the delta. The existing 'round-trip exact + 0 unresolved + monotonic' triple is 100% green on a file with two missed definitions and must never again be reported as proof of coverage.

[MEDIUM] tools/jr_isolate_all.py :: jr_inventory() — re.fullmatch(r'func_[0-9A-Fa-f]{8}', fn) on the .s basename (line 81)

  • ✅ FIXED (A9g, §26-A): the .s basename is now resolved through the symbol table (overlay_src_split.addr_of()), which handles BOTH func_<hex> and a curated name, so the handwritten listCdBuffer jr (0x80180000, in ov_SC01_084/ov_SC03_108/ov_SC03_118/ov_SC03_119) is no longer dropped. Fixed together with the roster finding below (same rewrite of jr_inventory). (The --only path's own func_-fullmatch — a 2nd instance — is left: it parses user-supplied core names, not the corpus.)

  • candidates 5899 / parsed 5895 / real skips 4

  • evidence: Candidate = every .s under asm/ov_/nonmatchings// whose text references a jtbl_ symbol (5,899). Parsed: 5,895. The 4 misses are all the same function under a CURATED name: asm/ov_SC03_119/nonmatchings/ov_SC03_119_jr_80178D40/listCdBuffer.s, and the same in ov_SC03_118, ov_SC01_084, ov_SC03_108. This is NOT a data label — it is a genuine 0xA64-byte handwritten jr function at 0x80180000 with 2 jtbl references ('/* Handwritten function */ nonmatching listCdBuffer, 0xA64' / glabel listCdBuffer / sra $v0, $v0, 16 ...), and it is INCLUDE_ASM'd from real source: src/ov_SC03_119/ov_SC03_119_jr_80178D40.c:4442 INCLUDE_ASM("asm/ov_SC03_119/nonmatchings/ov_SC03_119_jr_80178D40", listCdBuffer);. Because its name is listCdBuffer and not func_XXXXXXXX, the fullmatch filter drops it and jr_inventory reports it does not exist. (Related, LOW: the same curated name defeats jtbl_carve.all_data_labels' (?:jtbl_|D_)[0-9A-Fa-f]{8} regex in the 33 OTHER overlays where 0x80180000 is data — dlabel listCdBuffer at asm/ov_SC01_000/data/tail.data.s:2380 is invisible to the carve-boundary oracle. I checked exploitability and it is currently NIL: listCdBuffer sits at 0x80180000 in the general data region, never immediately after a jtbl, and jtbl_words' enddlabel-bounded trailing-zero trim clamps end to the true extent regardless of a missed boundary label. The boundary oracle is silently incomplete but the trim accidentally masks it — worth an assertion, not a fix.)

  • blast radius: Lost MATCHES (4 potential banks), and it falsifies the tool's core invariant. jr_isolate_all's docstring and its lines 118-131 establish the rule that EVERY jr in a cut object must get its own region, because 'a region may host AT MOST ONE .rodata carve' — that invariant is what the func_8015AE2C/func_801734BC +33-byte image corruption taught. listCdBuffer is a jr that the tool cannot see, and in these 4 overlays it currently sits INSIDE <ov>_jr_80178D40, sharing a region with an already-banked jr. So the invariant 'every jr has its own region' is false in 4 overlays right now. The moment listCdBuffer is matched and banked, jtbl_carve hits its 'subseg would host NON-CONTIGUOUS .rodata carves' fail-loud (jtbl_carve.py:250-254) and the bank cannot proceed without a hand-isolation. Fail-loud, so no byte corruption — but 4 banks are blocked and the blocker will present as a mysterious carve error rather than a naming gap.

  • fix: Resolve the .s basename through the symbol table instead of demanding the func_ shape. jr_inventory already loads syms = oss.load_ov_syms(ov) (line 88) for the realc walk — hoist it above the asm scan and use overlay_src_split.addr_of(), which handles BOTH the func_ shape and a curated-name lookup:

    syms = oss.load_ov_syms(ov) for p in glob.glob(f'asm/{ov}/nonmatchings//.s'): if not re.search(r'jtbl_[0-9A-Fa-f]{8}', open(p).read()): continue fn = os.path.basename(p)[:-2] a = oss.addr_of(fn, syms) # was: re.fullmatch(r'func_[0-9A-Fa-f]{8}', fn) if a is None: unresolved.append(fn) # and FAIL, see the assertion continue asm_jr[a] = fn

Same class of fix in jtbl_carve.all_data_labels: accept any (?:dlabel|glabel)\s+(\S+) and resolve the name via the overlay's symbol stack, so a curated data name still bounds a carve.

  • assertion to add: assert not unresolved, f'jr_inventory: {len(unresolved)} jtbl-referencing .s files whose symbol could not be resolved to an address: {unresolved}' — a jr function the isolator cannot NAME must abort the run, never be silently dropped from the cut list. Pair it with a post-plan invariant that is the tool's actual contract: for every -O2 object being cut, assert len(jr_in_region) <= 1 for each emitted region — i.e. re-derive the jr set from the asm AFTER planning and assert no region hosts two, so a jr the inventory missed is caught by the invariant even if the naming fix is ever regressed.

GROUP: family-engine

Headline: family_remap.extract_unit cannot read a DEFINE_func macro body out of src/shared/engine_core.h — so 93 of the 218 "matched" h_seq exemplars (43%) are PHANTOM and 1834 still-stubbed, PURE/IMM-clean, symbol_map-clean member templates are never even attempted by the sweep.

Scanners measured: 23 Verified CLEAN: MEASURED AND CLEAN — do not spend time here: (1) dedup_propagate.onboarded_overlays — the ^OVERLAY_BINARIES\s*:=\s*(.*)$ regex parses 134 overlays; there are exactly 134 src/ov_* dirs and config/overlays.mk contains zero += and zero line-continuations, so the single-line assumption holds. (2) family_sweep.load_sigs (glob .run/sig.ov_*.jsonl) — 139 sig files exist, 134 parsed; the 5 excluded are sig.SLUS_007.26, sig.resident, sig.resident_image, sig.sep8_SLUS_007.26, sig.aug31_USA_DEMO.EXE — genuinely not overlays, fully justified. (3) family_sweep's hard-reg PIN GUARD (__asm__\s*\(\s*"\$) — crude detector (any line with register + asm) finds 18,319 candidates across src/ + all 5 raw-crack draft dirs, the guard catches 17,908; all 411 gap items classified: 408 are COMMENT lines mentioning "register-asm", and 3 are register s32 r asm("$2"); inside src/shared/engine_core.h macros, which this guard never scans. Zero real skips. (Latent-only: the bare asm("$N") and __asm__ volatile ("$N") spellings would evade it — worth widening to \b(?:__asm__|asm)\b[^;]*"\$ for free.) (4) dedup_propagate's macro-safety plan filters (the "//" in l / trailing-backslash / (\b(struct|union)\s+\w+\s*\{)|(\btypedef\b) rejects) — measured on ov_SC01_077's visible defs: 18 candidates (def, unregistered, h_exact reach>=2), and ALL 18 survive all three filters. They cost nothing today and their skips are printed, not silent. (5) family_remap.apply_remap — single-pass simultaneous longest-first substitution; no coverage gap found. (6) family_remap.classify_member / reg_fields / imm_map_tier1 — their skips (STRUCT 137, LEN, "unresolved immediate: asm-ambiguous" 179 / "not-in-C" 8 / "non-imm diff op=0xf" 3) are all COUNTED and REPORTED by the sweep, i.e. declared capability limits, not silent skips. Likewise the sweep's largest single loss bucket, pinned-exemplar (3,427 members), is a declared §42e refusal. (7) extract_unit's _DECL_LAYER_END guard and its post-R14 trailing-comment strip both behave correctly: over 2,030 matched functions in 4 overlays, extract_unit produced ZERO malformed units (no unbalanced braces, no unit containing two function definitions) — the R14 declaration-swallowing bug is genuinely fixed. NOT MEASURED (deliberately): dedup_propagate.compiles_standalone — exercising it writes .run/dpcc/t.c and forks cc1, which I would not do while the fleet build is mid-flight; its skips are at least printed ("[skip] N not self-contained"), so it is not silent, but its true rejection rate is unaudited. Two LOW/latent items not worth a finding slot: dedup_propagate's o0_skip guard tests _p.name.endswith("_o0.c") while _o0b.c is ALSO compiled -O0 (Makefile WHALE_O0B_OBJS) — currently vacuous because every _o0b.c contains 0 inline defs, but it is a live trap the moment a second whale-region function is matched; and find_site's stub_line() hardcodes nonmatchings/{ov}, so it can never recognise a stub in a split file ({ov}_after, {ov}_jr_*) — harmless today because no caller consumes its 'stub' verdict, but it will silently mislead the moment one does.

[CRITICAL] tools/family_remap.py :: extract_unit (glob src/<ov>/<ov>*.c + the ^\s*[A-Za-z_][\w *]\bfunc_\s( def scan)

  • candidates 218 / parsed 122 / real skips 96
  • evidence: Ground truth = .run/family_hseq.json exemplars with kind matched/matched-ov077 (218). extract_unit returns a unit for only 122. Classified all 96 of the gap: (a) 93 families = the exemplar is matched via a DEFINE_func_() macro whose BODY lives in src/shared/engine_core.h, not in any .c — extract_unit only globs src//*.c, so it returns None. e.g. src/ov_SC01_077/ov_SC01_077_after.c:1797 DEFINE_func_8014FDF4() (family 0x80151FB4 = 268 members; 0x80174784 = 243; 0x8016B448 / 0x80172780 / 0x80146AB4 = 134 each) with the body at src/shared/engine_core.h:13266 #define DEFINE_func_8014FDF4() \. (b) 3 families = the exemplar is a NAMED INCLUDE_ASM stub (see the stub_map finding). ZERO justified exclusions in the gap.
  • blast radius: Lost MATCHES. The 96 phantom families carry 2157 candidate members; of those, 1834 are measured to be (i) still an INCLUDE_ASM stub, (ii) classify_member PURE/IMM, (iii) symbol_map-clean, (iv) unpinned — i.e. they would be STAGED and byte-gated today and are instead dropped before the first build. Bands: mid=801, tiny=1033 — this lands squarely on the NEXT queued task ("Task 9 — mid-band"). It also corrupts METRICS: the manifest's "218 matched exemplars" is really 122; every sweep run silently spends its skip budget on families that can never produce a draft. These are the HIGHEST-reach functions in the project (they were dedup'd into engine_core.h precisely because reach>=2), so the loss is concentrated in the largest families.
  • fix: In extract_unit, after the per-.c scan fails, fall back to the shared macro: search src/shared/engine_core.h for ^#define DEFINE_func_<ADDR>\(\) \\\n((?:.*\\\n)*.*)$ (re.M) and reconstruct the unit by stripping the trailing line-continuation backslashes and the 4-space indent from each captured line — make_macro() built it from exactly externs + def block, so the reconstructed text IS the unit extract_unit wants. Return (body, 'src/shared/engine_core.h'). Also scan the other shared bodies (src/shared/func_*.h) the same way. NOTE: the caller in family_sweep must keep using the SIBLING's src_rel for the reconcile/scope_data step (the exemplar's cf is now a header, not a TU).
  • assertion to add: extract_unit must never return None for an addr that the caller believes is MATCHED. Add: assert unit is not None, f"{ov} func_{addr:08X}: believed matched but no source form found (stub? macro? named stub?)" — and in family_sweep/family_hseq, assert that every exemplar classified matched/matched-ov077 yields a unit, failing loud with the count (phantom exemplars: N of M) instead of silently incrementing a skip counter.

[HIGH] tools/dedup_propagate.py :: overlay_files (hardcoded suffix allowlist for suf in ("_a", "_o0", "_o0b", "_after"))

  • candidates 811 / parsed 404 / real skips 407
  • evidence: Over-approximating detector = every file matching src//*.c across the 134 onboarded overlays: 811 files. overlay_files() returns 404 of them. The 407-file gap is ENTIRELY the Phase-26 jr-isolation splits, which the allowlist predates: _jr_8015AE2C ×134, _jr_801734BC ×134, _jr_80178D40 ×134, _jr_8017FCB0 ×2, _jr_80182268 ×2, _jr_8017BEBC ×1. Those invisible files contain 36,135 INCLUDE_ASM stubs and ~32,000 inline defs — half the corpus. Zero justified exclusions. This is the SAME bug as the Phase-24 _o0b/_after miss, re-opened by the new split family.
  • blast radius: Lost MATCHES (free ones). overlay_files gates source_text/find_site/apply_plan/struct_check/reconcile_caller_extern, i.e. ALL of dedup_propagate. Measured on the propagation source ov_SC01_077: 435 of its 689 inline defs live in jr* files and are invisible to --auto-from. Two of them are h_exact reach-134 AND still INCLUDE_ASM stubs in 133 overlays each — 266 free ×N member banks that --auto-from cannot even nominate today: func_801749C8 (105 ins, def at src/ov_SC01_077/ov_SC01_077_jr_801734BC.c:2916, stub at src/ov_SC01_000/ov_SC01_000_jr_801734BC.c:2681) and func_80165CA0 (99 ins, def at src/ov_SC01_077/ov_SC01_077_jr_8015AE2C.c:4117, stub at src/ov_SC01_000/ov_SC01_000_jr_8015AE2C.c:3922). Every future jr-region crack is likewise un-propagatable. (--addr 0x801749C8 --source-overlay ov_SC01_077 also fails: it fails CLOSED with "not inline-def", so no registry corruption is reachable — it just does nothing.)
  • fix: Replace the allowlist with a glob: out = [(c_path(ov), ov)] + [(p, p.stem) for p in sorted(ROOT.glob(f"src/{ov}/{ov}_*.c"))] — the asm_subdir is always the file stem (ov_SC01_000_jr_8015AE2C.c -> asm subdir ov_SC01_000_jr_8015AE2C), which is exactly the invariant the existing four entries already satisfy. Do NOT add _jr_* to the tuple: the next split family will re-open the same hole. MUST be fixed together with the find_site K&R bug below — fixing overlay_files alone exposes the jr files, whose biggest matched functions (func_8015AE2C, func_80166994) are K&R and would still be silently dropped.
  • assertion to add: assert {p for p,_ in overlay_files(ov)} == set(Path(f"src/{ov}").glob(f"{ov}*.c")), f"overlay_files missed {...}" — a hardcoded suffix list must be proven exhaustive against the directory on every call, and fail loud when a new split kind appears.

[HIGH] tools/dedup_propagate.py :: find_site (defre ^\s*[A-Za-z_][\w *]\b{s}\s([^;{{])\s({{)?\s*$ + the "brace on this line or the next non-blank line" rule)

  • candidates 2030 / parsed 2002 / real skips 21
  • evidence: Ground truth over 4 overlays (ov_SC01_077/000, ov_SC02_000, ov_SC06_008), scanning ALL files (so this isolates the regex from the overlay_files bug): M = addrs in the sig that are neither an INCLUDE_ASM stub nor a DEFINE macro = 2030. find_site returns kind 'def' for 2002. Every one of the 28 gap items classified: 16 = K&R OLD-STYLE definitions — the sig line matches, but the next non-blank line is a parameter DECLARATION, not '{', so the if not lines[j].lstrip().startswith("{"): continue drops it silently. src/ov_SC01_077/ov_SC01_077_jr_8015AE2C.c:1327 s32 func_8015AE2C(arg0) (THE ×134 whale), :4357 s32 func_80166994(param_1, param_2, param_3, param_4), ov_SC01_077_a.c:2747 s32 func_80133CD4(arg0, cmd, base, arr), ov_SC01_077_after.c:4279 s32 func_8015A3C8(arg0). 5 = signature not terminated by ')' on its own line: ov_SC01_077_after.c:3420 void func_80157158(s32 a0, u16 a1, u16 a2, s32 a3, (multi-line params, reach-134) and ov_SC01_077_o0.c:36 void func_8013B7AC(int a0) { D_801DAA08[a0].f0 = 0; } (one-line body). 7 = JUSTIFIED (no source form at all: func_80144B9C lives in src/shared/func_80144B9C.h; 2 addrs are splat-merged into a neighbouring asm chunk). family_remap.extract_unit finds ALL of these (its pattern has no end-anchor and its brace scan is a pure forward count) — so the two tools disagree, and only find_site is wrong.
  • blast radius: Lost MATCHES — currently LATENT (0 live lost banks: the 4 affected reach>=2 fns are already banked everywhere), but it is the second half of the overlay_files fix. K&R is the project's house style for exactly the hard, high-reach functions the cookbook says to write that way, and the jr* files are full of them; the moment overlay_files is fixed, find_site will silently drop the biggest prizes in the newly-visible half of the corpus. It also silently caps the o0_skip guard (which is built from find_site).
  • fix: Two changes to find_site's defre block. (1) Relax the anchor so a multi-line signature is admitted: match ^\s*[A-Za-z_][\w \*]*\b{s}\s*\( (as extract_unit does) and then, if the line has no ')' , consume forward lines until the paren depth returns to 0 — that consumed span is the signature. (2) Replace the strict "next non-blank line must start with '{'" test with a scan forward over K&R parameter-declaration lines (lines matching ^\s*[A-Za-z_][\w \*]*\b\w+\s*(\[[^\]]*\])?\s*;\s*$) until the first line containing '{' — if a ';'-only prototype terminator or another def signature is hit first, it is a prototype (reject). Add the single-line-body case by allowing the line itself to contain '{'.
  • assertion to add: find_site and family_remap.extract_unit must agree: assert (find_site(...)[0]=='def') == (extract_unit(ov,addr)[0] is not None) for every non-stub, non-macro sig addr — run it as a fleet-wide self-check in the tool's --check-only path and fail loud on any disagreement. Independently: assert defs_found + stubs_found + macros_found + known_shared == len(sig) per overlay.

[HIGH] tools/family_remap.py :: symbol_map (prefix chosen from the RELOC KIND: pfx = "func_" if ke == "call" else "D_") — fed by reloc_targets, which labels every lui/%lo pair "data"

  • candidates 7 / parsed 0 / real skips 7
  • evidence: Over-approximating oracle = splat's own .s ground truth. grep -rhoE '%lo\(func_[0-9A-Fa-f]+\)' asm/ov_SC01_077/nonmatchings/ = 7 occurrences: a FUNCTION's address taken via lui/%lo (address-taken callbacks), e.g. asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80178D40/func_8017F290.s %lo(func_8017E1D4), .../func_8017D98C.s %lo(func_8017DA08), .../ov_SC01_077_jr_801734BC/func_80173A60.s %lo(func_80173B4C). reloc_targets emits kind='data' for all of them, so symbol_map names them D_ — but the exemplar's C writes func_<ADDR>. apply_remap's word-bounded rx therefore matches NOTHING and the substitution silently no-ops. 0 of the 7 are named correctly.
  • blast radius: Lost MATCHES, live TODAY. Measured over the manifest's non-phantom matched families: 3 families (ov_SC01_077 exemplars 0x8017D840, 0x8017DCEC, 0x8017DD9C) x 37 still-stubbed members produce a symbol_map containing a D_-keyed FUNCTION address that the C body writes as func_. e.g. ov_SC01_077 0x8017DCEC -> ov_SC01_080 0x8017ED48: map has D_8017DE84 -> D_8017EEE0 while the unit says func_8017DE84. The staged sibling body keeps the EXEMPLAR's function pointer, the whole-binary gate rejects it, and the loss is booked as a "byte failure" — indistinguishable from a genuine compiler wall. It also burns a bisect slot in every chunk it lands in.
  • fix: Name the symbol by WHAT THE ADDRESS IS, not by how it was loaded. In symbol_map, load the overlay's sig once (the set of function addrs) and use pfx = "func_" if (ke == "call" or ae in func_addrs) else "D_". Belt-and-braces (and cheaper): for a data-kind reloc emit BOTH keys into the table — m[f"D_{ae:08X}"] = f"D_{at:08X}" AND m[f"func_{ae:08X}"] = f"func_{at:08X}" — addresses are unique, so only the token that actually appears in the C can ever match, and apply_remap is a single simultaneous pass so the extra key is free.
  • assertion to add: After apply_remap, assert that NO per-overlay exemplar address survives in the output: for ex_addr in symbol_map keys: assert not re.search(rf'\b(func_|D_){ex_addr:08X}\b', remapped) — a remap that leaves an exemplar-only symbol in the sibling body is a tool failure, not a byte failure, and must fail loud rather than be handed to the gate.

[MEDIUM] tools/family_remap.py :: gather_externs (^\sextern\b[^\n;{}]\b{sym}\b[^\n;{}]*; over the exemplar TU — single-line only)

  • candidates 199 / parsed 190 / real skips 9
  • evidence: Over-approximating detector = for every func_/D_ ref in a matched exemplar's unit that the unit does not itself declare, is it declared ANYWHERE at file scope in the TU (any line ending in ';', excluding assignments/calls)? M=199 across the matched-exemplar corpus; gather_externs carries N=190 extern lines. All 9 gap items are the same shape: a MULTI-LINE comma-separated extern. src/ov_SC01_077/ov_SC01_077.c:271-272 — extern unsigned char D_801DAA78, D_801DAA79, D_801DAA7A, D_801DAA7B, D_801DAA7C,\n D_801DAA7D, D_801DAA7E, D_801DAA7F, D_801DAA80;. The [^\n;{}]* class cannot cross the newline, so line 271 has no ';' and line 272 has no extern — the ENTIRE group of 9 symbols is invisible, including the ones on the first line.
  • blast radius: Lost MATCHES. The exemplar that references them, ov_SC01_077 func_8013D178, is a band=substantial family with 133 members. Its remapped siblings are staged with NO declaration for D_801DAA78..D_801DAA80, so every sibling TU fails to compile (D_801DAA7D undeclared), which fails/bisect-storms the whole (overlay,split) gate group it is chunked with — so the damage is not even confined to the one function. Booked as a compile failure, i.e. invisible.
  • fix: Make the extern scan statement-oriented rather than line-oriented. Pre-join the TU's file-scope declarations by scanning from each ^\s*extern\b to the next unbraced ';' (a small hand loop, or regex with re.S: ^\s*extern\b[^;{}]*?; with re.M|re.S), then test \b{sym}\b against each joined statement and carry the joined statement verbatim (newlines preserved — they are legal C). The same single-line assumption exists in dedup_propagate.reconcile_caller_extern's rx ([\w \t\*]*?) and in find_site's preceding-extern walk (^\s*extern\b.*;\s*(/\*.*\*/\s*)?$) — fix all three from one shared helper.
  • assertion to add: assert every func_/D_ symbol referenced in the emitted draft is either declared in the draft, declared in the target TU, or resident/common.h — i.e. gather_externs should return a second value unresolved and family_sweep should refuse (loudly, with the symbol names) rather than stage a body with a symbol it could not find a declaration for.

[MEDIUM] tools/family_sweep.py :: stub_map (INCLUDE_ASM("([^"]+)",\s*(func_[0-9A-Fa-f]+)))

  • candidates 59226 / parsed 59123 / real skips 3
  • evidence: Over-approximating detector = every line in src/ov_/.c containing the token INCLUDE_ASM: 59,226. stub_map's regex extracts 59,123. The 103-item gap classified in full: 3 = comment lines merely mentioning INCLUDE_ASM (justified); 100 = REAL stubs whose symbol is NOT named func_XXXXXXXX — INCLUDE_ASM("asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_80178D40", listCdBuffer); (src/ov_SC01_005/ov_SC01_005_jr_80178D40.c:4444, and 99 more overlays). 97 of those 100 are JUSTIFIED (0x80180000 is not a function in those overlays' sigs, so the address-keyed engine never asks about it). 3 are REAL SKIPS: in ov_SC03_094, ov_SC06_008 and ov_SC02_041 the sig DOES contain a function at 0x80180000, and it is stubbed under the name listCdBuffer — so stub_map reports "not a stub", and family_hseq therefore classifies it as a MATCHED exemplar.
  • blast radius: Corrupt METRICS + lost MATCHES. Three PHANTOM matched-exemplar families are created (ov_SC03_094 0x80180000 band=mid with 19 members, ov_SC06_008 with 7, ov_SC02_041 with 2 = 28 members). extract_unit returns None for all of them, so every run of the sweep re-nominates them, produces nothing, and books 28 silent skips; the "matched exemplars" headline count is inflated. Symmetrically, if such a function ever became a real sweep TARGET, to_addr not in stubs[ov] would silently classify it as already-matched and skip it.
  • fix: Widen the symbol group to any C identifier and resolve it to an address via the overlay's symbol table: INCLUDE_ASM\("([^"]+)",\s*([A-Za-z_]\w*)\) , then addr = int(m[1].split("_")[1],16) if m[1].startswith(("func_","D_")) else symbols[ov][m[1]] (config/symbols..txt already has listCdBuffer = 0x80180000;). Skip only names that resolve to no address, and COUNT them.
  • 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).

✅ FIX LEDGER — what has been repaired (Phase 26-A, session 9)

This document opened as diagnosis only. It is now also the ledger. Every row below is COMMITTED and VERIFIED; the byte-gate stayed green throughout, and nothing here was accepted on a prediction.

The root cause, and the shape of the fix

All 28 round-2 findings collapse to ONE defect repeated ~10 times:

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.

So the fix is not ten repaired regexes. It is one derived oracle (tools/corpus.py) and ~10 deleted scanners — the "best outcome is a deleted scanner" rule (R33), applied at scale.

# tool was now commit
A1 dedup_integrate a fail-closed gate that printed false greens: 7 groups named a macro that did not exist; a missing sig → "0 validated, 0 failed" + exit 0; the bank claim never checked all three fail-closed with negative controls; the bank claim derived from the build invariant bb65d3634
A3 tools/corpus.py (new) — the single derived oracle: files/stubs/matched/asm-path, coverage-asserted. Plus make audit-corpus, a second oracle that can disagree with the corpus f7b7399eb
A3 build_fuel_manifest 3-file allowlist → 30 of 264 stubs; 91.6% of all remaining gain invisible; 117 of 127 reach-134 fns never nominated 263 targets · 127 reach-134 · 994,633 ins (the audit's predicted figure to the unit) a302908f2
A3 wave_targets REGION_SUB 3-entry dict → 78 of 87 targets handed an asm path that does not exist (wasted attempts booked as matching failures → reserved_walls() → false walls) 0 of 263 missing; raises rather than guess a302908f2
A3 harvest_verify + gate_stage the byte-gate could see ONE TU: 4.9% of ov_SC01_077, 96.6% of fleet stubs unreachable, 1,290 of the grinder's own 1,298 queued fns unbankable 100%; each draft spliced into the TU that holds its stub; verdict untouched 70a99aa9f
A3 family_manifest matched-set from one overlay → the endgame plan advertised 2,758 families / 11.0 MB; 1,071 (62% of the byte-weight) were ALREADY MATCHED 1,495 / 3.9 MB — derived from the invariant; the dedup-hash scanner deleted 9794b13ed
A3 family_hseq func_-only stub regex → 3 phantom "matched" exemplars corpus-derived (+100 curated stubs); stale hardcoded baseline labelled 9794b13ed
A3 census_conflict_callees wave scope 2 when the truth is 57 (96% under-report) 0/57; DELETED with derive_canonical_sigs (its only consumer) — R33, the dead Phase-17 chain ceases to exist; reconcile_tu answers it from the build 9794b13ed→A9d
A3 tools/cdecl.py (new) + make audit-cdecl fifteen tools each carried their own regex model of "what is a C declaration", and they disagreed; all fifteen were blind to fn-ptr / sized-array / multi-declarator decls ONE recursive-descent parser of the C declarator grammar — total by construction, not by shape enumeration. 2,952,246 statements → 2,731,521 declarators, 0 parser defects; 50,405 declarations round-tripped through the real cross-gcc, 0 rejected. Cookbook §51g (LAWS 4–8) (this commit)

What cdecl measured that the audit had not (all new, all reproducible via make audit-cdecl)

  1. reconcile_decls.DATA_DECL_LINE_RE finds ZERO declarations in engine_core.h — it is line-anchored (^…;$) and every decl in that header ends in a \ continuation. Its "authoritative tier" over the shared header has always been empty; every symbol falls through to the fleet-plurality vote. (The round-1 finding measured its draft-side gap at 4.6% and never checked the oracle side.)
  2. Two scanners count BLOCK-SCOPE externs as file-scope canonicals. gen_harvest_targets.DATA_DECL_RE and sig_unify.DATA_DECL_RE each report 6 symbols (D_800636C8, D_800AE6A8, D_800DE5A0, D_80126AE4, …) that are declared inside a function body in engine_core.h (byte-proven: the macro's body { opens at line 23366, the extern is at 23371). A block-scope decl is private to its function — promoting it to ambient truth is exactly the §8d conflicting types for D_801812A4 confusion.
  3. A recovery tool once wrote non-C into drafts: extern if ((func_80029178(0x119) & 0xFF) != 0); appears in 33 drafts, only in -canon/-recanon/-uni/-sigfix output dirs (the pre-recovery draft has none), and gcc rejects it outright. R14 blast-radius check: the source bug was already fixed in Phase 19 — today's oracle emits 0 garbage over 300 signatures — so this is dead historical residue, not a live defect. Mechanism confirmed, consequence nil. But note what it cost at the time: a draft that cannot compile fails the byte-gate and reads, downstream, as an intrinsic compiler wall.
  4. tu_ambient's function regex drops any callee with a fn-ptr parameter. Its param class is [^()]*, so extern void func_8012A568(void (*a0)(void)); — a real declaration in ov_SC01_077 — lands in no bucket at all: not funcs, not data, not typedefs.

🔴 A3e — THE ONE THAT MATTERS: gate_stage pinned the byte-gate back to 4.9% — of its own fix

tools/gate_stage.py:315

summary = run_gate(a.drafts, binary=b,
                   src=a.src or f"src/{b}/{b}.c",     # <- ALWAYS the main .c

src restricts the byte-gate to ONE translation unit. _gate1 then does if src: cmd += ["--src", src] — and src is always truthy.

A3 fixed harvest_verify to derive each draft's home TU when --src is omitted, taking the byte-gate's reach from 4.9% to 100%. gate_stage never omits it. The fix was silently neutralised by its own caller, and gate_stage — the primary banking path, the thing every wave, the grinder, the orchestrator and bulk_harvest call — remained structurally incapable of banking 250 of ov_SC01_077's 263 stubs (95%).

How it hid. harvest_verify cannot splice a draft whose stub is not in the TU it was pointed at, so those drafts simply never verify. They are then logged as near/failed — i.e. as matching problems — and the wave reports a low close-rate. A tool that cannot bank a function is indistinguishable, in every log this project keeps, from a function that cannot be banked. Proof: func_80129C40 — gate_stage rejected it; harvest_verify run directly (no --src) VERIFIED it byte-identical and banked it. Same draft, same gate, same second.

And a counting bug that hid the hiding (gate_stage:261): when match_one says MATCH but the whole-binary gate rejects, the record is logged status="near" and the counter is never incremented. A run of 63 such drafts printed banked 0, near 0, failed 0 — three zeros that do not sum to 63, printed for phases, and nobody ever added them up.

The backlog says how big this is. 1,588 entries at closeness == 0 (body byte-exact per match_one, whole-binary gate rejected). 1,215 have been banked since by other paths — leaving 373 still-open stubs whose bodies are already byte-exact, sitting in a backlog that describes them as unrecoverable.

A3c — the first consumer migration (cast_call_sites), and what it measured

[CRITICAL] the recovery passes canonicalize against the WRONG TU --src-file is an optional hand-passed flag; unset it defaults to src/<ov>/<ov>.c, and no caller knows about the Phase-26 _jr_<ADDR> carves. Measured: ov_SC01_077 has 263 open stubs across 12 TUs — only 13 in the main .c. So 95.1% of drafts were reconciled against a translation unit that would never compile them, while harvest_verify (A3) correctly spliced them into the right one. Fixed by DERIVING the TU from corpus.stubs() (the INCLUDE_ASM line is self-describing). Repair reach 8 → 58 of 196 drafts (7×).
[HIGH] the conflict predicate was wrong in both directions norm_sig/_norm_type collapse the int family to one token, so a signedness change reads as "already compatible" and gets no rewrite — while cc1 rejects that redeclaration. Replaced by cdecl.compatible(), validated against the real cc1 on 1,485/1,485 live corpus pairs.
🏆 [FINDING] the Phase-15 narrow-param wall rests on a false premise The () no-prototype rule is ORDER-DEPENDENT on cc1: void X(s16); void X(); compiles; only the reverse order fails. Phase 15 closed "the 159 arity/narrow-param conflicts" as "no clean deterministic fix". The stated cause does not hold. → A10 re-test target.
[FINDING] cc1 ≠ modern gcc ≠ the C standard They give three different answers on typedef redefinition, qualifier mismatch, and the no-proto rule. Any compiler rule validated against modern gcc is validated against the wrong compiler. See cookbook §51g LAW 9 for the table.

AND THE NULL RESULT, RECORDED HONESTLY (P9/R14). Those 58 repaired drafts banked ZERO functions. The historical draft tail fails on codegen, not plumbing — func_801387B8, which finding F1 blames on a single unparsed [4], is really 67/100 instructions off with a $s0/$s1 swap (that F1 claim does not reproduce on today's tree). What the fix genuinely buys: 52 drafts moved from "won't compile" to "compiles, N instructions off" — from an invisible failure that reads as a compiler wall to a scored near-miss the permuter can act on. Real, but narrower than it first looked. Three separate times in one session a confirmed mechanism produced a null consequence — verify the blast radius, not just the defect.

Still open (round-2 findings not yet fixed)

masked_diff/match_one (155 provably-wrong closeness scores — feeds false walls) · harvest_verify._TD (frozen 10-name typedef allowlist; 22 open stubs never got to compile) · worklist/exemplar_miner/difficulty (inherited) · jr_isolate_all.jr_inventory (banked-roster read from an ephemeral gitignored scratch file) · jtbl_carve.all_data_labels · canon_draft_decls · plus the whole round-1 fix campaign (the cdecl class, reconcile_tu wiring, the family engine, dedup_propagate, build_engine_types, split-infra, lint_symbol_refs).


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 (<ov>.c, _a, _o0, _o0b, _after, and the Phase-26 _jr_<ADDR> 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/<ov>/<ov>.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_<ADDR>). 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("...", <any C identifier>); 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=<bin> — 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 ... <name>; 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 cc7ee23d0), 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 b09916943 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/<ov>/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/<ov>/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 <sel> -> 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/<ov>/<ov>.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<j and was dropped by _SAFE_TYPE, and which are not re-declared at any scope inside region j or in the carried header. RESULT: exactly 1 LIVE STRAND, not thousands. ov_SC01_077 / object ov_SC01_077_jr_8016AB6C, region @0x8016AE5C uses D_800AE620 (src/ov_SC01_077/ov_SC01_077_jr_8016AB6C.c:3200 localMatrix = (*(Blk20_8016DF5C*)&D_800AE620);) whose ONLY file-scope decl is line 1952 extern Blk20 D_800AE620; — dropped. That region .c would not compile (gcc-2.7.2: undeclared identifier; note it does NOT print the word 'error'). The other 5,565 are absorbed because the house style redundantly re-declares externs inside each function group, so the decl travels with its own item. So: LATENT-but-armed on exactly ONE overlay — and it is ov_SC01_077, THE EXEMPLAR OVERLAY of the entire ×134 program, in an object (jr_8016AB6C) that already carries a banked core and has two further un-cut jr (0x8016AE5C, 0x80171B4C). The next core banked into that object fires it, and jtbl_family_bank reports it as a bare isolate-fail/gate-fail. Honest characterisation: a 5,566-line silent drop whose measured consequence today is 1 lost bank, not a fleet-wide wall. I am reporting the measurement, not the scare number.
  • R33: NO — this scanner cannot be deleted. It is a source TRANSFORM (it must physically reconstruct a TU's decl environment), not a metric or an oracle, so there is no invariant to derive it from; the SHA1 gate remains the arbiter, as the docstring says. But the R33 SPIRIT applies to the FILTER: _SAFE_TYPE is a heuristic that makes the transform LOSSY, and a lossy transform under a byte-gate produces silent LOST WORK (gate-fail) rather than conservatism. The correct posture — harvest_verify's — is to be maximally inclusive and let the gate be conservative. Delete the WHITELIST, keep the scanner.
  • fix: Drop the and _SAFE_TYPE.match(line) conjunct at jr_isolate_all.py:336 and carry EVERY col-0 _HOIST_RE decl. The tool's own byte-neutrality argument ("decls emit no code"; _render_region's docstring, :353-364) already licenses this: every carried decl coexisted with every definition in the ONE original TU, so no NEW conflict can arise, and file_scope_types() (already measured working) emits the file-local typedefs FIRST. The whitelist trades a hypothetical compile error for a CERTAIN silent drop — the wrong direction. Separately, make the scan construct-based rather than line-based so the 5 multi-line decls are captured (parse_overlay_c already gives brace/comment-aware construct bounds via scan_construct — reuse it).
  • assertion (R32): R32: _file_scope_decls must return a (emitted, skipped) pair, and build_new_config must sys.exit if skipped is non-empty for any item that lands in a region other than the last — i.e. a col-0 decl that the tool declined to carry forward is a hard error, never a silent drop. Coverage assertion: candidates == emitted + justified(anchors, K&R params, typedefs-routed-to-file_scope_types); anything else aborts.
  • skeptic: RAN, in /home/musashi/bfm-decomp/.run/audit/skeptic/: (1) count2.py — re-derived the corpus counts myself over all 1604 src/ov_/.c using the tool's own oss.parse_overlay_c with the real per-overlay oss.load_ov_syms, then applied J._HOIST_RE and J._SAFE_TYPE directly. Result: emitted (HOIST&SAFE) = 1,737,749 and REAL SKIPS (HOIST & !SAFE) = 5,566 across 1,060 distinct decl texts, parse-fail=0. That matches the claim's parsed=1737749 / real_skips=5566 / 1060-distinct to the digit. (My first pass got 4,954/848 with 101 parse failures — that was MY bug, I passed an empty syms set; corrected, it reproduces exactly.) (2) Justified-exclusion probe: s32 arg0; (K&R param) gives _HOIST_RE=False, so K&R params are NOT inside the 5,566 — the claimant did not pad the number. INCLUDE_ASM and typedef/struct lines likewise excluded. The gap items ARE real, safe-to-hoist decls: Blk20 is typedef'd in src/shared/engine_types.h, an INCLUDED HEADER, so extern Blk20 D_800AE620; names an include-provided type and is unconditionally safe to hoist — _SAFE_TYPE's own stated rationale (file-local types) doesn't even apply. Code genuinely contradicts its comment at :296-298. (3) Evidence exists: ov_SC01_077_jr_8016AB6C.c:1952 = extern Blk20 D_800AE620;, :3200 = the use; multi-line decls at ov_SC01_077.c:269 and ov_SC01_077_after.c:382. (4) blast.py — my OWN independent simulator over J.plan() + J._partition() for all 134 overlays, checking per region for symbols used but whose only file-scope decl was dropped and not re-declared locally or in ambient: exactly 1 LIVE STRAND, ov_SC01_077 / ov_SC01_077_jr_8016AB6C / region @0x8016AE5C / D_800AE620 — identical to the claim. (5) Reachability: 0x8016AE5C IS in the default J.plan('ov_SC01_077') cut list, so it is reachable. DOWNGRADE REASON: nothing is broken today. src/ov_SC01_077/ov_SC01_077_jr_8016AE5C.c does not exist; every existing region .c already carries its decls and the build is green. 5,565 of 5,566 drops are absorbed by the house style's redundant per-fn-group re-declarations. The defect is real, the mechanism and counts reproduce exactly, and the fix (drop the whitelist, keep the scanner) is correct — but the measured consequence is ONE latent, not-yet-fired gate-fail on one object of one overlay. A latent bug with a blast radius of 1 is MEDIUM; HIGH appears inherited from the 5,566 scare number rather than from the damage the claimant themselves measured.

[MEDIUM] tools/family_hseq.py — DOWNGRADED

  • scanner: the stub scanner: re.findall(r'INCLUDE_ASM\([^)]*,\s*func_([0-9A-Fa-f]+)\)', txt) (tools/family_hseq.py:38) — the func_-only regex that decides matched vs unmatched for the entire h_seq survey. Same regex, same bug, at tools/family_manifest.py:34.
  • counts: candidates 58717 / parsed 58617 / real skips 100
  • evidence: Over-approximating detector: INCLUDE_ASM\(\s*"[^"]*"\s*,\s*([A-Za-z_]\w*)\s*\) (ANY symbol), resolving non-func_ symbols through config/symbols.us.txt. Measured over src/ov_/.c: any-symbol INCLUDE_ASM = 58,717 ; tool regex = 58,617 ; gap = 100 ; unresolvable symbols = 0. All 100 gap items are the curated name predicted in the brief, e.g.: src/ov_SC01_084/ov_SC01_084_jr_80178D40.c: INCLUDE_ASM("asm/ov_SC01_084/nonmatchings/ov_SC01_084_jr_80178D40", listCdBuffer); src/ov_SC03_094/ov_SC03_094_jr_80178D40.c: INCLUDE_ASM("asm/ov_SC03_094/nonmatchings/ov_SC03_094_jr_80178D40", listCdBuffer); src/ov_SC06_008/... , src/ov_SC02_041/... (100 overlays total) config/symbols.us.txt:981 -> 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 → DELETED (A9d, R33)

Deleted together with census_conflict_callees (its only input generator). No live consumer (output .run/canonical_sigs.json read by nothing; no Makefile/workflow; last touched Phase-17). The canonical-sig purpose was retired in A3d (the fleet-majority oracle → reconcile_tu's per-TU oracle), and the asm-arity heuristic is 36% wrong vs byte-exact banked C — so per R33 the heuristic ceases to exist rather than being fixed. Original DOWNGRADED analysis retained below as the evidence trail.

  • 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/<src>/nonmatchings/<src>/func_<A>.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 <scratch> -> 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 c0379f073 (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 9c8bb5183 (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 c0379f073 (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 → ✅ FIXED (A9g, R33)

The ephemeral roster is GONE. jr_inventory's banked is now DERIVED: a real-C def/define fn is a banked jr iff family_remap.reloc_targets shows it references a committed .rodata carve offset (the image + the config, both durable). No .run/banked_func_*.json glob, no cand filter — so a rm -rf .run/fresh clone no longer blinds it, the CROSS-ADDRESS sibling func_8017FCB0 is now found (roster missed it), and NON-LEADER banked jr (carve in the object's own subseg, not a _jr_ leader) are found where a subseg-name model would miss them. The R32 assertion from :1320 is implemented: every committed carve must resolve to exactly ONE owner or jr_inventory aborts. Image read once + passed to reloc_targets(…, data=) (new backward-compatible param), ~6s→fast. Verified: the 3 audit orphans (ov_SC01_000/ov_SC02_000/ov_SC02_003) all resolve; full-fleet 1:1 ownership holds (no false abort).

  • 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_<addr>] 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("...", <sym>) 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 <sym> 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 -> f0dd9351e, 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 f0dd9351e (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 ba6cb87d3, 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 <fn> 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 <sym>, 0x<size> 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 <sym>, 0x<SIZE>. 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/<ov>/<ov>.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 1d94f9ec9 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 + 3branches + 25jtbl + 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+2njalr 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 2ncalls 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("<dir>", <sym>); 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/<ov>/<ov>*.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/<ov>/<ov>*.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_<ADDR>.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 <ov>_JTBL_INTERLEAVE var (so revert's committed-value restore path is exercised, not the drop path) and the <ov>_SPLAT_YAML := config/splat.<ov>.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 <sub>.o in its own overlay's --order list: 0 misses. Subseg names are <ov>_-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_<ADDR>) is <ov>-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-overlayglob.glob(f"{ovdir}/.c")`` — 1604/1604 — NO suffix allowlist. The plain *.c glob reads every overlay source file including the Phase-26 _jr_<ADDR>.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 silentif 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 <symbol> 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('<I')) is correct. CAVEAT: this clean bill is for LINE parsing only; the separate function-BOUNDARY defect (inline data after alabel/dlabel, 4 functions) is reported as a LOW finding.
  • match_one.py:77-95 — the inline per-instruction diff loop — 1589/1589 — VERIFIED EQUIVALENT to masked_diff.diff_object_s, no divergence. match_one re-implements the compare inline rather than calling diff_object_s; I checked the two agree on all edge cases (mask driven by MY object; i >= 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 */ <mnemonic> 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.

🔴 NEW FINDING (session 9, found while fixing A4) — a stale object can produce a FALSE PASS

Severity: HIGH (byte-integrity). Found by cutting the R22 corner and being punished within minutes.

Makefile tracks HEADER dependencies (-MMD -MP, Phase 15) so an edit to engine_core.h correctly triggers a recompile. But an object's assembly arrives through INCLUDE_ASM, which expands to a .include consumed by maspsx/as — after cpp. So the dependency

    build/src/<bin>/<tu>.o   <-   asm/<bin>/nonmatchings/<tu>/*.s

is never expressed. make compares the .o against the .c only. Re-extract (which rewrites every .s) and then build incrementally, and Make sees a .o newer than its .c and does not rebuild it.

Why this is worse than an inconvenience: it can FALSELY PASS. INCLUDE_ASM pastes the ORIGINAL assembly, so a stale object still contributes the original bytes — the image stays byte-identical and SHA1 goes green, while the split that was actually just changed is never exercised at all. A broken config/ change can therefore be "verified" by an incremental build.

Measured, live, during the A4 fix: after make clean-less re-extraction, build/src/ov_SC05_000/ ov_SC05_000_after.o (timestamp 02:19:59) was newer than its .c (01:43:55), was NOT rebuilt, and still carried 1 undefined reference to a symbol that no longer exists. 8 of 136 binaries failed to link. They failed loudly only by luck — an undefined symbol is a link error. Had the stale .s merely been a different valid split of the same bytes, all 136 would have gone green on stale objects and the regression would have shipped.

This is exactly the failure R22 and H3 already legislate against ("verify from a CLEAN rebuild"; "after any config/ change, make clean before re-extract") — the rules are right, and I broke them. But a rule that depends on a human remembering is not a gate. Make it structural.

Fix: extract must invalidate the objects that include what it just rewrote. In the extract target, rm -rf build/src/$(BINARY) (and build/asm/$(BINARY)), so a re-extract can never be followed by a build from stale objects. Cheap, total, and it removes the need to remember the rule.

Assertion (R32): after build, assert every .o linked into the image is NEWER than every .s it includes; fail loud on the first inversion. A build that consumed a stale object must never be allowed to report BYTE-IDENTICAL.


S44 NEWCODE AUDIT (2026-08-06) — every tool vs the 78 unclaimed payloads

Drew's directive: "analyze every single one of our tools and determine how/if it needs to be updated to properly account for our new code findings." Ground truth: 3 read-only exploration agents over the full inventory (117 tools/*.py, 13 tools/*.sh, tools/bfm_extract/ ×11, tools/ghidra_scripts/ ×11, tools/workflows/ ×6, tools/permuter/, diff_settings.py, Makefile, the config registries). Vendored submodules out of scope.

The class mostly DISSOLVES: the 3 big payloads are ordinary overlays (standard slot, existing machinery); only the small modules + the SC07 pair need a genuinely new binary class ("md_*", config/modules.mk). Classification: (a) parameterized per-binary · (b) derives the binary set from configs · (c) hardcodes overlay shape · (d) binary-agnostic/N-A.

Registration surfaces (the choke points)

surface class point verdict
Makefile b :55 BINARIES; prune :511; check-all :753 CODE: -include config/modules.mk, + $(MODULE_BINARIES); downstream of $(BINARIES) auto-OK
config/overlays.mk b generated registry NEW SIBLING config/modules.mk, same 18-var block, per-alias VRAM
tools/dup_report.py BINARIES b :26-180 (sentinel :167) registration line/binary (non-ov_ aliases already take the individual-ingest path :210)
tools/progress.py BINARIES b :23-305 (sentinel :304) registration line/binary
diff_settings.py BINARIES b :16-437 (sentinel :437) registration line/binary
tools/audit_binaries.py (R36) c onboarded() :41-43 globs splat.ov_*.yaml; startswith("ov_") :92,:111,:124,:131 CODE: derive from splat.*.yaml minus main (R33); keep engine_core-include check ov_-conditional
tools/corpus.py b :373 from dup_report; sig_is_independent :336 CODE at :336 (ov_/resident-only ⇒ module sigs untrusted); rest auto-OK
tools/difficulty.py b→derived cfg_for(alias) :22-34 auto-OK (P27 T6 migration)

Must-change (code)

tool defect fix
family_remap.py VRAM = 0x80128158 module const :30, used in ALL offset math :98,:160 (img_path is already yaml-derived) vram_base_of(alias) from config/splat.<a>.yaml — the pattern jtbl_carve.overlay_vram_base() :65-71 already implements
Makefile sig targets sig-overlays hardcodes OVERLAY_VRAM :=0x80128158 :292; sig-resident separate one generalized target over $(filter-out main,$(BINARIES)) with $($(a)_VRAM_BASE) (+ per-alias TEXT_LO); keep old names as aliases
family_hseq.py src/ov_* :43 + sig.ov_* :45 globs; self-declared overlays-only :189,:219 include resident+modules (glob sig.*.jsonl minus main, or read registries)
progress.py --weighted sig.ov_* glob :647 + explicit resident :648 derive from BINARIES
audit_frontier.py :57-59 same glob shape same fix
backlog.py alias regex :137 `(ov_… resident
prefetch_fleet.py :67 ("main","resident") add modules
dedup_propagate.py reads only overlays.mk :44 also read modules.mk

Retire (R33)

disc_code_sweep.py — superseded by disc_audit.py (whole-disc partition, both layers, no window, claims); zero build refs (Makefile mentions it only in a comment); doc refs to update: docs/SETUP.md:667, docs/disc-completeness.md · reconcile_decls.py — self-declared RETIRED · rollout_801457a4_o0.py, rollout_whale_o0.py, rollout_o0_cluster.py — one-shot, slot-locked historical rollouts · ghidra_scripts/ImportOverlay.java + VerifyOverlay.java — 1-overlay-era hardcoded tables (ghidra_import_raw.sh is the live path).

new_overlay.sh → tools/new_binary.sh

Overlay-specific: alias pattern :29, payload path ${ENTRY}.dec :30, VRAM= :31, slot literals re-hardcoded at :39,:44, the overlay splat template. Generic and reusable verbatim: check.sha + symbols creation, the 18-var mk block, the sentinel-anchored 3-dict registrar :104-133 (ast-checked), extract+build byte-check. ⇒ parameterize {ALIAS, PAYLOAD, VRAM, TEXT_LO, TEMPLATE, REGISTRY}; new_overlay.sh becomes a wrapper with the old defaults.

sig_image.py — no code change; a USAGE law

--bootstrap linear-partitions from lo = vram_base (:232, bootstrap_seeds :81-98) ⇒ assumes code at file offset 0. 75/78 payloads start with the module-id word (+ sometimes a ptr table) ⇒ 0 seeds. Law: pass --text-lo past the header (prologue offsets are in the disc-ledger roster). --seeds accepts a sig jsonl or 0xADDR lines (:47-59).

Auto-OK once registered — (a) parameterized / (b) derived

gate_stage · harvest_verify · match_one · rtu_match · masked_diff/masked_scorer · family_sweep (cross-address --to-addr EXISTS: :332-343,:537) · family_manifest(glob fix rides family_hseq) · dedup_extend · dedup_integrate · jtbl_carve (the model implementation) · jtbl_family_bank · jr_isolate/jr_isolate_all · o0_subsplit · overlay_src_split · split_src_region · blast_radius (derives from splat.*.yaml — best-in-class) · worklist · exemplar_miner · diff_regions · lift_types · build_engine_types · uniquify_type · canon_sig_reconcile · recover_giant · recover_integration · fix_arity_callers · fix_header_decl · cast_call_sites · sig_unify · reconcile_tu · canon_draft_decls · canon_resident_calls · inject_capped_externs · scope_tu_externs · scope_data_externs · normalize_self_decls · conform_decls · blocker_probe · demacroize · autopsy · residual_class · bank_exemplar · t7_bank · sweep_parallel · bulk_harvest (alias side via lora_grind.binaries() = check-sha glob) · lora_grind (binaries() auto-OK; reach-glob rides the hseq fix) · gen_harvest_targets(same) · build_fuel_manifest(same) · wave_targets · build_wave_args · idiom_loop · audit_digest (via progress.BINARIES) · lint_symbol_refs · cookbook_index · symcheck · burndown · p16_permute/permuter_ils/permuter_weights/ p16_improve/p16_known_answer · grinder · auto_driver · workflows (worker_wave.js etc. — aliases are prompt args) · glm_reconcile.

N-A (binary-agnostic)

Extract stack (bfm_extract/* — already extracted the payloads) · PsyQ linking (psyq_*, gen_lib_subsegs, ld_interleave, make_*_used, make_libgs.sh, jtbl_rodata_pads) · LLM tier (serve_local, api_draft, train_lora, eval_lora, format_finetune, export_pairs, idiom_hunt, glm_parallel.sh, orchestrator) · permuter internals (run_masked, compile*.sh) · Ghidra plumbing (ghidra_import.sh, ghidra_import_raw.sh — the live module importer, ghidra_mcp_*.sh, the .java scripts except the two retired above) · automation shell (auto_status/stop/supervisor.sh, treelock.sh) · decompile.py · match_protos.py · ram_probe.py · audit_text_sources.py · sweep_citations.py · ab_match.js/ab_score.py · disc_audit.py (the new oracle itself; its claimed-by derives from config/check.*.sha, so newly onboarded binaries flip to claimed with ZERO wiring).