Files
BFM-decomp/cookbook/C0064.md
T

21 KiB
Raw Blame History

§61 — Task 14: the gate ladder's missing stage is the ARITY pre-pass, and it is TU-side not draft-side (Phase 29, 2026-07-21)

gate_stage's recovery ladder was four DRAFT rewrites (canon_resident_calls → cast_call_sites → reconcile_tu → gate → sig_unify → gate). The dominant residual blocker is not in the draft at all.

Diagnosis (not assumption). The 12-draft integration probe banked 1/12 and reported the SAME failure label for 10 of the 11 failures: warning: conflicting types for built-in function 'memcpy'. That label is the §58 red-herring — it is a WARNING, from an unrelated TU position, and it is not the failure. Splicing three of the highest-reach failures individually and reading real cc1 stderr gave the actual cause:

src/…_jr_8016AB6C.c:3839: conflicting types for `func_8016EFC8'      <- 3 of 3
src/…_jr_8012ACE0.c:879:  redefinition of `struct V8'                 <- a SECOND class

The first is the loose-typing arity conflict: an already-banked shared caller macro in src/shared/engine_core.h declares the function with FEWER parameters than its byte-true definition takes (extern s32 func_8016EFC8(s32); and it calls with one arg, while the def takes two — the original calls K&R-style with fewer args than the callee reads). A C89 prototype makes that a hard error.

The fix already existed and was simply not wired in. tools/fix_arity_callers.py --any-proto rewrites those caller decls to the no-prototype K&R form (byte-neutral: an empty/short call emits identical code, and a no-proto decl is compatible with a definition whose params are default-promotion-safe). Byte-probe: func_8016EFC8 (reach-138) went gate-REJECTED → BANKED byte-identical after 7 caller decls were rewritten.

