Files
BFM-decomp/docs/decision-log.md
T
Drew T f4502f11bb fix(phase-26a): A3c — the recovery passes were reconciling 95% of drafts against the WRONG TU
FIRST CONSUMER MIGRATION onto the cdecl oracle — and the compiler taught me two things I had
wrong, one of which reopens a wall that has been closed since Phase 15.

1. cdecl.compatible() — "will cc1 accept these two declarations of one name?"
   The predicate four tools each half-implement and get wrong: 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. Right about codegen, wrong about the front end,
   which never reaches codegen.

2. THE ADJUDICATOR MUST BE THE COMPILER THAT COMPILES YOUR CODE (cookbook §51g LAW 9).
   I wrote the rules from the C standard, then let a compiler judge. It contradicted me -- and then
   the RIGHT compiler contradicted the first one. Three different answers:

       declarations in one TU        | standard | modern gcc | gcc-2.7.2 cc1
       typedef int X;  twice         | error    | ACCEPTS    | ERROR
       extern u16 X; + volatile u16 X| error    | error      | ACCEPTS
       void X(s16);  then  void X(); | error    | error      | ACCEPTS
       void X();     then  void X(s16)| error   | error      | ERROR

   --compat now adjudicates with tools/bin/gcc-2.7.2-psx/cc1, the front end that actually
   arbitrates the build: 1,485/1,485 live corpus pairs agree, 0 disagree, 0 skipped.

3. THE PRIZE: the Phase-15 narrow-param wall rests on a false premise.
   The no-prototype rule is ORDER-DEPENDENT. `void X(s16); void X();` COMPILES; only the reverse
   fails. Phase 15 closed "the 159 arity/narrow-param conflicts" as "no clean deterministic fix --
   it is simply C's default-promotion rule". cc1 does not enforce that rule in the direction the
   wall assumed. Four three-line probes, 90 seconds, zero tokens. -> A10 RE-TEST TARGET.
   Probe the compiler for FACTS; read its source only for LEVERS; byte-validate both. (We read
   gcc-papermario for five phases believing it was 2.7.2. It was 2.8.1.)

4. THE MIGRATION: cast_call_sites canonicalized 95.1% of drafts against a TU that would never
   compile them. `--src-file` is an OPTIONAL HAND-PASSED flag defaulting to src/<ov>/<ov>.c, and no
   caller knows about the Phase-26 _jr_<ADDR> carves: ov_SC01_077 has 263 open stubs across 12 TUs
   and only 13 are in the main .c -- while harvest_verify (A3) correctly splices into the real one.
   Now DERIVED from corpus.stubs() (the INCLUDE_ASM line is self-describing), with the canonical map
   derived from cdecl.tu_scope() (cpp -- so macro-injected DEFINE_func_* decls are finally visible).
   Callee-conflict repair reach: 8 -> 58 of 196 drafts (7x).

5. AND THE NULL RESULT, REPORTED AS SUCH (P9/R14). Those 58 banked ZERO functions. The historical
   draft tail fails on CODEGEN, not plumbing -- func_801387B8, which the audit blames on a single
   unparsed `[4]`, is really 67/100 instructions off with a $s0/$s1 swap (that claim does not
   reproduce on today's tree). The real gain is narrower and still worth having: 52 drafts moved
   from "won't compile" to "compiles, N instructions off" -- from an INVISIBLE failure that reads as
   a compiler wall into a SCORED near-miss the permuter and the §47/§48 dials can act on. That is
   the audit's thesis, not a bank. THREE times in one session a confirmed mechanism produced a null
   consequence.

Also: my own new audit printed "ALL ORACLES GREEN" while silently skipping 100% of its corpus (a
missing -Isrc). The exact bug class, in the tool written to hunt it. An unadjudicable check is not
a passed check.

  R22 clean-fleet: make clean + extract-all + check-all -> 136 passed, 0 failed of 136
  src/ untouched (0 changes)   make audit-cdecl: green   --compat: 1485/1485
  NEXT: sig_unify + reconcile_decls carry the SAME wrong-TU bug (same --src-file flag).
2026-07-14 12:26:01 -06:00

83 KiB
Raw Blame History

Decision & Pivot Log — the "why" behind BFM's strategic turns

Purpose. An append-only record of the project's STRATEGIC pivots, dead-ends, and reversals — the judgment behind major direction changes, captured WHILE FRESH (R31). This is the perishable layer the other records don't hold:

Record Holds
phase-ends/PhaseEnd_*.md per-phase build history + terse Deviations tables (WHAT changed, briefly)
docs/matching-cookbook.md TECHNICAL idioms (asm↔C compiler quirks, tool recipes)
this file STRATEGIC why: what we believed → what we tried that failed → why we turned → the hindsight "better path"

Why it exists. It is the substrate for two future deliverables (see the Phase-25 discussion, 2026-07-08):

  1. the project retrospective — "with hindsight, the best way to have done this"; and
  2. the public "how to AI-decomp a brand-new project" wiki at the public flip.

The quantitative curve (fleet % over time) is safe in git + the PhaseEnds forever; the reasoning — "we tried X, it was a dead end because Y, so we turned to Z" — is what evaporates between sessions. So it is logged here as it happens.

Discipline (R31 — confirmed by Drew 2026-07-08, Phase 25). Forward-only. Do NOT backfill historical pivots from compressed summaries — that reproduces exactly the low-quality reconstruction R30 exists to prevent (the PhaseEnds/cookbook already hold what was captured fresh at the time). Log each NEW strategic pivot during the session that produced it. Route TECHNICAL idioms to the cookbook; this file is for direction/judgment.

Entry format:

## [date] · Phase N — <pivot title>
- Context / belief: what we were doing and assumed.
- Dead-end (if any): what we tried that didn't work + the byte/measurement evidence.
- Pivot: what we changed to.
- Why: the grounded reason (bytes, measurement, constraint).
- Hindsight / for the wiki: the transferable lesson — what a from-scratch project should do.

2026-07-08 · Phase 25 — a fresh session nearly closed an OPEN phase (handoff-misread)

  • Context / belief: resuming Phase 25, the CURRENT_PHASE.md handoff header read "finish T7 → Close; do NOT start T4 yet." I concluded the remaining tasks (T4/T5/T6 + the exemplar-cracking that is the phase's actual goal) were being deferred to Phase 26, and drove toward writing the PhaseEnd.
  • Dead-end: I reframed the plan's core work as a "Phase-26 backlog" and presented a milestone-close for confirmation.
  • Pivot: Drew corrected — T7 had been pulled ahead opportunistically (only its free/cheap MECHANICAL wins); "not yet" meant finish those first, not defer to next phase. The 127-draftable-family curriculum (T4→T5→T6→T7-cracking, the 6.7 MB byte-weight prize) remains this phase's substance. No PhaseEnd.
  • Why: I anchored on the literal "→ Close" in a handoff header — a phrase written for one moment ("don't start T4 in THIS session") that I read as a permanent scope decision. The approved plan of record (plan-mode-…-galaxy.md) plainly lists T4–T7 as this-phase work with an open-ended milestone; reconciling the handoff against it first would have prevented the error.
  • Hindsight / for the wiki: the #1 failure mode of a long multi-session AI project is a fresh session misreading a compressed handoff. What saved it: (a) an approved plan-of-record that outlives any single handoff, and (b) a human who caught it in one message. What to bake in: handoff headers must distinguish "not this session" from "not this phase" in unambiguous words; and a resuming session must reconcile the handoff against the plan-of-record before ever concluding a phase is done — never close on a handoff phrase alone. (This is why the plan-of-record and the two-gate model earn their keep.)

2026-07-08 · Phase 25 — the "mechanical" family sweep has a hard ceiling at TU-local type collisions

  • Context / belief (T7.2): the h_norm structural-family remap is mechanical — crack one exemplar, lift the local types it references into the shared header, and every sibling overlay compiles + banks.
  • Dead-end: lifting ALL overlay-split-file types into the fleet-wide engine_types.h broke the build — typedef Buf has a DIFFERENT layout in _a.c vs _after.c (they never clashed as separate .o TUs), and _a.c locally redefines PsyQ SDK names (MATRIX/VECTOR) that a fleet-wide lift would shadow.
  • Pivot: lift only the collision-free subset (base + _after minus Buf); defer the conflicting families to per-type reconciliation (Phase 26). Banked 1,729 of the ~3,857 targeted — the rest is genuine work, not mechanical.
  • Why: the overlay split files are independent translation units with independent local type namespaces; "share everything" is unsound across that boundary. (Technical detail → cookbook §40a.)
  • Hindsight / for the wiki: know the mechanical ceiling before launching a mass pass. A cheap detect-collisions-first probe would have scoped the safe subset up front instead of discovering it via a failed build. General lesson: an automated bulk transform needs an explicit soundness boundary, and the byte-gate (not optimism) is what stops a partial success from masquerading as a full one.

2026-07-08 · Phase 25 — the local-7B tier is capacity-bound and off the endgame critical path (T4)

  • Context / belief: the fine-tuned local drafter (bfm-match-7b-v3) was a core cheap tier; retraining v4 on the much larger post-giant-campaign corpus (2,891→3,574 pairs, +994 medium + 597 large functions v3 never saw) should extend its band upward and make it a stronger drafter for the T5 wave.
  • Dead-end: v4 did not beat v3 — it was marginally WORSE. Gate-true head-to-head on identical held-out functions: easy 6-14 ins both 5/5; medium 18-40 ins v3's near-misses closer (one at near-1, permuter fuel) with 1 compile-fail vs v4's 4 — v3 closer on 9/12; hard 45-85 ins both 0/10. Crucially v4 scored 0/5 even on the 76-83 ins functions it TRAINED on (verified ~1.4-1.7k tok, well inside maxlen 2048 → NOT truncation → genuine capacity). (Note: a real corpus-prep flaw exists — functions >85 ins WERE truncated at maxlen 2048 → training on cut-off completions, likely the source of v4's slight medium regression — but it doesn't touch the decisive band.)
  • Pivot: discard v4, keep v3 (the frozen ceiling), and stop investing in the local-7B tier. Not retired (still a $0 mop-up for the ≤~15-ins setter/leaf tail), just no longer load-bearing and no more retrains.
  • Why: the byte-gate A/B settled it directly (G3/P9). "Corpus quality > size" landed empirically: v2→v3 gained from better data (the extern-capture fix); v3→v4 was just more/harder data and it didn't lift a capacity ceiling. Byte-matching's hard part is compiler-codegen REASONING (scales UP with model size), not language breadth (which a smaller model could shed) — so neither "more data" nor "a smaller RE-specialist" is the lever; the reasoning has to come from a large pretrained base or a frontier model, and the RE-smartness that IS small+deterministic already exists as m2c (rules, not weights).
  • Hindsight / for the wiki: the endgame engine is frontier-crack → deterministic-propagate → byte-gate, with the permuter softening near-misses — the local small model is a convenience on the small tail, not a load-bearing part. For a matching decomp you already own the ground-truth compiler + a perfect verifier, so the ML task is candidate-PROPOSAL + search (proposal quality scales with reasoning/size; the check is free). A bespoke small "RE model" founders on data scarcity (the asm↔C-under-a-specific-compiler corpus only exists, tiny, in decomp git histories). The honest tiering: m2c for structure, a frontier reasoner for the byte-exact precision on the hard/byte-weighty band, the permuter for regalloc/schedule search, a frozen small LoRA only for the cheap ≤15-ins tail. Don't spend GPU-hours chasing band-extension on a 7B; rent a bigger GPU or use the frontier tier when the hard band is the target.

2026-07-09 · Phase 25 — the GIANT def-side wall is mechanically crackable → build the lever, don't just measure (T5b batch-2)

  • Context / belief: T5b batch-2 (the 29 giants) was scoped as a pure measure wave — draft, match_one, map the frontier; the plan filed the def-side loose-typing wall as a T7 (post-curriculum) problem, expecting ~0 giant banks. Belief going in: giants would mostly near-miss and feed Fable5/permuter; any that isolation-MATCHed would bank via the gate's existing sig_unify/cast transforms.
  • What was tried that failed: all 16 R14-verified isolation-MATCH giants banked 0/16 through gate_stage AND through raw harvest_verify. Root cause (dug out by placing one and reading the cc1 error, not trusting the gate's summary): conflicting types for func_X — the drafters wrote Ghidra-typed sigs (void f(u32*, s16*)) that clash with the TU's canonical sig, which lives inside a DEFINE_func_* macro in engine_core.h where sig_unify (a file-scope-extern rewriter) can't see it. First reconcile attempt ALSO failed twice: an s32/s32-args form conflicted with the engine_core.h void/void* canonical; then intermediate cast-locals (u32 *a0 = (u32*)arg0) compiled but produced the WRONG bytes (70ff4748) — a fresh pseudo shifted regalloc.
  • Pivot: stop treating the def-side wall as a future-T7 abstraction and build the lever now (tools/canon_sig_reconcile.py): strip ambient-dup typedefs/externs, rewrite the def to the engine_core.h canonical, and cast each changed param AT ITS USES, never via a local. That banked func_8013B274 byte-identical, then 5/16 giants total; 3 swept ×134. Batch-2 turned from "measure + backlog" into "prove + partly-automate the phase's #1 lever," pulling a chunk of T7 forward on real data.
  • Why (byte/measurement-grounded): the cast-local vs at-use difference is a measured byte fact (70ff4748 wrong vs d19c9580 right), not a style call — an intermediate local is a new pseudo gcc-2.7.2 may color differently; an at-use cast is free. The whole-binary byte-gate stayed the sole arbiter throughout (G3/P9): every one of the 5 banks is byte-identical, and R22 clean-fleet is the backstop — it caught my own buggy R22 harness (an unexpanded $(OVERLAY_BINARIES) that only extracted 2 of 136 binaries) before any false "136/136" could be reported.
  • Hindsight / for the wiki: when a whole class of candidates fails the gate identically, read the raw compiler error on ONE placed candidate before concluding "hard / defer" — the summary ("0 banked") hid a mechanical declaration conflict behind what looked like an intractable matching wall. The giant tier was never a matching problem; it was a plumbing problem (the body was already right in isolation). General lesson for frontier-crack→propagate→byte-gate: distinguish codegen residuals (permuter/Fable5) from TU-integration residuals (a deterministic reconcile) early — they look identical at the gate ("0 banked") but have completely different levers, and the integration ones are cheap ×134 wins hiding as "hard giants." Residual: non-identical ambient types and macro-local data symbols are the genuinely-hard remainder (real reconcile_decls/rename work), and a reconciled body doesn't family_sweep cleanly to siblings (per-sibling re-reconcile needed) — both logged for T7.

2026-07-09 · Phase 25 — T6: the def-side wall was ~71% tool-shaped; the endgame's frontier shrank from 95 to 33 (Fable5 curriculum session)

  • Context + belief going in (the T5 handoff): the 95 still-stub family exemplars decomposed as "~19 clean-canonical mechanical via canon_sig_reconcile; ~32 harder implicit-int fallback; 31 genuine near-misses; 11 hard walls (non-identical types / macro-local data); the 3 _o0 giants need a special -O0 reconcile; the frontier 'match' statuses carry un-verified agent claims (spot-check 3/5); reconciled bodies don't sweep (func_8016DC20 = 133 sibling failures)." T6's job was to author the crack curriculum from that map, with the expectation that most of the 95 needed per-fn Fable5/permuter work.
  • What was tried that failed / surprised: (1) R14 re-verification of ALL 95 (not 5) flipped the caution — 62/95 genuinely isolation-MATCH; the spot-check pessimism didn't generalize. (2) Probing all 62 through the REAL TU (splice + full pipeline + masked in-TU byte-compare) showed v1 canon_sig_reconcile itself was the wall for most: six probe iterations (v1→v3.1) flipped 10→20→37→44 fns to BANKABLE as five tool defects fell (scalar-typedef strip; preprocessed-TU canonical; block-scope- move-not-strip; collision RENAME; decl-line cast protection). (3) Two of my own v2/v2.1 policies (ambient-type rewrite of data externs; canonical-text re-emission + blanket casts) BYTE-DRIFTED or parse-broke dozens — caught only because every iteration re-probed all 62 (the probe program, not the first diagnosis, was the method). (4) A byte-perfect gate run reported MISMATCH because I hand-typed --good-sha from memory — the check file is the only source of a hash.
  • The pivot: T7 is now mostly DETERMINISTIC EXECUTION, not discovery: tiers M1 (44 reconcile-banks, 4,254 ins, 13 giants) → M2 (sweep ×134 via remap + PER-SIBLING re-reconcile — proven 6/6, dissolving §41's "sweep fragility") → M3 (6 fns: no-proto rewrite of engine_core.h macro externs — arity conflicts with a visible typed prototype are the one thing no draft transform can fix) → M4 (8 fns: §33 TU stale-decl retypes) ≈ +2.2% fleet for ~0 agent tokens; the true Fable5/permuter frontier is 33 fns (31 verified nears + 4 perturbs, minus overlaps) + 2 trivial drafts. The "-O0-specific reconcile" need was refuted outright. Deliverables: docs/phase25-t6-curriculum.md, .run/t6_worklist.json, cookbook §41a, canon_sig_reconcile v3.1.
  • Hindsight / better path: a "wall" verdict is only as good as the TOOL REVISION it was measured against — re-derive wall taxonomies after every tool change (three iterations moved 34 fns from "wall" to "mechanical"). And validate the gate-proxy against the real gate before trusting either direction (the probe caught real walls the drafts hid; the gate caught my SHA typo). The general law: when a residual class's members share an error SHAPE (conflicting types, undeclared, parse-at-decl), suspect the pipeline before the compiler.

2026-07-10 · Phase 25 — T7 executed the curriculum; the "mechanical tier" was ⅓ probe-over-counted (Opus-Max)

  • Context + belief going in: the T6 Fable5 curriculum projected 58 fns / ~2.5 MB as MECHANICAL (M1 44 reconcile-banks, M3 6 no-proto, M4 8 reconcile_decls-retypes), sized from an in-TU OBJECT probe (compile the fn in the real TU, mask jal/%hi/%lo, byte-compare). Drew: "run T7."
  • What was tried / surprised: executing against the WHOLE-BINARY gate (harvest_verify) revealed the object probe systematically OVER-counts, because it is blind to three things it cannot see: (1) rodata — 4 "M1" jump-table fns have byte-perfect .text but a switch table in rodata diverges (this also REFUTED the T6 "Q3 -O0 REFUTED" claim — the _o0 giants are jump-table fns); (2) link — 3 "M1" fns are the only asm referencer of a scratch data symbol, so C-ifying them drops splat's auto-symbol → ld undefined; (3) in-TU codegen perturbation — all 8 "M4" fns are byte-correct in ISOLATION but drift 8–69 in the real TU (scheduling order, volatile-loss), and reconcile_decls banks 0/8 (4 have no data-decl conflict at all). Plus 4 M3 residue (arity/loose-typing). Net: 19 of the 58 "mechanical" were per-fn F-band work.
  • The pivot: bank the TRULY-mechanical core and re-tier the rest honestly. M1 37 + M3-clean 2 = 39 exemplars, reconciled (canon_sig_reconcile v3.2 — a type-name-uniquify fix unblocked the struct-collision giants) and swept ×134 via family_sweep --reconcile (the Q5 per-sibling re-reconcile law: plain remap banks 0, per-sibling reconcile banks 94%). ~4,694 fleet fns, fleet 72.29 → 73.66% (+1.37%), R22 136/136, ~0 agent tokens. The 19 over-counts → F-band (permuter/§31/Fable5) or specialist workflows (jump-table-in-rodata, manual undefined-syms).
  • Hindsight / better path: an in-TU OBJECT probe is a necessary FILTER but must never SIZE a "mechanical" tier — gate a full sample on the whole binary first, and budget ~⅓ of any object-probe "drift/fail" bucket as genuine per-fn work. The mechanical ×134 sweep is the real economic engine (4,655 members from 39 cracks); the curriculum's value was concentrating the crack effort onto the 39 exemplars, not the tier-size projection. Reinforces R14 (verify vs bytes) at the tier-classification level and cookbook §41b/§41b-addendum.

2026-07-10c — Propagation-recovery (task b) is NOT a simple --edit-remap gap; it's a family_remap LIMITATION on lever-heavy F-band cracks

Context+belief: After waves 3/4 banked exemplars but dropped ~1,200 siblings, I projected task (b) as a cheap family_sweep --edit-remap enhancement (carry the exemplar's //@EDIT file-scope edits per sibling) — a ~0-token bulk win.

What failed / the diagnosis: Probing the 7 dropper families showed the drops are HETEROGENEOUS and dominated by family_remap symbol-pairing FAILURE ("133 remap-fail"), NOT the decl/byte layer an --edit-remap would fix. family_remap was built for MECHANICAL (reloc-only, T3) families; the F-band cracks carry heavy levers (register __asm__ pins, density dead-reads, phantom-frame frame_pad, pointer casts, //@EDIT flips), whose disassembly doesn't positionally pair to the sibling image cleanly → remap aborts. func_8014FE60 (engine_core void→s32) also remap-fails even after the correct global flip. The families that DID sweep (func_80166690/8017B238/80131B14/ 8016CF04/8014FBC0) have remap-clean bodies; the droppers don't.

The pivot: recovering the ~1,200 dropped siblings needs EITHER (a) a family_remap upgrade that pairs lever-heavy bodies (structure-aware, not positional-reloc-only), OR (b) a per-sibling re-crack (m2c+reconcile+rtu_match fan-out ×133/family — expensive). Both are a focused follow-up, not a quick enhancement. Backlogged.

Hindsight better-path: the ×134 economics assumed family_remap propagates any crack; it only propagates remap-clean ones. Future crack-then-sweep waves should CHECK remap-ability of the exemplar body BEFORE counting the ×134 (a cheap family_remap --dry per exemplar), so the frontier map's leverage estimate reflects propagate-able families, not all same-address families.

2026-07-10c (CORRECTION, same day) — the "family_remap limitation" was a MISDIAGNOSIS; the real bug was canon_sig_reconcile's def-finder (R14)

Correcting the entry above. I concluded the propagation drops were a family_remap limitation because family_sweep reported "133 remap-fail". That label was misleading. Running family_remap directly on all 7 droppers SUCCEEDED (it paired 4–6 symbols each). The None that reconcile_remap returns — which family_sweep counts as "remap-fail" — actually came from canon_sig_reconcile.reconcile raising "no definition of func_X found in draft": its def-finder regex required a leading \n (r'\n(<type> fn(...)){'), but stripping the //@EDIT lines left the fn definition on LINE 1 of the raw draft (no leading newline) → not found. The swept-clean families happened to have a leading // @class comment, so their def had a \n before it.

FIX (1 char, low-risk): def-finder regex \n → (?:^|\n) (also match a def at the draft start; only ADDS matches). Result: func_8014FE60 fully recovered — 133/133 siblings banked (fix + its engine_core.h void→s32 global flip).

Residual (the GENUINE --edit-remap work): 4 families (func_8016DF5C/80136334/8013D9B0/80156044) now RECONCILE but BYTE-DRIFT per sibling — their crack levers (s32↔void return flip, array-decay pointer //@EDIT, no-proto //@EDIT) aren't carried/re-derived per sibling. Recovery = carry the exemplar's //@EDIT per sibling (symbol-remapped) + apply the return-type flip to the shared engine_core.h decl once. Still a focused follow-up, but SMALL and well-understood now — NOT a family_remap rewrite.

LESSON (R14): a tool's failure LABEL can misattribute the failing STAGE. "remap-fail" was actually a reconcile-def-finder throw. Trace the real exception (reconcile_remap swallows it) before concluding a limitation.

2026-07-11 · Phase 25 — task B: --edit-remap BUILT, but 4/6 byte-drift families are cc1-crash-walled (~266, not ~800)

Context + belief (from the 2026-07-10c handoff): the 6 byte-drift //@EDIT families were framed as "SMALL and well-understood — recover ~800 fns by carrying the exemplar's //@EDIT per sibling + a once-global engine_core.h flip." Drew locked B first on that basis (scoped, mechanical). I built family_sweep --edit-remap MANIFEST to do exactly that.

What the byte-gate revealed (probe-before-invest, R14): the families are NOT one bucket. Only the 2 array-decay pointer-flip families (extern s32 D_x[];→extern s16 *D_x;) recover — func_80136824 + func_80136334 banked 266/266 siblings byte-identical (0 failed), full ×134. The other 4 are register-pin-heavy (func_80133AB0's exotic register int zr __asm__("$0"); func_8016DF5C/func_8013D9B0's GTE 20-pin bodies; func_80156044's inline-asm trampoline) and cc1-2.7.2 SIGABRTs (make Error 134) compiling the SIBLING TU — the identical body compiles fine in ov077. Universal (func_80133AB0 crashed 3/3 siblings tested). The hand pins are ov077-TU-context- specific: cc1's fixed-table 1996 register allocator aborts on the pin pattern in a different overlay's surrounding function set. func_80156044's engine_core.h int→void flip IS byte-neutral (verified) — the wall is its body, not the edit.

The pivot: ship the 2 tractable families (266 ×134, R22 136/136 green, fleet 74.40→74.48%), backlog the 4 crashers as exemplar-only (×1) / per-sibling permuter-Fable5 fuel, and move to A (the 7 giants — all remap-clean 133/133, ~938 fns high-byte-weight, the real ROI). The --edit-remap tool is reusable for future array-decay-class cracks; its yield must be sized by that subset, never by "family has an //@EDIT."

Better path (hindsight): the "~800" estimate counted sibs × families without asking "does the CRACK compile in a sibling TU?". A hand crack that banked in ov077 by exotic register pins does not generalize — a 30-second single-sibling make build probe per family would have sized B honestly up front. LESSON (R14): an exemplar match proves the crack in ITS TU only; the ×134 claim needs a sibling-TU compile probe, because pins are TU-context-specific and cc1 crashes (not just drifts) on the ones that don't transfer. Corollary: rtu_match/match_one are blind here — their neutralized/ isolation compiles crash too (harness artifact); only the real make build is the arbiter.

2026-07-11 · Phase 26 — the "reach-1 tail" is largely a reloc-tracker blind spot, not unique code (Task 1)

Context + belief (from the Phase-25 close): the h_seq reframe had already shown the "36k unique tail" collapses ~90% into per-location families. The open question entering Phase 26 was HOW the families differ — the megaplan framed immediate-substitution as the central new problem (families "differ in immediates, so are NOT free dedup").

What the design pressure-test found (byte-verified before any scaling — R14): the dominant difference is NOT immediates — it is a tracker blind spot. norm_stream/reloc_targets dropped the lui-hi on every R-type write, but gcc-2.7.2's indexed-global idiom lui;addu $idx;lw %lo($at) preserves it. So D[i]-indexing functions were mis-normalized per overlay → they inflated the "h_norm reach-1 tail," and family_remap silently dropped their indexed D_ symbols → those families couldn't bank even though they are pure per-location templates. On the substantial tail the classification is PURE-same-addr 62 fams / 1.55M ins · PURE-cross-addr 103 / 0.10M · genuine IMM only 8 / 0.10M — i.e. ~95% of the byte-weight is reloc-only, fixable by a ≤15-LOC tracker change, and the immediate engine shrinks to an escalation tier for ~8 families. A second latent bug surfaced alongside: remap's sequential substitution corrupts chained/permuted maps (harmless on h_norm, breaks the imm engine).

The pivot: front-load the tracker fix (Task 1) as the load-bearing change, demote the immediate engine to a diff-driven 3-tier escalation (Task 3), and add a free validation corpus — 63 families / 0.31M ins already have a MATCHED exemplar and only failed earlier sweeps from this bug → they bank with zero cracking the moment the fix lands (Task 5 V2), simultaneously measuring the real template success rate before any Fable5 spend.

Better path (hindsight): the tracker's own design note already said "conservative: can miss a match, never forge one" — but a missed reloc in a REMAP tool isn't harmless the way a missed h_norm match is; it silently produces a wrong-but-compiling sibling body that only the byte-gate catches. When a normalization/remap tool is REUSED for code generation (not just clustering), its conservative-miss becomes a correctness bug. LESSON (R14): before treating a "unique/unmatchable" population as intrinsic, re-run the grouping and the remap under a corrected fingerprint — here the "reach-1 tail" and the "unremappable family" were the SAME artifact of one dropped register-tracking case.

2026-07-11 · Phase 26 — Task 5 GO/NO-GO: the h_seq engine is 100% correct on clean families; the substantial matched band is type-dominated (a reconcile follow-on, not a machinery gap)

Context + belief: Task 5 was the validate-before-scaling gate — run the whole-binary byte-gate on the matched-exemplar families and MEASURE the real template success rate before any Fable5 spend. Expectation: a high mechanical bank rate on the "free-win" tracker-miss corpus.

What the byte-gate revealed (R14): the substantial matched-exemplar band banked 532/1507 non-pinned members (~35%) — but that aggregate is misleading, and the per-family breakdown is the real signal:

  • 3 families banked 100% ×133 = 399 byte-perfect members — exactly the 3 tracker-miss PURE families (0x8015d5e8/0x8015f118/0x801407f4) the design pass named. The addu-hi tracker fix + the extern-carry fix (below) make these template flawlessly. The machinery is 100% correct on clean families.
  • 9 of 13 clean families ZERO-banked — all reference overlay-local custom struct types (Work8016, Work8017, SV4_8017B368, Prim, E4) → the templated body is undeclared-type in the sibling TU = the §41 def-side / type-lift RECONCILE class, which the existing family_sweep --reconcile / build_engine_types path already handles. NOT a machinery failure — a known follow-on (Task 8).
  • 16 of 29 substantial matched families were pinned-exemplar (×1-only hard-reg cracks) → skipped by the new static pin guard → Task-7 pin-free re-crack.

The load-bearing tooling fix (Task-5's "measure then fix", R16): the first gate run banked only ~34% because extract_unit grabs only immediately-preceding externs — a per-location body that indexes a global ((*D_x[i])()) references symbols declared once at file scope elsewhere in the exemplar TU; templated into a sibling TU that never declared them, they are undeclared at the gate. Added remap_hseq.gather_externs (carry the file-scope externs for every body-referenced symbol, remapped). func_8015F118 went from gate-fail to BYTE-IDENTICAL; the 3 clean families then banked 133/133 each.

The pivot / verdict: GO. The zero-crack h_seq machinery (tracker + imm + cross-address + extern-carry) is byte-proven correct. Scale it (Task 8: the mid/tiny bands + the reconcile pass for the type families). The Fable5 window (Task 6/7) goes to the big PURE cores that have NO matched exemplar (890/562/536… — they need a crack before they can template).

Better path (hindsight): the raw aggregate rate (35%) nearly read as "the engine is weak"; the per-FAMILY breakdown showed it is "the engine is perfect on the families it targets; the rest are a different, already- solved problem." LESSON: when measuring a mechanical harvest, stratify by family/class before judging the rate — an aggregate mixes 100%-clean, 0%-type-blocked, and skipped-pinned populations that demand different follow-ons.

2026-07-11 · Phase 26 — Task-8 pipeline-validation slice (pre-Fable5-window de-risk): reconcile→bank works; templating reconcile-class cracks needs per-sibling re-reconcile in --hseq

Context + decision (Drew): before spending the closing Fable5 window (Task 7), validate that the reconcile→gate→template pipeline actually banks an isolation-crack end-to-end — else the window's output (same isolation-crack format) could pile up un-bankable. Ran optimal-order step 1 only ("bank the wins, pause before Task 8").

What the byte-gate revealed (R14), in two halves:

  1. Reconcile→bank WORKS. The 23 triage closeness-0 cracks gate 0/23 raw (they carry standalone struct Obj/scalar typedefs + Ghidra sigs → §41 def-side wall). Run through canon_sig_reconcile v3.2 (strip ambient dups, canonicalize the sig, cast callees at use) they bank 4/15 into ov077 (func_801506A4/func_8016A73C/func_80167540/func_80155800, byte-identical). The 11 residual fails are a data-extern-typing gap (e.g. conflicting types for D_801891B8, a fn-ptr array the seed types differently than the TU) the reconcile's pt-9 data-extern handling doesn't fully cover for these seeds.
  2. Templating a RECONCILED body ×133 FAILS (0/4). The reconciled ov077 body is TU-SPECIFIC — its canonical-sig casts + Name_<addr> collision-renames fit ov077, not the sibling TUs (each has its own ambient types/sigs). Plain remap_hseq copies the ov077-reconciled body → re-hits the def-side wall in every sibling. This is the decision-log 2026-07-11 lesson again: an exemplar match proves the crack in ITS TU only; ×134 needs per-sibling work.

The implication (the point of validating first): the PURE tracker-miss families template cleanly via plain --hseq (Task 5: 399 banked). But the type-using families — the triage cracks AND the 61 Fable5 cores — are reconcile-class: their cracks bank as ov077 exemplars but need per-sibling re-reconcile to template ×134. That machinery EXISTS for h_norm (family_sweep --reconcile / reconcile_remap, the Phase-25 M2 4,389-bank path) — it just needs porting into the --hseq path (over remap_hseq, i.e. with cross-address + imm). So the Task-8 prerequisite before the Fable5 window is productive: wire per-sibling reconcile into hseq_sweep. Otherwise Fable5 output stalls at ×1 (ov077-only).

Outcome: kept the 4 real ov077 exemplar banks (byte-verified). Paused before building the per-sibling reconcile wiring (that IS Task 8, per Drew). LESSON: the validation slice paid for itself — it converted "the pipeline works, go spend the window" into "reconcile→bank works, but templating reconcile-class needs one more wiring step first," a decision that would have been very expensive to learn after the window closed.

2026-07-12 · Phase 26 — the crack-harvest has TWO tooling gaps + the rtu_match-vs-whole-binary lesson (Fable5 batch-1 processing)

Context: processing the Fable5 batch-1 cracks + the 23 triage isolation-cracks through the whole-binary gate revealed the "closeness-0 / rtu_match-MATCH" counts were optimistic. Whole-binary reality (G3/P9):

1. The rtu_match blind spot on jump-table functions. Both Fable5 cracks (func_80159C84, func_8015444C) rtu_match-MATCH but FAIL the whole-binary gate. rtu_match neutralizes INCLUDE_ASM (excluding the §8 jump-table rodata .s) and compares only the masked INSTRUCTION stream — it never verifies the jtbl rodata data bytes. The code is right; the jtbl rodata isn't confirmed. rtu_match is NOT a sufficient sole arbiter for jr-functions — the whole-binary gate is (as always, G3). Pattern is clean: all 6 whole-binary-banked cracks are no-jtbl; every jtbl crack (2 Fable5 + the 2 jtbl triage cracks) fails.

2. Two distinct harvest gaps, both fixable Task-8 tooling:

  • §8 jtbl-rodata gap: replacing an INCLUDE_ASM jr-function with C needs the compiler-generated jtbl to byte-match + land in the right rodata slot (the §8 dotted-.rodata-subseg + ld_interleave). The overlay splits don't have this per-cracked-jr-function setup → every jtbl crack fails the binary. This blocks the jtbl-heavy Fable5 window (most top cores are jr giants).
  • reconcile data-extern gap: ~15 of the 21 no-jtbl triage cracks fail canon_sig_reconcile on a conflicting types for D_x (fn-ptr-array / typed-global the seed declares differently than the TU) — pt-9 data-extern handling is incomplete for these. Only 6 no-jtbl reconcile-clean cracks bank (729 members: 463 committed + 266).

The implication for the Fable5 window: cracking a jtbl giant with an rtu_match-only agent produces an UNVERIFIED result — the §8 rodata must be handled + the whole-binary gate must be the arbiter. So the window is only productive on jtbl cores AFTER the §8-overlay-jtbl tooling exists (or with serial whole-binary verification). Better path (hindsight): the Fable5 crack prompt should have required the whole-binary gate (or an rtu_match variant that includes the jtbl rodata) as the bar, not plain rtu_match — for jr-functions the two diverge. LESSON: an indicator that MASKS a byte-region (rtu_match masks relocs + excludes neutralized INCLUDE_ASM rodata) cannot arbitrate a match whose difference lives in that region.

2026-07-12 · Phase 26 — §8 unblocked the HEAVIEST byte-weight lever (switch functions), reframing the endgame priority

  • Context / belief: built the §8 overlay jtbl-rodata tooling + the ×134 automation (jr-functions can now bank as C, proven func_8012ACE0 ×133), then recommended continuing with "the 45 small jr families" as the next mechanical lever.
  • Dead-end (the off-plan recommendation, Drew caught it): that rec optimized for mechanically easy (small families template cleanly) instead of the endgame's actual objective — heaviest byte-weight first. Measured: the 45 small jr families = 129,028 templatable ins (trivial).
  • Pivot: re-target the heavy tier. The frontier byte-weight (the instr-weighted metric lever): jr (switch) substantial families = 191 fams / 5,534,884 ins; non-jr substantial = 1,168 / 7,328,348; all substantial = 12.86M. And decisively — 9 of the 10 heaviest unmatched family cores are switch (jr) functions (func_80178D40 890×134 = 477K ins alone; func_8017BEBC 952×113; func_8015AE2C 562×134…).
  • Why: switch functions were UNBANKABLE before §8 (the jtbl-rodata duplicate). §8 didn't just enable a small mop-up — it unlocked the single heaviest chunk of the remaining game (the byte-weight is dominated by big switch cores). The endgame plan (heaviest-byte-weight-first via crack-core → template ×134) is intact; §8 was its key enabler, and the correct follow-through is the HEAVY jr cores, not the light tail. This needs Task 7 (Fable5) un-paused: Fable5 cracks the giant switch core → §8 + the ×134 automation bank it fleet-wide → the now-10×-faster R22 verifies.
  • Hindsight / for the wiki: when a build-mechanism unblocks a whole class, re-rank the endgame by the class's byte-weight, not by which member is easiest to bank next. "Easy and on-metric" ≠ "easy"; the plan's objective (heaviest byte-weight) must gate the next-target choice, or you grind the light tail of a newly-opened heavy vein. (Drew's steer: "the endgame plan is 1st smartest play to unlock the heaviest byte-weighted remainder — does your recommendation follow it?" — it did not; this corrects it.)
  • Sequencing refinement (Drew, same day): do the 45 SMALL jr families first as a de-risk preamble (NOT for byte-weight — ~+1% instr), THEN the heavy 191. The decisive reason isn't size: jtbl_carve only built the single-jtbl carve, but func_8012ACE0 is now matched in all 133 siblings, so family #2 forces the multi-jtbl address-ordered carve — build & prove THAT on cheap 30-ins targets before a Fable5-cracked 890-ins core depends on it. Also needs no Fable5 (progress without burning its limits). Guardrail: the small tier is a MEANS (harden the pipeline), not the objective — pivot to the heavy 191 once multi-jtbl is proven; don't grind the light tail because it "feels productive." Wiki lesson: when a newly-built mechanism has an un-built sub-case that the expensive targets will hit, force that sub-case out on the cheap targets first — de-risking and building-the-missing-piece are the same move.

2026-07-13 — the jr-core ISOLATION wall: mechanical TU-splitting breaks gcc-2.7.2's lenient scoping

  • Context + belief: Stage 2 of the multi-jtbl campaign (heavy jr cores → template ×134) needs each matched jr-function ALONE in its own code subseg so its jtbl carves without a same-subseg collision. Drew's steer: build the scalable "isolate-ALL-jr-per-sibling" upfront resegment (one-shot multi-cut per overlay) so every Stage-2 core bank is a trivial fill during the closing Fable5 window. Belief going in: this is mechanical source-splitting — partition the overlay .c at jr boundaries, repoint config + carves, rebuild byte-identical.
  • What was built + PROVEN: tools/overlay_src_split.py — an overlay-.c-aware partition (header = includes + Phase-17 canonical-sig layer; each addressed item = its preamble + body; robust definition/declaration/K&R/DEFINE_func/SETTER/RETCONST classification). Fleet-validated 404/404 overlay .c, 341,902 items, round-trip exact / 0 unresolved / 0 non-monotonic. tools/jr_isolate_all.py — multi-cut resegment (config split at jr boundaries, source repartition + INCLUDE_ASM path repoint, banked-jr carve repoint, -O0-object skip). SINGLE-cut isolation byte-identical (isolate func_8013FFD8 in the simple main object → clean make build = d19c9580, R22).
  • What FAILED (byte-verified): the FULL 54-jr isolation on ov_SC01_077 hits a long tail of C-scoping edge cases, culminating in the decisive one: func_801734BC uses D_80126B3E with no local decl; D_80126B3E is declared extern s16 ONLY inside DEFINE_func macros in engine_core.h. The original _after.c compiles because gcc-2.7.2 lets a block-scope extern (from an earlier DEFINE_func macro expansion) persist to file scope for the rest of the TU — splitting _after separates the core from the earlier macro that declares the symbol → undeclared. Earlier tail members (all fixed incrementally, in order): block-scope externs must not be hoisted (per-fn type shadows — D_80115118 is unsigned short in most funcs but the struct S115118 in one); file-scope decl ORDERING across a cut (D_80115110 used above its in-region decl); ambient decl context (a region needs the file-scope decls that lived in earlier regions of the object — solved: prepend, original order, shadow-safe because a file-scope-declared symbol can't carry a different-typed block shadow or the original wouldn't compile); file-local-typed externs (extern Vec8 D_…;) can't hoist above their typedef.
  • The why (root): these overlay TUs are hand-matched against a compiler that treats a block-scope extern as declaring the symbol for the WHOLE TU. Mechanical splitting into per-jr TUs breaks that invisible cross-function dependency, and the dependency is carried through DEFINE_func/SETTER macro expansions in engine_core.h, not just visible col-0 decls — so no amount of col-0 ambient-carry fixes it.
  • The candidate fix (not yet built): declaration-completion — build a global symbol→type map from engine_core.h's macro externs + all overlay col-0 decls, and for each region emit a file-scope extern <type> <sym>; for every D_/func_ symbol the region USES, EXCLUDING type-inconsistent symbols (the D_80115118 shadow set, kept block-scope in bodies). This makes every region self-contained regardless of where the original declared the symbol. Est. ~40–60 LOC on top of the proven parser; the whole-binary byte-gate arbitrates. Owner decision pending (Drew): invest in declaration-completion vs a different Stage-2 approach — surfaced this session before sinking more time (P5a: repeated failures, distinct root cause each).
  • Hindsight / for the wiki: "mechanical source split" of matching-decomp overlay code is NOT mechanical — the C is written against a specific compiler's lenient scoping (block-scope-extern TU persistence, macro-injected decls, per-function type shadows). Splitting a TU means REBUILDING each fragment's full declaration environment from a global symbol map, not relocating text. The parser (structure) was the easy 20%; the declaration environment (semantics) is the 80%. Prove the mechanism on the SIMPLE object first (it passed) but budget for the dense object's scoping tail before committing to upfront-×134.

2026-07-13 (session 6) — the §8b scoping wall RESOLVED: rebuild the decl environment, don't map symbols

Context + belief going in. Session 5 hit a wall isolating jr cores: the full 54-jr split of ov_SC01_077 failed with D_80126B3E undeclared, and I logged the cause as "gcc-2.7.2 block-scope-extern TU-persistence" — i.e. a non-conformant compiler quirk where an extern inside one function body leaks to file scope for the rest of the TU. The proposed fix (Drew-approved) was declaration-completion: build a global symbol→type map and emit a file-scope extern for every symbol a region uses, minus a heuristic "type-shadowed set".

What was actually wrong (R14 — the hypothesis was incorrect). There is no gcc quirk. DEFINE_func_80173460() expands at file scope to extern void func_801734BC(...); extern struct S80126B38 D_80126B38; extern s16 D_80126B3E; void func_80173460(...) { … }. Those externs are genuinely file-scope — they are merely textually invisible in the .c, because they live in engine_core.h. Any col-0 scan of the source can never see them. The wall was a blind spot in our own tooling, not a compiler eccentricity.

The pivot — and why the approved design was the wrong one. Chasing "declare every used symbol from a global type map" would have been actively harmful. The engine is loosely typed: func_80173544 is defined at file scope as s32 f(void *) while func_801734BC's body declares extern void f(void); — contradictory, and legal only because the block-scope decl never meets the definition. Hoisting "every used symbol" lifts that shadow to file scope, creating a conflict that then needs the heuristic shadow-set to dodge. Instead I reconstructed the original TU's file-scope declaration environment and carried it strictly forward. That is conflict-free by construction: every carried decl already coexisted with every definition in the one original TU, and decl compatibility is order-symmetric. Shadows stay inside bodies and travel with them. No heuristic, no shadow set.

What the bytes taught (found by gating, not by reasoning). Three decl sources were lost, not one — and I only found #2 and #3 because the byte-gate kept failing with a new error class each time:

  1. DEFINE_func_* macro leading externs (3,929 lines / 1,462 symbols) → D_80126B3E undeclared.
  2. A definition is itself a declaration for everything below it in its TU → func_8012B2CC undeclared.
  3. File-local typedefs used by a carried prototype → parse error before '*' (Vec3s).

Result. Full 54-jr isolate-all on ov_SC01_077 → d19c9580 byte-identical, R22 clean-fleet 136/136. Two latent bugs fell out and were fixed: func_subseg derived the owning subseg from the asm tree, which make extract never prunes — so after an isolation it returned the STALE owner and silently re-created the collision the isolation had just removed (now derived from the config); and the sweep's revert deleted the shared overlays.mk carve var unconditionally, which would have destroyed a committed carve (all 134 overlays have one) on any failed sibling (now restored to its committed value).

Upfront vs lazy (new information for the owner). Drew chose lazy isolation when isolate-all was failing, to avoid ~7,200 region files. Isolate-all is now byte-proven at 136/136, so upfront is available — but lazy is strictly cheaper (pay only for cores we bank) and is what shipped: jtbl_family_bank catches jtbl_carve's NON-CONTIGUOUS fail-loud → isolate that one core → re-carve. Proven on func_80178D40 (890×134, the heaviest core): blocked → isolated (byte-neutral) → carve lands in its own subseg. The heavy-jr harvest is unblocked.

Hindsight / for the wiki. Two lessons. (1) A wall's stated root cause is a hypothesis until the bytes confirm it — I recorded a compiler quirk that did not exist, and the "fix" it implied would have introduced real conflicts. Re-derive the mechanism before building on it. (2) Splitting a translation unit is a semantic operation, not a textual one. The parser (structure) was the easy 20%; the declaration environment (semantics) was the 80% — and the correct move is to reproduce the environment the original had, never to invent a new one from a global map. Faithful-forward-carry needs no heuristics; "declare everything used" needs a growing pile of them.

2026-07-13 (session 8) — the ×133 sweep blocker was OUR tool, not the compiler: the R17 triage rule, applied

Context / prior belief. Session 7 banked func_8015AE2C (562 ins, reach 134) ×1 but its ×133 sibling sweep failed on conflicting types for D_801812A4, and the checkpoint diagnosed it as reconcile_decls resolving against a fleet-majority canonical oracle instead of the type the TU can actually see. Drew had just asked the routing question and we had committed the rule: "wrong BYTES" → read the gcc source (R17); "won't COMPILE" → read our Python. This was the first real test of that rule, and it held — but the diagnosis underneath it was only half right, and the half that was wrong is the interesting part.

What the bytes taught. Reproducing one sibling by hand (rather than trusting the handoff — R14) produced a much sharper picture than the checkpoint's:

  1. The isolated region compiles and builds [ OK ] without the body. So §8b isolation was never implicated. The conflict is introduced entirely by the templated body.
  2. D_801812A4 was the only hard error in the whole build. All 27 carried function externs were fine raw — cast_call_sites was not needed at all. (The checkpoint's "cast_call_sites already fixes the function half" was true but irrelevant; it also implied ~4 data symbols needed reconciling. Eight were demoted; none needed a type reconcile.)
  3. The real mechanism is an ordering asymmetry, both halves byte-proven: BLOCK(int) → BLOCK(struct*) → FILE(void*) builds; FILE(void*) → BLOCK(int) is a hard error. family_remap.gather_externs prepends carried decls at file scope. For a per-location symbol the sibling declares only at block scope inside its own later functions, that carried decl establishes a global declaration the TU never had — and every later block-scope extern of it must now agree. In loosely-typed engine code they never do. D_801812A4 is one fn-ptr dispatch table declared four incompatible ways in a single region and the TU is perfectly happy — until we add a fifth decl at the top.
  4. reconcile_decls was the wrong instrument twice: its oracle answers "what does the fleet call this symbol" when the question is "what can this TU see" — and its DATA_DECL_LINE_RE cannot parse the fn-ptr-array form extern void (*D_x[])(void *); at all, so it silently skipped precisely the symbols that were failing. (This is the same "reconcile fn-ptr-extern gap" logged on 2026-07-12; it had been filed as a separate, smaller lever and was in fact the blocker itself.)

The pivot. Don't teach reconcile_decls a TU-visible oracle (the checkpoint's plan, and a much bigger, riskier change to a proven path). Instead don't change the TU's decl environment in the first place: tools/scope_data_externs.py demotes a carried D_ extern to block scope inside the function body whenever the TU has no file-scope decl of it above the insertion point. It then declares no global, nothing below can conflict, and the environment is preserved exactly. Byte-neutral (an extern emits no code; the declared type and every access opcode are unchanged), and strictly never worse than raw, so it needs no type comparator, no fn-ptr parser, and no oracle. It also restores fidelity — the original source declares these symbols at block scope in exactly this way. Wired as the scoped stage (raw → scoped → recovered → reconciled).

Result. First sibling byte-identical on the first try; the 133-sibling sweep run to completion.

Hindsight / for the wiki. Three lessons. (1) The R17 triage rule paid for itself immediately. The temptation with a conflicting types failure on a 1997 compiler is to assume the compiler is being exotic. It was not — gcc was correctly rejecting plain C89, and every minute spent in cse.c would have been wasted. Ask which half of the compiler is complaining: the front end (our C is invalid → our bug) or the back end (our C is valid but the bytes differ → read the source). (2) A tool that no-ops on the failing input looks exactly like a tool that has nothing to fix. reconcile_decls reported success while skipping the only symbol that mattered, because its regex couldn't see fn-ptr arrays — a silent-skip class we have now been bitten by three times (find_site braces, overlay_files splits, this). Prefer transforms that fail loud on unparsed input. (3) The cheapest fix was to do less, not more. The instinct was to make our reconciler smarter (a TU-visible oracle, a fn-ptr type comparator, a cast-at-use taxonomy). The correct move was to stop perturbing something we had no business perturbing. When a transform breaks a TU, first ask what it is changing that it needn't.

2026-07-13 (session 8, Fable5 Max) — func_8017BEBC closed: the allocno-tie class is a DIAL, not a wall

Context / prior belief. The 952-ins jr core (reach ×113, the largest unmatched function in the game) sat at close=2 — two transposed preheader addius. The session-7 Fable5 agent had localized it to global.c's allocno-priority tie and prescribed a §45-B gdb-on-cc1 read of allocno_live_length; the permuter had run 25 minutes without closing it. The residual class: allocation order and emission order are COUPLED (both follow creation order), but the target needs them to DIFFER — the shipped draft could have either correct, never both.

What the bytes taught. The dumps alone settled it — gdb was never needed. .lreg gave the two pseudos' ground truth: refs 13/13, live lengths 783/782 → pri = int(390000/L) = 498/498, an EXACT int-truncation tie (the agent's remembered "270000/L" had the wrong refs count — reading beats recalling, R14). The quantization boundary sat one insn away: +1 on both lengths → 497 vs 498. And the split direction is FORCED: the later-created pseudo always has the shorter live range, so a split always hands it the earlier allocation — precisely the "allocation ≠ creation" the target requires.

The pivot. Rather than hunting an L-shifter that survives cse (the agent's proposed hunt), the map's own zero-byte-asm toolkit already contained the dial: __asm__ volatile ("") placed BETWEEN two existing GTE volatile asms adds no new cse/sched barrier (one is already there) — it is purely +1 static insn at global-alloc time, zero bytes emitted. Natural operand order restored (emission correct), one slider inserted → MATCH 952/952 first try. Whole-binary gate BYTE-IDENTICAL (jr function — the §8a trap respected); one TU-visible decl reconcile en route (D_800B9A02, §8d sub-class b). Banked ×1; the ×113 sweep is IMM-class Task-8 work.

Hindsight / for the wiki. (1) An "irreducible" tie is often a measurable quantization accident — the formula is public, the dumps print its inputs, and the fix is one insn of live-range arithmetic. Before declaring a register-order residual intrinsic, READ THE PRIORITY NUMBERS. (2) The dumps-first discipline scales: .lreg/.greg gave everything gdb would have, at a fraction of the setup. gdb remains the tool for DYNAMIC questions (which reg find_reg actually grants when hand-modeling stalls), not for static quantities the dumps already print. (3) The zero-byte toolkit compounds: the slider now joins the density dial and the lifetime-extender as the third allocation dial that emits nothing — and the "adjacent to an existing volatile asm" placement rule makes it safe in GTE-heavy renderers, which is exactly where the remaining jr cores live.

2026-07-14 (session 8) — the silent-skip class: promote the lesson from a rule to a MECHANISM (Drew approved)

Context / prior belief. Six silent-skip bugs surfaced in one session (scope_data_externs' file-scope placement; extract_unit decl-vs-def; _body_open_brace's own-line brace; SIG_IN_BODY_RE's 10% oracle hole; revert()'s config residue; jr_isolate_all's empty region 0) — and THREE were the same brace-placement class, the same class as the Phase-15 find_site bug and the Phase-24 overlay_files bug. Each was written off at the time as a one-off parser slip.

What the bytes taught. They are not one-offs; they are a structural blind spot in how this project is built. Every instance has the identical shape: a scanner extracts N items from a corpus, the true count is M > N, and nobody ever compared N to M. The whole-binary byte-gate (G3/P9) is a perfect guard on correctness — it never once accepted a wrong match — but it is blind by construction to work that was never attempted. A tool that silently no-ops on input it cannot parse is indistinguishable from a tool that had nothing to do. That is why these survived 26 phases: nothing in the system was looking.

The cost is not hypothetical. SIG_IN_BODY_RE hid 186 of 1801 (10%) of the shared-callee signatures, which is why nine byte-exact cores from the crack wave would not bank — the draft kept its guessed signature, hit conflicting types, and the recovery pass truthfully reported nothing to fix. It read exactly like an intrinsic wall. One character class turned it into a zero-hand-edit bank.

The pivot (Drew, "agreed"). Promote the lesson from a rule to a mechanism, and do NOT audit by reading regexes — that is precisely the failure mode that wrote them. Instead measure coverage: for each scanner, build a deliberately OVER-APPROXIMATING candidate detector, run both over the corpus, and report found-vs-candidates; every gap must be classified as a real skip or a justified exclusion. Going forward, a new text scanner ships with a coverage assertion or it does not ship. (Rule candidate for PhaseEnd, P10.)

The bigger prize (the uncomfortable part). Several verdicts we have treated as settled physics were reached on top of the broken oracle: 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, the 9 zero-bank type-using families. Each was diagnosed as "no C declaration exists satisfying both the definition and the call site" — but the tool computing the call site's canonical signature was blind to 10% of them. 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 against the repaired tools.

Hindsight / for the wiki. The deepest lesson of the phase, and it generalizes far past decomp: an incorruptible correctness gate creates a false sense of completeness. It tells you everything you banked is right. It tells you nothing about what you never tried. Pair every correctness oracle with a coverage oracle, or you will spend phases mistaking your own parser's blind spots for properties of the problem.

2026-07-14 (session 8) — the coverage audit's biggest finding was REAL, and my reading of it was WRONG. The correction is the lesson.

Context / prior belief. Six silent-skip bugs in one session led to the coverage-oracle rule (Drew: "agreed"). The audit's headline came back alarming: progress.py under-counts by ~243k instructions because classify() reads a K&R definition as a forward declaration. I verified the MECHANISM against the bytes (it is real: s32 f(arg0) / s32 arg0; / { — the ; precedes the brace, so the scan calls it a declaration and drops the function into NO bucket), measured 400 banked instances / ~190k instructions in that shape, and told Drew our headline numbers had been under-reporting our own progress.

What the bytes taught — I was wrong, and the null result caught me. After fixing it, old-vs-new on the same tree moved the headline numbers by +376 instructions, not +190,000. A null result where a large effect was predicted is not noise; it is a refutation. Reading the code: weighted_metrics() never calls classify(). It determines "matched" as func not in src_stubs(binary) — and because the fleet is 136/136 byte-identical, anything NOT wrapped in INCLUDE_ASM is necessarily compiled C emitting the exact original bytes. It never parses a definition, so it is structurally immune to the bug. The published instr-weighted (65.6%) and distinct-code (44.9%) were CORRECT ALL ALONG; only the secondary REAL count and fn-count % were wrong.

I had done the R14 thing (verify the mechanism against the bytes) and still got the conclusion wrong, because I verified the DEFECT and not its BLAST RADIUS. The auditor conflated "classify() is blind" with "the metrics are wrong", and I propagated it — to the owner, as fact, in the same breath as lecturing about unverified oracles.

The pivot. Both bugs are still worth fixing (they corrupt the REAL/fn-count report, and the phantom-dedup over-count double-counts 532 stubs) and a coverage assertion now guards classify(). But the strategic conclusion inverts:

A metric DERIVED FROM A PROVEN INVARIANT beats a metric that RE-PARSES THE WORLD. weighted_metrics() leans on the byte-gate — "not a stub ⇒ byte-exact, because the build is byte-identical" — and inherits its correctness for free. classify() re-derives the same fact by parsing C, and inherited a bug instead. Two tools, one question, and the one that refused to re-derive is the one that was right.

Hindsight / for the wiki. Three lessons, and the third is the real one. (1) Verify the blast radius, not just the defect. "This tool is broken" and "this number is wrong" are different claims needing different evidence. A confirmed mechanism proves nothing about consequence. (2) A null result where you predicted a large effect is a refutation — chase it. The +376 delta was the whole story, and it would have been trivially easy to wave off as noise or as "the fix worked, the numbers moved". (3) The coverage-oracle rule is right but incomplete. Auditing parsers is treating the symptom. The cure is to STOP PARSING where an invariant already answers the question. Our byte-gate proves a strong property (byte-identical build); every fact derivable from it should be derived, not re-computed by regex. Before adding a coverage assertion to a scanner, ask the better question first: why is this scanner re-deriving something the build already guarantees?

2026-07-14 (session 8 close) — Drew: the TOOLING-INTEGRITY AUDIT gates further matching work, and gets its own phase

Context / prior belief. Session 8 was the most productive of the project: 13 cores cracked (incl. the four heaviest functions in the game), a 12-agent wave at 11/12 first-pass MATCH, fleet 63.0→65.6% instr-weighted / 39.1→44.9% distinct-code, 136/136 byte-identical throughout. The natural next move was obvious: bank the six blocked cores (~1.2 MB, all plumbing), then run the next wave.

Drew's call: "I feel like we should do T14 now, before the rest of the work. but not in this phase." The tooling-integrity audit gates the remaining matching work, and it is substantial enough to deserve its own phase rather than being squeezed into Phase 26.

Why this is right (and why I would not have prioritised it as hard). The session found seven silent-skip tool bugs, and the instinct is to treat them as a tax — annoying, fixable, keep moving. That instinct is wrong, for a reason that only became clear at the end:

The byte-gate is a perfect CORRECTNESS oracle and a null COVERAGE oracle. It 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 percentage. So every silent skip is invisible to the one instrument we trust absolutely.

The cost is not wrong answers. It is invisible work, and walls that aren't there. A single 10% hole in the callee-signature oracle (SIG_IN_BODY_RE, a \s that could not match a line-continuation backslash) made nine byte-exact functions look like an intrinsic compiler wall — and we would have written them up as such. 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, the 159 arity conflicts, the type-heavy tail — all were diagnosed on top of that hole. That is the question the audit answers, and it is worth more than the next 1.2 MB.

Auditing after more matching would compound the problem: every wave run on broken selection tooling produces more "walls" we would then have to re-litigate.

Scope discipline (do NOT audit all 82 tools). 19 were audited (23%), chosen by risk. The filter for the rest is: does it PARSE something, and does it GATE or SELECT work? (~15 tools.) Priority order: dedup_integrate.py (a fail-closed validator that can print a FALSE GREEN — "1813 validated, 0 failed") → jtbl_family_bank.py (3 bugs found by hand this session, never audited) → the SELECTION tools (family_hseq, wave_targets, exemplar_miner — a hole here makes work invisible to planning, the worst kind) → masked_diff/match_one (the closeness oracle every agent trusts).

And apply R33 to each, first: why is this tool re-deriving something the build already guarantees? harvest_verify is the model — it derives from make build + SHA1, so a parse hole makes it conservative, not wrong. Tools that lean on the invariant inherit its correctness for free; tools that re-parse inherit bugs. The best audit outcome is not a fixed regex — it is a deleted scanner.

Hindsight / for the wiki. The owner saw this faster than I did. I had just spent the session proving that every wall was our own tooling, had written the coverage-oracle rule, had corrected myself about a metric — and my instinct was still "bank the 1.2 MB first". The lesson: when your measurement layer is suspect, more measurements are not progress. Fix the instrument before taking more readings.


2026-07-14 (session 9) — The audit runs as an INSERTED HALF-PHASE, not as Phase 27

Context. Session 8 closed by gating the tooling-integrity audit ahead of all further matching work (entry above) and left the phase-boundary shape as an explicit Tier-1 question for the owner: (a) close Phase 26 early with a PhaseEnd and open the audit as Phase 27, or (b) run it as an inserted half-phase (the Phase-3.5 precedent) and return to Phase 26 afterwards.

Drew's call: "audit as an inserted half-phase in the current phase and then resume phase 26." (Effort: Max.)

Why (b) is right. Option (a) reads as the tidier choice — a clean PhaseEnd, a fresh phase, a fresh context window. But it would have closed Phase 26 on an unmet milestone. Phase 26's milestone is structural completion, and the audit is not a successor to that goal — it is a prerequisite to reaching it: the family engine's own numbers are what the audit found broken (93 of 218 "matched" exemplars are phantom; 1,834 clean member templates never attempted; 407 of 811 overlay files invisible to propagation). Closing the phase would have forced a PhaseEnd that reported the milestone as abandoned, when in fact the tooling that measures the milestone was the thing at fault. The half-phase keeps the goal live and fixes the instrument under it.

The Phase-3.5 precedent is exact: a spike inserted mid-arc, on the owner's directive, to answer a question that gates the work either side of it. It closed with a go/no-go, not a PhaseEnd, and the roadmap resumed.

The structural insight this rests on (worth repeating, because it is the whole reason the audit exists). A scanner extracts N items from a corpus; the true count is M > N; nobody ever compared N to M. The seven silent-skip bugs were not typos — they are that one blind spot, seven times. And the byte-gate cannot see it: it is a perfect correctness oracle and a null coverage oracle (green since Phase 5 at 0% decompiled, because INCLUDE_ASM pastes the original asm). So the audit's ordering rule is R33 before R32 — before adding a coverage assertion to a scanner, ask whether the scanner should exist at all. The best outcome is a deleted scanner, not a fixed regex.

First finding, immediately (A1). dedup_integrate.py — the fail-closed byte-honesty validator, and the audit's #1 priority precisely because a silent skip there prints a false green from a gate — has three false-green paths, all confirmed within minutes of opening it: the 7 stale registry groups name a DEFINE_func_* macro with zero hits in src/ and still print [ OK ]; an absent .run/sig.*.jsonl yields "0 validated, 0 failed" and exit 0 (on a fresh clone the gate validates nothing and passes); and it never checks that a member is actually banked rather than still INCLUDE_ASM — which is exactly the invariant the build already proves. The tool that guards byte-honesty was the one least able to prove its own.

Hindsight / for the wiki. The owner's framing — fix it inside the phase, don't ceremonially close the phase around it — avoided a subtle honesty trap. Writing a PhaseEnd that says "milestone: not met, closing anyway" when the real story is "our measuring tape was short" would have been technically true and substantively misleading. Phase boundaries should follow the work, not the paperwork.


2026-07-14 (session 9, A2) — The audit found the endgame plan was majority-fiction

What we ran. 6 auditor agents over the 18 unaudited PARSE+GATE/SELECT tools, each finding handed to an independent skeptic told to REFUTE it. 38 agents, 2.24M tokens. 32 findings raised → 28 survived, 4 refuted, 16 downgraded, and 40 scanners measured clean. The skeptic pass earned its keep: it killed four claims and corrected magnitudes in both directions.

The root cause is singular, and it is not a regex. Almost every finding is the same defect:

a hand-maintained model of the corpus layout — a file allowlist, a single-.c assumption, a func_-only symbol regex, a REGION_SUB dict — sitting on top of a filesystem that already answers the question.

An overlay's source is spread over up to 14 .c files (<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. The decay is measurable: .run/fuel_manifest.json from Jul 8 recorded 130 live stubs; the same tool run today returns 30. The Phase-26 splits moved ~100 stubs out from under a dict literal last edited in Phase 22. Nobody noticed, because a target that is never nominated produces silence, not an error.

Why this is worse than a wrong answer. 91.6% of all remaining project gain is invisible to the target-selection layer (994,633 instructions of real work; the manifest sees 83,305). 117 of the 127 reach-134 functions — the entire high-ROI band — are never nominated by anything. We were about to run Task 7's crack waves against that.

Three results overturn things we had written down as settled:

  1. "The permuter's fuel is exhausted" (Phase 22) is unsafe. The grinder banks through harvest_verify, which can only see one translation unit — and 1,290 of the grinder's own 1,298 queued functions live in a different one. 99% of its queue could never have banked, however good the permuter's output was. "7 all-time banks, 0 since Phase 21" is equally consistent with "the tool could not bank" as with "there was nothing to bank." We concluded the latter and moved on. Re-test before repeating it.

  2. The Phase-25/26 endgame plan is majority-fiction. docs/family-manifest.md — the document the whole structural-family endgame is planned from — advertises 2,758 multi-member families holding 11.0 MB of hidden leverage. 1,071 of them (6.80 MB, 62% of the advertised byte-weight) are already fully matched. The matched-set oracle scans a single overlay. So the byte-weight ranking, which is the entire purpose of the file, is sorted mostly on dead work, and the real targets are buried under phantoms.

  3. A corpus defect the byte-gate cannot see, and never could. config/symbols.us.txt:981 declares listCdBuffer = 0x80180000 — a main-EXE data symbol — and every overlay's splat config loads that file. In overlay space, 0x80180000 is code. splat therefore cuts 97 real functions in half and invents 96 phantom ones: 193 slices that cannot be matched by anyone, across 97 of 134 overlays. You cannot write C for a function that ends on a lui with no return, nor for one that begins by reading the assembler temp $at. They sit in the harvest queue as ordinary work items, so agents burn on them indefinitely and the failure reads as an intrinsic compiler wall. And the full-binary byte-gate stays green the entire time, because the .s halves are pasted back verbatim in original order.

    This is the purest instance of the thesis that motivated the audit: the byte-gate is a perfect correctness oracle and a null coverage oracle. And note precisely what rescued us — sig_image was right. Its independently-computed function boundaries agree with spimdisasm on 58,524 of 58,621 functions, and on all 97 disagreements sig_image is demonstrably correct. A second, independent oracle is the only reason the defect was visible at all. That is a design lesson worth more than the fix: when one oracle is structurally blind to a class of error, the answer is not a better assertion inside it — it is a second oracle that can disagree with it.

The fix follows the root cause: ONE derived corpus oracle, and ~10 deleted scanners. Not ten fixed regexes. tools/corpus.py answers — from the filesystem and the proven invariant, with coverage assertions baked in — which files make up a binary, which stubs are live, which functions are matched (sig − stubs, derived, never re-parsed), and where a function's asm lives (globbed, because splat already wrote the truth). Then the allowlists, the REGION_SUB dict, the single-TU regexes, and census_conflict_callees in its entirety all get deleted. This is the "best outcome is a deleted scanner" rule (R33) applied at scale.

Hindsight / for the wiki. The strategic error was not writing any one of these tools badly. It was letting the corpus layout become a fact that lived in ten places. Each split was a correct, well-gated change to the build; none of them updated the ten private models of the tree, and nothing existed to notice. A derived fact has no maintenance cost and cannot rot; a hand-maintained copy of it is a liability that grows with every structural change. And the reason it stayed invisible for four phases is the deepest lesson of the audit: we had no instrument that could report absence. Every gate we owned answered "is this right?" — none answered "is this all?"


2026-07-14 (session 9, A4) — A corpus defect the byte-gate could never have caught

The defect. config/symbols.us.txt:981 declared listCdBuffer = 0x80180000. That is a correct, Phase-3-derived name for a main-EXE RAM buffer (the LIST.CD cache). But 0x80180000 lies outside main's image (0x80010000–0x80074800) and inside the overlay slot (0x80128158–~0x801DAB30) — and every overlay's splat config stacks symbols.us.txt. High RAM is reused: an address that is a buffer to main is live code to an overlay.

So splat saw a symbol boundary in the middle of overlay code and, across 97 of the 134 overlays:

  • cut 97 real functions in half — leaving a head that ends on a lui with no return, and
  • invented 96 phantom functions — a tail that begins by reading the assembler temp $at.

193 slices that nobody can ever match. Not "hard". Not "a compiler wall". Unmatchable by construction — there is no C you can write for either half. And they sat in the harvest queue as ordinary work items, so agents would burn on them indefinitely and the failures would be filed as intrinsic compiler residuals.

Why no gate caught it, and why that is the important part. INCLUDE_ASM pastes the two .s halves back verbatim, in original order, so the image is byte-identical either way. The full-binary byte-gate — the instrument this project trusts absolutely, and rightly, because it has never once accepted a wrong match — was green the entire time and always would have been. It is a perfect correctness oracle and a null coverage oracle. No assertion added inside it could ever have found this.

What found it was a second, independent oracle: tools/sig_image.py derives function boundaries from the ORIGINAL bytes without splat, and it disagreed with the corpus. It agrees with spimdisasm on 58,524 of 58,621 functions and is demonstrably correct on all 97 disagreements. That is the whole lesson, and it generalises well past this bug:

When one oracle is structurally blind to a class of error, the answer is not a better assertion inside it. It is a second oracle that can disagree with it.

make audit-corpus now is that second oracle, standing.

The evidence that makes it concrete. The phantom listCdBuffer.s in ov_SC01_005 begins: lw $ra, 0x10($sp) / addiu $sp, $sp, 0x18 / jr $ra. splat cut a function immediately before its epilogue and called the epilogue a function. You cannot write C for a routine that restores a return address it never saved.

And it had already contaminated real work. In ov_SC03_031 the cut happened to land where the epilogue was exactly jr $ra; nop, so the Phase-26 ×134 sweep innocently "matched" it as void listCdBuffer(void) {} — byte-correct, gate-green, and completely fictitious — while leaving func_8017FFC4 permanently unmatchable. A phantom got banked.

The rule, which nobody had written down. R13/R15 say overlay-derived symbols are overlay-region only and must never be merged into symbols.us.txt. The mirror is equally true and was never stated:

A symbol whose address falls inside ANOTHER binary's vram window must never enter that binary's symbol stack.

Fix: config/symbols.us.ram.txt — main-scoped symbols that live outside main's image — stacked only by config/splat.us.exe.yaml. Main keeps the name (its asm carries 10 %hi / 11 %lo references and rebuilds 143dbb89 byte-identical); the overlays never see it. Exactly one symbol was in scope fleet-wide, and the resident window was clean.

Hindsight / for the wiki. We had two oracles all along and never made them argue. The byte-gate and sig_image were both trusted, both correct, and silently disagreeing about the shape of 193 functions for four phases. The cheapest possible check — do our two independent views of "where does this function start and end" agree? — was never run, because each oracle was individually green and nobody thought to ask them the same question. Redundancy is only worth what you spend comparing it.


2026-07-14 — cdecl: parse the grammar, do not enumerate the shapes

Context & belief. The audit's own prescription for the fifteen broken declaration scanners was a shape-aware alternation per tool: add an (fn-ptr|sized-array|scalar) branch to DATA_DECL_RE, mirror it in DATA_DECL_LINE_RE, add a fnptr kind to parse_data_decl, add a fn-ptr arm to _uniquify_draft_types, and so on — roughly fifteen coordinated regex edits, each with its own suggested coverage assertion.

Why I did not do that. The audit had already proved that fifteen independent hand-maintained models diverge: two tools in ONE pipeline disagree today about whether extern s32 D_a, D_b; is a declaration. Patching fifteen regexes is fifteen fresh chances to diverge again, and an alternation only ever covers the shapes somebody remembered — it is the same hand-maintained model, one shape wider. The real problem was never the character class. It was that the thing being scanned has a grammar, and nobody was parsing it.

C's declarator grammar is small, closed, and total. It describes fn-ptr arrays, sized and 2-D arrays, multi-declarators, fn-ptr parameters, and K&R identifier-lists without being told they exist. A 250-line recursive-descent parser is less code than the fifteen regexes it deletes, and it is exhaustive by construction rather than by anyone's memory. That is R33 in its strongest form: the best outcome is not a fixed regex — it is a deleted model.

The measurement (not a belief). Three oracles, whole corpus: coverage (2,952,246 depth-0 statements → 2,731,521 declarators, 0 parser defects), the real cross-gcc (50,405 distinct declarations round-tripped, 0 rejected), and a differential against the incumbents (0 symbols they see at file scope that cdecl misses; 26 in engine_core.h they cannot see; 6 they wrongly promote from block scope).

Two design decisions worth keeping.

  1. The candidate set is derived, not hand-written. At file scope C admits nothing but declarations, so the over-approximating detector R32 demands is every depth-0 statement — supplied by the grammar, with no second model to rot. (LAW 4.)
  2. gcc adjudicates my own coverage gap. When 40 statements would not parse, deciding for myself which "don't count" is grading my own homework — the precise habit that wrote the fifteen bugs. gcc decides instead: a statement it also rejects is not C (my rejection is correct, the input is corrupt); one it accepts and I do not is my defect. All 33 residual came back NOT-C, all in dead scratch. (LAW 5.)

Hindsight / for the wiki. The near-miss is the lesson. Those 33 corrupt drafts were written by a recovery tool that prepended extern to an if statement, and I was one step from reporting a live tool bug. Checking the blast radius instead (R14) showed the source defect was fixed back in Phase 19 — today's oracle emits 0 garbage over 300 signatures. Mechanism confirmed, consequence nil. But note what it cost while it was live: a draft that cannot compile fails the byte-gate, and the failure reads downstream as an intrinsic compiler wall. That is the audit's whole thesis in one artifact — and the new parser is what finally makes the guarding assertion expressible: every canonical signature the callee oracle emits must PARSE as a C declaration. Before cdecl, nothing in the repo could tell a signature from garbage.

Scope discipline (deliberate). This commit lands the parser and its proof and changes no consumer — so it cannot move a byte, and check-all is 136/136 by construction. That is not timidity: the audit explicitly warns that making the parser see more ARMS dormant downstream transforms — the moment reconcile_decls can parse a fn-ptr decl, its data_access_subs would happily mangle D_1[i]() into ((u8 *)D_1)[i](). Consumer migration is therefore one tool at a time, each byte-gated.


2026-07-14 — Probe the compiler; and the adjudicator must BE the compiler

Context. Building cdecl.compatible() — "will cc1 accept these two declarations of one name?", the question every recovery pass in this repo actually asks and four of them half-implement. I wrote the rules from the C standard, then validated them against a compiler.

What happened. The compiler contradicted me — and then the right compiler contradicted the first one. Validating against modern mipsel-linux-gnu-gcc and against the real gcc-2.7.2 cc1 gives three different answers (with the standard as a third): typedef redefinition is an error in C89, accepted by C11 gcc, and rejected by cc1; a qualifier mismatch is an error to modern gcc and accepted by cc1; the no-prototype/narrow-param rule is an error to both — and accepted by cc1 in one direction.

The decision. --compat adjudicates with tools/bin/gcc-2.7.2-psx/cc1, the front end that actually arbitrates the build. Now 1,485/1,485 live corpus pairs agree. Validating a compiler rule against a compiler that is not the one compiling your code is not a shortcut — it is the same class of error as the five phases we spent reading gcc-papermario believing it was 2.7.2. It was 2.8.1.

The prize (→ A10). Phase 15 closed the "159 arity/narrow-param conflicts" as "no clean deterministic fix — it is simply C's default-promotion rule." cc1 disagrees. The rule is order-dependent: void X(s16); void X(); compiles; only void X(); void X(s16); fails. The wall's stated cause does not hold. Four three-line probes, 90 seconds, zero tokens.

Hindsight / for the wiki. Probe the compiler for FACTS; read its source only for LEVERS; byte-validate both. Reading source is inference and can be wrong (it was, for five phases). Probing is ground truth, because it IS the compiler — and it is orders of magnitude cheaper. We have the exact binary sitting in the tree and spent 26 phases reasoning about it instead of asking it.

And the discipline that saved this from being an over-claim. Fixing the wrong-TU bug (95.1% of drafts canonicalized against a TU that would never compile them) took the callee-conflict repair from 8 to 58 of 196 drafts — 7× reach — and banked exactly zero functions, because the historical tail fails on codegen, not plumbing. The real gain is narrower and still worth having: 52 drafts moved from "won't compile" to "compiles, N instructions off" — from an invisible failure that reads as a compiler wall into a scored near-miss the permuter can act on. Three times in one session a confirmed mechanism produced a null consequence. "This tool is broken" and "this number will move" are different claims, needing different evidence.