Wiring notes that cost real time:

  • It is a TU-side pre-pass, not an _xform: it edits shared state (engine_core.h + the overlay's own inline caller decls), so it is scoped to the drafts in play and reverted for every function the gate then rejects — a bank that SUCCEEDED must keep its loosened decl or the tree stops building.
  • --funcs is REQUIRED; --drafts is only the narrow-param filter. Wiring it with --drafts alone made the stage exit no funcs given — and because sh() does not raise on a non-zero exit, the surrounding try/except never saw it. The stage silently did nothing and the gate reported 0/6 as if diagnosed. Hence the explicit returncode check now in the ladder: a pre-pass that quietly no-ops is indistinguishable from one that found nothing to do, which is the exact failure this ladder exists to remove (R32). Author's note: this was committed roughly an hour after writing §60b about silent skips.

Measured: on the 7 highest-reach-weighted ov_SC01_077 integration candidates, the enriched ladder banks 2 (func_8016EFC8, func_80164418) against a 1/12 baseline for the old ladder.

THE INCIDENT THIS STAGE CAUSED, and the constraint it establishes. Pairing --apply --any-proto with --revert for the drafts that did NOT bank broke 138 of 140 binaries. --revert rewrites () -> (void), which inverts a PLAIN apply but not --any-proto (which relaxes ANY prototype), so the round-trip turned an unbanked function's real decl extern void func_801708B0(void *a0); into (void) — a DIFFERENT signature — in engine_core.h and in 6 places in the overlay's own sources.

A SINGLE-BINARY GATE CANNOT VALIDATE A FLEET-WIDE EDIT. harvest_verify --binary ov_SC01_077 reported byte-identical and was RIGHT — about that one binary. The other 137 were broken and structurally invisible to it, because the edit lands in a header all 138 overlays include. Only the standing R22 clean-fleet sweep saw it. This is the same shape as §55b's propagation law, one level down, and it yields a hard constraint:

Any ladder stage that mutates SHARED state (engine_core.h, engine_types.h, another binary's TU) must be undone by SNAPSHOT RESTORE, never by an inverse transform, and must be validated fleet-wide (R22) rather than by the per-binary gate that authorised it.

gate_stage now snapshots every file the pre-pass touches and undoes by restore + re-apply-for-the-banked- set-only: exact by construction, and incapable of inventing a signature. The planned type-lift stage edits engine_types.h — also shared — so it inherits this constraint by default.

The residual class, named for the next stage: redefinition of 'struct <T>' — the draft defines a local struct the TU already defines. That is the type-lift / local-typedef-uniquify class (§19/§57a/§59), NOT the arity class, and three of the five remaining failures carry a (void) header decl that the arity pass alone does not clear. Wire that next, and validate it the same way: splice one, read real stderr, probe, then wire.

§61a — The Task-5 wave: 11/12 MATCH, 0 banked — three DISTINCT integration walls, each now named (Phase 29, 2026-07-21)

A 12-agent Ultracode wave over freshly-prefetched ov_SC06_018 exemplars returned 11 MATCH / 1 near (~2M agent tokens), including all three giants (710 / 673 / 478 ins). The whole-binary gate banked ZERO. This is §58's law at its sharpest — and splicing each class individually gave three different blockers, none of which the ladder currently clears:

  1. jtbl NON-CONTIGUOUS CARVE — 10 of 12 drafts. (Corrected: I first filed this as "§8e-2 table-count drift". That is the SYMPTOM the filter reports; it is not the wall, and the fix is NOT a jtbl_carve code change.) jtbl_rodata_pads: more rodata .align directives than pad specs (2) — table-count drift vs the carve. The draft introduces a switch/jump table into a TU whose carve has a FIXED pad spec, so jtbl_rodata_pads fires. But re-running jtbl_carve --func <fn> to re-derive the spec REFUSES with the real reason: "subseg would host NON-CONTIGUOUS .rodata carves (0xaa810 and 0xaa920) — a single object can't leave a gap for the unmatched jtbl between them." The newly-banked function's table is separated from the TU's existing carve by an UNMATCHED function's table, and one object cannot straddle that gap.

    THE RECIPE (byte-proven on func_80135A4C, 181 ins / 138 members):

    tools/jr_isolate_all.py <ov> --only <fn>     # give the fn its OWN code subseg
    make extract BINARY=<ov> && make build       # isolation is BYTE-NEUTRAL by construction — verify
    <splice the draft>
    tools/jtbl_carve.py <ov> --func <fn>         # now the table carves contiguously in its own object
    make extract BINARY=<ov> && make build       # -> BYTE-IDENTICAL
    

    The tool names its own remedy in the refusal message, and jtbl_family_bank already auto-isolates on this class (Phase-29 Task-8) — but gate_stage/harvest_verify do NOT, which is why a wave that banks through the ordinary gate reports a flat 0 and looks like a compiler wall. A config change needs make extract, not just make build (the R22 corollary) — both steps above. The structural finding: fresh crack fuel in a well-matched overlay CONCENTRATES in jtbl-carved TUs (10 of 12 here), because the non-carved TUs were harvested first. So §8e-2 is not a rare straggler — it is the gate on the next tranche of substantial cracking.

  2. §57 self-decl / prototype conflict — the 2 plain-TU drafts. argument 'arg2' doesn't match prototype (def at :595 vs the TU's own decl at :447). tools/normalize_self_decls.py exists for exactly this and is wired into family_sweep but NOT into gate_stage — the same gap the arity pre-pass had.

  3. Local-type redefinition (redefinition of 'struct V8') — seen in the Task-14 diagnosis set; wants the type-lift.

So gate_stage's ladder needs three stages, not one, and today only the arity pre-pass landed. Ranked by what they unblock HERE: jtbl-drift (10/12) > self-decl (2/12) > type-lift.

Method note that made this cheap: the gate's own per-draft label is useless for this (§58's memcpy red-herring), and make build … | grep -i error MISSED the real failure twice — once because the true error was a jtbl_rodata_pads line containing no "error" token, once because the build failed at a later stage than the warnings I was reading. Check rc, and read the tail unfiltered. A filtered build log is a selection tool, and every selection tool in this project has eventually lied (R32/R35).

Preserved: all 12 drafts at .run/giants/t5wave_* (R20) — they are genuine cracks with per-function lever notes, recoverable the moment the three ladder stages exist. Do NOT re-draft them.

§61b — The jtbl gate stage: built, and the ORDERING law it exposed (Phase 29 Task-14 stage 4, 2026-07-21)

gate_stage now carries a jtbl stage (_jtbl_prepare): for every draft whose function references a jtbl_, carve its table into a contiguous object, auto-isolating (jr_isolate_all --only <fn>) on the §8b walls — the logic lifted from jtbl_family_bank rather than re-implemented (R33). It is wired, it runs, and it does not yet bank, for a reason worth writing down:

THE CARVE MUST FOLLOW THE SPLICE. The non-contiguity that requires isolation is only detectable once the function's body is in the object. While it is still INCLUDE_ASM, jtbl_carve reports SUCCESS and produces a spec that does not hold once the body lands.

Byte-witnessed both ways on func_80135A4C: carving the spliced function → NON-CONTIGUOUS … 0xaa810 and 0xaa920; carving the unspliced one → prepared 1/1, no isolation, and the draft then gates as a byte-DIFF. The MANUAL order banks it byte-identical:

jr_isolate_all --only <fn> ; make extract ; <splice> ; jtbl_carve --func <fn> ; make extract ; build

gate_stage runs the stage before _gate1, but harvest_verify owns the splice — so the fix is a per-draft prep INSIDE the splice loop (harvest_verify), not a batch pre-pass in gate_stage. That is the next increment; the stage's carve/isolate/undo machinery is correct and reusable as-is.

RESOLVED (2026-07-21, same session): the prep belongs in harvest_verify, and it BANKS there. harvest_verify._jtbl_prep() splices each table-bearing draft TEMPORARILY, asks jtbl_carve, isolates on the §8b walls, un-splices, re-extracts, and re-derives the stub map + baseline (isolation MOVES a stub's TU, so both are keyed on stale paths otherwise). Byte-proven: func_80135A4C goes [jtbl] carved 1/1 -> + chunk(1) -> BYTE-IDENTICAL, fully automated.

BUT THE BATCH STILL FAILS, AND THE REASON IS A NEW, PRECISE TOOLING GAP:

Banking a jtbl core makes its own carve UNOWNED to jr_inventory, which then refuses every subsequent isolation in that overlay — committed .rodata carve ownership is not 1:1 (R32/R33) — a stranded/duplicated carve (§8b func_801734BC class): [('UNOWNED', '0x801d288c')].

Byte-proven both ways: on the COMMITTED tree jr_isolate_all --only func_80135260 --dry-run succeeds; with func_80135A4C banked it fails the ownership assertion. The assertion is right — a banked function's stub .s is pruned, so the owner lookup finds nobody — but its conclusion is wrong: the carve IS owned, by C rather than by a stub. So today jtbl cores bank ONE PER OVERLAY. Measured on a 10-draft batch: 6 table-bearing, 1 carved, 4 isolate-FAILED on this assertion, 1 stale-asm carve failure.

NEXT INCREMENT (precise): teach jr_inventory's ownership check to attribute a carve to a BANKED (C) function — i.e. resolve owners from corpus.matched ∪ stubs, not stubs alone (R33: the same derive-don't-reparse move that fixed the corpus oracle). That unblocks batch jtbl banking and the 9 preserved cracks.

Two sub-findings, both paid for:

  • A wholesale git checkout -- config/… undo is WRONG in a batch gate. jfb.revert is right for jtbl_family_bank's one-function-at-a-time flow, but here it discarded a PREVIOUSLY-banked-but- uncommitted carve in the same overlay, leaving that bank's source with no subseg → undefined reference to func_80136C90 at link. An inverse/wholesale undo cannot know what it did not do. Now a SNAPSHOT-RESTORE of config/splat.<ov>.yaml + config/overlays.mk, plus removal of only the region files THIS run created (§61's constraint, applied where I had first ignored my own rule).
  • Being in a _jr_* TU ≠ having a table. Only 4 of 8 wave drafts in jtbl-carved TUs actually reference a jtbl_; the stage correctly prepares only those. The other 4 fail for other classes.

§61c — The jtbl bank is INCREMENTALLY valid and CLEAN-INVALID (Phase 29, 2026-07-21) — the blocking finding

func_80135A4C banks through the automated jtbl path every time: [jtbl] carved → + chunk(1) → verified 1 / failed 0 BYTE-IDENTICAL. And it fails a clean rebuild, twice, identically:

incremental (harvest_verify's own gate) : BYTE-IDENTICAL
make clean && extract-all && check-all  : 139 passed, 1 failed  ([FAIL] ov_SC06_018)

So the carve+isolation path yields a state that is not reproducible from committed config + source — the incremental tree carries something the clean pipeline does not reconstruct (extraction order, or asm that only exists mid-flow). This is the §42b stale-incremental false pass in its most expensive form: the gate that authorises the bank cannot see the defect, because the gate IS the incremental build.

Until that reproducibility gap is closed, NO jtbl core can be banked — not by hand, not by the ladder. The correct next step is to diagnose the divergence itself (diff the incremental vs clean build/ov_SC06_018/** object set and the generated .ld/asm for the carved subseg), NOT to bank more.

⛔ §61c IS REFUTED — the blocker does not exist (2026-07-22, R35/R14)

The diagnosis above was run, and it never got as far as diffing objects, because the failure does not reproduce. On a tree carrying ONLY this bank, applied through the single-function automated path (harvest_verify --chunk 1, [jtbl] carved func_80135A4C → + chunk(1) → BYTE-IDENTICAL):

per-binary clean (rm asm+build for the ov; extract; build)   : BYTE-IDENTICAL  cbbc4f44…
make clean && extract-all && check-all   (run 1)              : 140 passed, 0 failed of 140
make clean && extract-all && check-all   (run 2, independent) : 140 passed, 0 failed of 140

So the carve+isolation path IS reproducible from committed config + source. There is no extraction-order effect and no mid-flow asm: the state the incremental gate blesses is the state a clean pipeline reconstructs.

What the 139/140 actually was. The failing R22 runs were taken on the tree left by the batch _jtbl_prep — the same run that ended 6 table-bearing → 1 carved, 4 isolate-FAILED, 1 stale-asm carve fail. That tree carried the residue of five failed preps (stranded carves and half-applied isolations); the per-function snapshot-restore that removes exactly that residue landed after those runs, in the same commit that named the blocker (41d65af73). The measurement was real; its attribution was to the wrong cause. The failing tree is not recoverable, so this is stated as the best-supported explanation, not a byte-proof — but the claim that matters (the path is clean-invalid) is byte-refuted twice, and that is the claim that was blocking the work.

The transferable lesson is R35 pointed at ourselves: a fault observed on a tree that is known to be polluted must be re-observed on a clean one before it is written down as a property of the mechanism. "Twice, identically" felt like replication; it was two reads of the same contaminated state, which is one observation. A replication has to re-create the state, not re-run the check.

Faults 1 and 2 below are unaffected — they are real, they are what polluted the tree, and their fixes are what makes the single-function path reproducible. Constraint that stands: jtbl drafts are processed one per harvest_verify invocation until the undo is region-aware.

Two design faults found on the way, both real and both fixed in harvest_verify:

  1. A stranded carve poisons the overlay. _jtbl_prep carved a draft the gate then REJECTED; the carve stayed with no owner (the fn is still INCLUDE_ASM), and jr_inventory's 1:1 ownership assertion then refused EVERY later isolation in that overlay ([('UNOWNED','0x801d288c')] = func_801299C8's table). The assertion was RIGHT and caught it — R32/R33 working exactly as designed. Fix: per-function carve with snapshot-restore on gate rejection.
  2. Per-function undo is unsound in a BATCH. Isolation REPARTITIONS shared source, so restoring one draft's snapshot deletes region files that now host OTHER pending drafts — their stubs vanish (KeyError in render). jtbl drafts must therefore be processed one per harvest_verify invocation, or the undo must be region-aware.

Measured, so the next session does not re-derive it: of 11 preserved wave cracks, exactly ONE (func_80135A4C) reaches byte-identical through the carve path; the other four table-bearing ones fail one-at-a-time too, on the PLUMBING classes (§57 self-decl et al), not on the carve.

§61d — The undo was eating the tree: two tools, one defect, invisible to the byte-gate (Phase 29, 2026-07-22)

The §61c blocker turned out not to exist (see the REFUTED block above), and chasing why it had ever been observed found the mechanism — in two tools, both times invisible to the gate that caused it.

jr_isolate_all repartitions a code object by writing region 0 back over the ORIGINAL src/<ov>/<nm>.c, TRUNCATED to just that region, and emitting the remainder as new _jr_<lo>.c files. An undo that restores only config/ and deletes the new region files therefore leaves the original TU permanently truncated — its stubs are gone, and nothing regenerates them (splat does not rewrite a committed overlay .c).

Both harvest_verify._jtbl_restore and gate_stage._jtbl_prepare snapshotted only the two config files. Measured, twice, on live re-probe runs: live stubs 419 → 414 → 406 → 395, five orphan region files, and undefined reference to func_80192F64 at link.

Why it survived so long: it is invisible to the byte-gate. The incremental build keeps linking the stale objects (§42b), so make build stays GREEN while a clean rebuild fails — the gate that authorises the bank is the same incremental build that hides the damage. This is the §42b blind spot doing real, cumulative damage, and it is what manufactured the "139/140, twice" reading that became the §61c blocker.

Fixes. harvest_verify: snapshot every src/<binary>/*.c, restore on rejection, and delete exactly the files the attempt created — derived from the snapshot's file set, not re-guessed from the _jr_* name shape (R33). gate_stage._jtbl_prepare: DELETED, not patched (R33 — the best outcome is a deleted stage). It was wrong on two independent axes: §61b had already byte-proved the carve must FOLLOW the splice, and harvest_verify now does the correct per-draft prep one layer down. Two implementations of one capability, the outer one both ineffective and destructive.

A label that is constant carries no information (and is worse than none). classify_fail searched the whole build stderr, so warning: conflicting types for built-in function 'memcpy' — benign, from an unrelated TU position, present on essentially every overlay build — won the match on 8 of 8 failures spanning four genuinely different causes. That is the §58 red-herring, and the cookbook had been recording "the gate label is useless here, splice individually and read real cc1 stderr" as a manual workaround for a one-line bug. Fix: classify on NON-warning lines; fall through to CC1-FAIL:<last error line>. The same failure instantly became ov_SC06_018.c:447: prototype declaration.

What the honest re-probe then measured (11 preserved t5wave cracks, one invocation each, clean tree):

  • func_8018F694 (478 ins) BANKED — a giant previously inside "the gate banked ZERO".
  • The other 10: 4 data-decl · 3 callee-decl · 3 self-decl conflicts. ZERO jtbl-drift, ZERO local-type redefinition, ZERO codegen DIFF. So §61a's "§8e-2 jtbl drift blocks 10 of 12" does not survive the carve-follows-splice prep — the carve succeeds; what is left is ordinary decl plumbing.
  • Through gate_stage's ladder: 0/10 bank, but 9/10 now COMPILE and land as whole-binary byte-DIFF. match_one close=0 on several and rtu_match MATCH-in-real-TU for func_80135888, while func_801299C8's transformed draft does not compile in its real TU at all — the residual is MIXED, and at least one is an IMAGE-level effect rather than the draft or its TU context (prime suspect: jtbl/rodata carve placement). Deliberately not generalized from one data point.

The pricing this yields: the existing ladder converts 0 of 10 of these residuals. Task 14 stages 2-3 are therefore NOT "wire in normalize_self_decls + the type-lift and collect ten banks" — a measurement, not a projection, which is the error §57a already caught once this phase.

The rule, stated generally: any stage that mutates shared state must undo by SNAPSHOT-RESTORE over the complete set of files it can touch — config AND source — and a stage whose undo scope is narrower than its write scope will silently destroy work that no byte-gate can see. §61's law, one level deeper.