Commit Graph

3729 Commits

Author SHA1 Message Date
Drew T cdd535e45b fix(tools): reconcile_tu's cc1 premise, the &-cast arms, and the worktree sig gap
The S74 checkpoint's "one unfixed defect that is actively costing banks"
(reconcile_tu manufacturing declaration conflicts), run to ground — plus the
harness gap that produced a false carve-corruption verdict.

reconcile_tu.py — three defects, measured against the real gcc-2.7.2 front end
(cdecl._cc1_accepts, the oracle cdecl.compatible was validated with; R33):
  * The premise "a decl BELOW still conflicts" is TRUE at file scope and FALSE
    at block scope. cc1 ACCEPTS a block-scope extern against a TU decl below it
    (pedwarn "type mismatch with previous external decl"); conforming it is
    destructive, because the TU's decl names the TU's TYPE and a type declared
    below the splice point is not in scope AT it -- the emitted result gets
    "syntax error before 'D_x'". Byte-witnessed on resident:func_800D06E8 (344
    ins), whose block-scoped `extern Blk80078E78` became `extern
    Struct80078E78`, typedef 388 lines lower. That construct is what this
    ladder's OWN scope_demote_drafts (§8d) rung emits on purpose, and three
    already-banked functions in that TU use it: one rung undoing another.
  * The cast pass rewrote COMMENT PROSE -- 8 rewrites inside one header comment,
    including inside a quoted cc1 diagnostic. Now matches on cdecl._mask
    (length-preserving, so a mask offset is a source offset) and splices into
    the original.
  * `&sym` emitted `&` applied to a cast: legal for the scalar arm, `invalid
    lvalue in unary '&'` (measured) for the array/fnptr/fnptr_array arms. `&`
    now selects a pointer form and consumes itself -- but ONLY with no trailing
    subscript, because `&sym[i]` is the address of ELEMENT i and the old code
    had that case right. That last clause exists because the R39 negative
    control caught the fold as a regression in the first cut of this fix.

gate_stage.py — `--skip-stages` / `GATE_SKIP_STAGES` (loud when used). Stage 0
gates raw drafts first, so a broken rung can only cost a RECOVERY, which is
exactly what makes it invisible: the function it destroys was already failing,
so its DIFF reads as a fact about the function.

verify_worktree.py / jr_isolate_all.py / parallel_gate.py — provision() now
symlinks every .run/sig.*.jsonl (main clone 259, provisioned worktree 0), the
third member of the class holding extracted/ and .run/obj40. parallel_gate was
fixed for this identical bug in S69: two provisioners, no shared list, found
twice; they now cross-reference each other. jr_isolate_all no longer swallows
the resulting FileNotFoundError into `except: continue` -- that turned a missing
index into a confident carve-CORRUPTION verdict over 2,603 of 2,603 functions
(R54). Adds _assert_scan_covered: attempted == raised means the scan measured
nothing, so its zero is an artifact, not a finding (R32).

Verification:
  * 4 cc1 probes (the table above), each run on the pinned front end.
  * R39 negative control over the stored-draft corpus: 661 adjudicated, 652
    IDENTICAL, 9 CHANGED and every one an intended class. 4,173 of 4,864 drafts
    unadjudicable (filenames that are not func_<ADDR>) -- stated, not hidden.
  * jr_isolate_all ov_SC03_105 --dry-run: unchanged in the main tree.
  * make clean/extract/build BINARY=resident -> 8e17e02f... BYTE-IDENTICAL.

Docs ship with the change (R21): cookbook §442/§443, index regenerated (1,112
sections), 3 docs/SETUP.md rows, CURRENT_PHASE S75 log.
2026-09-02 20:30:22 -06:00
Drew T 7f7e1c7d90 docs(phase-31): refresh the S74 checkpoint — §440/§441 and the doc-state line
The checkpoint listed §435-§439 and was written before the documentation catch-up; a stale
checkpoint is worse than an absent one. Now records that every tool change this session carries a
SETUP row and a cookbook entry, and names the rule the gap taught.
2026-09-02 19:54:10 -06:00
Drew T eb0329b3a4 docs: the five agent-authored tool changes that shipped with a commit message and no docs
I answered Drew's yes/no honestly — NO — and this closes it. Every gap had the same shape: a tool
change that came from a SUBAGENT arrived as a report, I merged the code and wrote it up in the
commit message, and a commit message is not the knowledge base. The six changes I made myself were
documented inline; these five were not.

SETUP.md tooling ledger:
  * `ld_interleave` — the row still said "interleave linker inputs" and predated BOTH --order
    (S72, main's 7-piece island) and --pre (S74, the resident's leading-rodata header).
  * `harvest_verify` — the typedef strip-set is computed SCOPED (`above=fn`) now, and why.
  * `jtbl_rodata_pads` — a new row for the three S74 measurement corrections, each of which ACCUSES
    THE CARVE when it fires, plus why the trailing-.align one stayed latent (zero_gap self-corrects
    an undershoot when the next item is an anchor, and a C jump table has no anchor).
  * NEW row `jtbl_carve` — the `covered` / `covered-tpad` verdicts.
  * NEW row `jr_isolate_all` — `_region_emit_start` and the empty-closing-region skip.

Cookbook:
  * §440 — a carve piece binds to a SUBSEG, not a function, so §8b's "non-adjacent => ISOLATE" is
    over-strict: EXTEND the carve across still-stubbed material instead. Four byte-proven
    corollaries (migrated tables self-align by SPAN-RELATIVE offset; JTBL_PADS counts cc1 tables
    only so a mixed span's spec grows as siblings bank; the zero-word rule is invalid across a
    migrated boundary; a covered table at 4-mod-8 gains 4 bytes when it banks). Plus the resident's
    rodata->text->data->rodata->data layout and why it needed --pre.
  * §441 — three more instrument defects that each produced a confident, precise, WRONG verdict
    about a correct draft, with the habit they share: when a gate rejects a body you have
    byte-verified standalone, the first suspect is the gate.

Playbook: new step 2a-0 — the same-address lead is size-filtered now; read the `⚠ IGNORE` line, and
regenerate any pack built before S74 rather than trusting a bare address lead.
2026-09-02 19:53:56 -06:00
Drew T b24394e732 docs(phase-31): S74 FINAL checkpoint — 50 banked, seven tool defects, R22 213/213
Every reject class this session was an instrument defect, not a codegen wall: 24 already-MATCHed
bodies were sitting behind seven tool bugs, six of which are now fixed. The checkpoint names the
seventh (reconcile_tu manufacturing declaration conflicts) as the first thing to fix next session,
and the harness gap (verify_worktree omits the sig files, and the scan that needs them swallows the
error and reports a false corruption) beside it.

Fleet verified from a clean rebuild AFTER the last bank: check-all 213 passed, 0 failed of 213.
split_indicator 213 OK and now a hard gate. INCLUDE_ASM lines in src/ 1,086 -> 1,036, measured at
both commits — the same 50 the gates reported, counted independently from the source.
2026-09-02 19:40:46 -06:00
Drew T 8edb918480 feat(cards): size-filter the same-address lead (§238 homonym) + bank the S74 lever set
THE CARD USED TO HAND AGENTS A WRONG TWIN ABOUT ONCE IN FIVE. `⭐ func X IS BANKED AT THIS ADDRESS`
never checked that the two functions were the same SIZE, and overlays share addresses between
unrelated functions as readily as they share code. Measured over this session's ~60 cards: about a
dozen agents reported discarding the lead themselves, and one card advertised a 72-instruction
namesake — with journal history claiming "already MATCH closeness 0" — to a 241-instruction target.
A confidently wrong lead costs more than no lead, because the agent believes it.

corpus.sig already carries `nins` and `h_seq`, so the fix is free: `_same_addr_banked` now returns
(binary, nins, h_seq); the card keeps a lead only at a MATCHING instruction count, marks it strong
when the mnemonic skeleton matches too, and prints an explicit `⚠ IGNORE` naming the binaries where
that address holds something else, with both sizes.

VERIFIED IN BOTH DIRECTIONS against known-true cases before being believed (never trust a filter you
have not tried to fool):
  * the trap: ov_SC03_105:func_801806F8 (241) vs ov_SC03_013 (72) -> `⚠ IGNORE`.
  * the positive: ov_SC02_003:func_80187B40 (158) -> strong lead to ov_SC02_000 (158, same h_seq,
    banked this session) AND, in the same card, warned off ov_SC04_011's 138-ins homonym at that
    same address. That is precisely the pair a wave agent sorted out by hand hours earlier.

Cookbook §438 (the law: a lead is fuel only if it carries the cheapest fact that can refute it —
size refutes a homonym for free and nobody had asked) and §439, the S74 lever set: MEM_IN_STRUCT_P
as a two-way alias-oracle dial (four agents converged on it independently); `goto`-into-a-shared-tail
vs longhand as a REGALLOC dial because gcc-2.7.2 cross-jumps after allocation; `for` -> do/while as a
length-changing scheduling dial; allocno PRIORITY via a non-volatile asm at a loop head, with the
measurement that register pins are actively harmful for that class; the -O0 global-RMW rule
(`x++` emits the copy-back quartet, `x = x+1` does not); why `sll 16; srl 16` survives only across a
CALL; `sltiu N` without `addiu -1` proving an empty `case 0` is mandatory; block-scoped temps in
duplicated bodies; two `register asm` vars cannot share a hard reg; and `x*32` vs `x<<5` emitting
lh vs lhu — which match_one's %lo mask HIDES, so it must be checked with objdump.
2026-09-02 19:38:56 -06:00
Drew T 5208f2279d feat(decomp): parallel gate — 2 fns across 1 binaries (1 workers)
ov_SC06_029    func_80182ED8 func_80184084
2026-09-02 19:35:21 -06:00
Drew T 6e840730a9 feat(carve): bank 4 more via the carve chain — and §8b's "non-adjacent => ISOLATE" is over-strict
resident:func_800D00E4/func_800D02D0/func_800D0488 + ov_SC07_002:func_80180248, all byte-verified
from clean rebuilds (resident 8e17e02f, ov_SC07_002 fad71342) and counted from the SOURCE.
ov_SC06_029's two are re-gated separately against HEAD — this agent's worktree predated five banks
there, so its numbers for that binary no longer apply.

TWO OF THE SIX NEEDED NO CARVE WORK AT ALL, AND CARVE-REFUSED WAS AN INSTRUMENT VERDICT.
ov_SC07_002:func_80180248's table is ALREADY inside a carve bound to its own subseg: in stub state
spimdisasm migrates the table into the fn's .s and the object fills the piece exactly, so banking
just swaps that block for cc1's identical one. `island_probe` classified it `tail` on the table's
ADDRESS, `apply()` routed it to build_carve, which resolves spans out of the RAW data asm where a
carved table no longer is -> "not found in the raw data asm" -> harvest_verify booked CARVE-REFUSED.
A verdict about the route we chose, not about the function (R43). jtbl_carve now has a `covered`
verdict (table inside an existing carve bound to the fn's OWN subseg) and a `covered-tpad` wall (the
retail copy carries a trailing §8a pad the matched body won't emit — bankable, needs a `0t<n>`
entry); a fully-covered batch is a no-op before either route.

THE RESIDENT CAN CARVE LIKE AN OVERLAY. Its three tables are adjacent and lead the island
(0x450e0..0x451ac, one span, all in subseg `resident`). The genuinely new part: the resident opens
with `- [0x0, rodata, hdr]`, a 1-word .rodata header BEFORE the code, so its layout is
rodata -> text -> data -> rodata(carve) -> data, which `ld_interleave --order` cannot express (every
listed piece lands after TEXT_START, and hdr.rodata.o would fall into the unchecked `empties` bucket
and be parked after the text, moving every byte). New `--pre` places a leading-rodata piece ahead of
the text; resident_JTBL_INTERLEAVE uses it.

NEW LAW, BYTE-PROVEN (§8b was over-strict — EXTEND the carve, do not isolate): a .rodata carve piece
binds to a code SUBSEG, not a function, and the object's .rodata is the address-ordered
concatenation of cc1's tables for BANKED functions and still-stubbed functions' MIGRATED tables. So
a span may legitimately hold a MIX, and extending a carve across an align-pad word and two unrelated
STILL-STUBBED tables was byte-identical with nothing banked — where the tooling demanded a
jr-isolation. Corollaries, all measured: migrated tables self-align (spimdisasm emits `.align 3` iff
the table's SPAN-RELATIVE offset is 8-aligned), so stubbed tables need no spec; JTBL_PADS counts cc1
tables only, so a mixed span's spec GROWS as each sibling banks; and the zero-word rule is INVALID
across a migrated boundary, because that zero is supplied by the preceding migrated block.

ALSO REPORTED, NOT FIXED (harness gap worth its own change): verify_worktree.provision omits
`.run/sig.<bin>.jsonl` — main clone 259 files, provisioned worktree 0 — and jr_isolate_all's
carve-ownership scan swallows the resulting FileNotFoundError in a bare `except: continue`. Measured:
2603 of 2603 functions raised, the scan found 0 owners, and the run aborted with a CONFIDENT FALSE
verdict ("committed .rodata carve ownership is not 1:1 — stranded/duplicated carve"). Both resolve
instantly once the sigs are present. Any worktree-run isolation before that is fixed reports a
corruption that is not there.
2026-09-02 19:34:09 -06:00
Drew T 089311e74d feat(md_SC07_004): 10/10 banked — md_SC07_004 is now ZERO stubs, and the "decl conflicts" were fake
Clean rebuild BYTE-IDENTICAL 87ac0de3; corpus.stubs('md_SC07_004') 10 -> 0, counted from the SOURCE.

THE §376/§378 CHAIN WAS NEVER NEEDED. Zero declaration edits: no fix_arity_callers, no
cast_self_callers, no --any-proto, no --sync-decls, no undo journal. `git diff -U0` on the TU removes
exactly the 10 INCLUDE_ASM stubs plus one hoisted typedef. Every recorded "declaration conflict" was
an INSTRUMENT defect. Four of them, all named, three patched here:

1. `CC1-FAIL(no-diagnostic)` was neither cc1 nor no-diagnostic. The failing stage was
   `jtbl_rodata_pads --derive`, which prints to stderr AFTER cc1 exits 0 quietly. `_items` matched a
   rodata anchor only as `D_xxxxxxxx:`, but a block written as inline `__asm__` in C arrives in the
   labels.inc macro form `dlabel D_xxxxxxxx` — so an 8-byte hole opened in the walk and every C jump
   table after it died with "island layout drift". The harness label was wrong twice: it said CC1
   when the failure was a post-maspsx filter, and no-diagnostic when there was a precise one.
2. Same file, UNALIGNED ANCHOR: the ctable branch read `word(pos)` without first stepping the
   sub-word zero gap, so a preceding `.asciz` ending at an odd address made it refuse a correct
   layout. Now reuses the same zero_gap the anchor branches already use — a no-op wherever pos is
   already aligned, i.e. everywhere that builds green today.
3. `harvest_verify` computed the typedef strip-set UNSCOPED: `cdecl.typedef_names(path)` without
   `above=fn`, which that function supports for exactly this. A typedef declared BELOW the splice
   point got stripped out of the draft that needed it -> `parse error`, logged as PLUMBING and
   indistinguishable from a real conflict. One line.
4. NOT PATCHED, AND THE MOST IMPORTANT ONE: `reconcile_tu.py` (gate_stage's `-rc` stage) MANUFACTURED
   both remaining "conflicts". The same drafts gate 9/9 byte-identical through harvest_verify and
   7/9 through gate_stage. Isolated stage by stage, `-rc` (a) rewrites deliberately BLOCK-SCOPED
   externs to a file-scope spelling whose typedef is declared ~2,800 lines lower — overwriting the
   TU's own byte-proven house style, which three already-banked functions in that file use; and
   (b) substitutes identifiers TEXTUALLY, including inside comments and inside `&`-expressions,
   emitting `*(T *)&((s32 *)&D_800AE620)`. A correct draft using the block-scope-extern idiom
   currently CANNOT survive gate_stage. Left for a deliberate fix: `--stages` should be able to skip
   reconcile_tu, or a draft should be able to opt out.

The two surviving non-stub source edits are byte-neutral (proven by the SHA above): a §304
migrated-rodata re-emission (`D_801A01EC`, the exact form this TU already uses three times, needed
because banking the body deletes the .s that carried the island word), and one typedef moved up so a
function above it can see it (typedefs emit no bytes).

Also banked the 10th stub (func_801ADA10) that defect 3 had been silently blocking.

Regression-checked by the agent: main, md_MAIN_003, md_SC07_003, md_SC03_073, md_MAIN_011 all
rebuild BYTE-IDENTICAL. A full R22 follows before this session closes.
2026-09-02 19:26:16 -06:00
Drew T 2954b250ec feat(decomp): parallel gate — 5 fns across 1 binaries (1 workers)
ov_SC06_029    func_801801D8 func_80180A70 func_801867D0 func_80187660 func_801898CC
2026-09-02 19:25:24 -06:00
Drew T 5e10215269 feat(md): bank the 4 -O0-stranded functions — and the class is now essentially empty
md_MAIN_011:func_800CF28C · md_MAIN_003:func_800D0268/func_800D0740/func_800D0C50, all byte-verified
from a clean rebuild and counted from the SOURCE: md_MAIN_011 is now FULLY MATCHED (0 open stubs),
md_MAIN_003 is down to 1 (func_800CF3E8).

THE PREMISE I HANDED THE AGENT WAS WRONG, AND IT SAID SO. md_MAIN_011 is already a whole-object -O0
module — no carve was needed. Its real blocker was tools/jtbl_rodata_pads._s_rodata_span ignoring a
trailing `.align`, the SAME defect this session fixed for md_SC07_003 from the other direction: two
agents converged on it independently. Adopted this agent's stricter form (only a TRULY trailing
align rounds `hi`; an interior one is followed by data that sets `hi` higher anyway).

Note WHY it stayed latent: `derive`'s zero_gap self-corrects a 1-3 byte undershoot whenever the next
stream item is an anchor. A C jump table has NO anchor — so the bug can only fire the moment someone
banks a switch function into such an object, and when it fires it accuses the CARVE ("island layout
drift"), not itself.

md_MAIN_003 needed one new -O0 object, and the boundary I proposed (0x1f74 -> 0x1e58) was both too
narrow and off by 0x2B8. The carve made is `md_MAIN_003_o0e` at 0x1308 (vram 0x800D0100) running to
the existing o0c boundary: everything in that span is a §265 verbatim __asm__ body or an INCLUDE_ASM
stub — zero optimizable C — so the whole tail flips with one cut. Proved byte-identical with NOTHING
banked first (§431 discipline), then the three drafts gated one at a time.

TWO MORE GENERAL DEFECTS FIXED IN jr_isolate_all, both of which silently mis-place a boundary:
 * an item-less CLOSING region emitted a duplicate `- [off, c, …]` line and the validator refused;
   the empty-region skip covered only region 0, and `_partition`'s empty `footer` made the closing
   region look non-empty.
 * A §265 VERBATIM __asm__ BODY IS PREAMBLE, AND PREAMBLE IS ASSUMED BYTE-NEUTRAL. It is not — it
   emits bytes. `parse_overlay_c` has four addressed-anchor forms and a verbatim body is none of
   them, so it attaches to the NEXT anchor: cutting at func_800D0268 would have moved 0x168 bytes of
   other functions into the new object while the yaml claimed the region starts higher. New
   `_region_emit_start()` derives the yaml offset from the region's CONTENT (item addresses + every
   .globl/.ent the text names that resolves inside the object) and takes min(cut, emit), so a
   boundary can only move DOWN. Where no verbatim asm is in play it equals the cut — every existing
   isolate is unchanged.

BLAST RADIUS PROVEN, not argued: jtbl_rodata_pads is in the build path (`--derive` for md_*/main),
so the agent rebuilt main + all 70 md_* from scratch (71/71) and then ran the full fleet:
**make check-all 213/213 passed, 0 failed**, main 143dbb89 BYTE-IDENTICAL.

CENSUS, denominator asserted (1057 live stubs, 0 without a .s): exactly ONE -O0-prologue stub
remains stranded in an -O2 TU fleet-wide — main:func_8002C410 in src/800_b.c, 299 ins. Nothing more
should be built for this class; the general tool already existed and what was missing was
correctness, not coverage.
2026-09-02 19:23:25 -06:00
Drew T 3a886b9652 fix(tu-split): a block comment a construct OPENS MID-LINE and WRAPS defeated every peeler
FIVE independently-MATCHed ov_SC06_029 bodies were rejected by a `parse error before '#'` in a file
the GATE ITSELF generates, at a line no draft contains. The isolation emitted, into the §8b carried
decl layer:

    extern #define CALL_80185C6C ((void *(*)(s32, s32))func_80185C6C) extern void func_8012C218();

CAUSE. Every peeler in the TU-split chain asked `line.strip().startswith("/*")`, which is blind to a
comment a construct opens MID-LINE and wraps. The declaration ends at its `;` BEFORE the `/*`, so
the caller resumed on the comment's PROSE with in_block=False — and the prose is hostile: `(s32,s32)`
closes a depth-0 paren, `seen_header` latches, and every later `;` reads as a K&R parameter
declaration, so one "construct" swallowed the whole preamble. `parse_overlay_c` then anchored a
`def` on a pure declaration run and `def_proto` rendered it as that definition's implied prototype.

A SECOND defect rode along: `_file_scope_decls` hoisted such a col-0 line VERBATIM, unterminated
`/*` included, so the carried layer opened a comment that silently ate the next two declarations —
a dropped file-scope decl is a silent byte-changer. Building the guard exposed a THIRD: `_strip`
tested for `/*` before stripping `//`, so `// … src/*/*.c` (7 lines in 5 sources) opened a phantom
block comment and blanked everything to the next `*/`.

FIX: one derived comment-state oracle, `comment_open_at()` (R33) — per line, does it BEGIN inside a
block comment — consulted by parse_overlay_c, def_proto, split_src_region.parse and
jr_isolate_all._file_scope_decls (which also truncates a hoisted decl at an unterminated `/*`).
`_strip` now lexes left to right. `parse_overlay_c` RAISES (R43) when a wrapped comment closes with
code after the `*/`, because that construct could never anchor — 0 occurrences fleet-wide.

MEASURED, not assumed:
  * the shape occurs 238 times across 193 tracked .c files; 153 are col-0 hoistable declarations in
    150 files — every one a binary whose next isolation would have carried a broken decl layer.
  * A/B over all 4,188 tracked sources, old parser vs new: round-trip identity 4188/4188 both ways;
    exactly 2 files' item lists change, each losing one PHANTOM def and gaining nothing; malformed
    implied prototypes 999 -> 984; 0 refusals.
  * negative control BEFORE any edit: ov_SC06_029 extract+build -j+check BYTE-IDENTICAL b7b0d4ae.
  * with the fix, gate_stage banked 5 of 6 drafts, counted from the SOURCE; the 6th
    (func_80184084) is the separate CARVE-REFUSED class.

The 984 residual malformed prototypes are a DIFFERENT pre-existing trigger (col-0 lines gluing
declarations to DEFINE_func_*() invocations); 4 still carry a `#` and survive only because it lands
in a dropped segment. Named in §437, deliberately not fixed here.

Cookbook §437 + a SETUP.md tooling-ledger row for comment_open_at (parse_overlay_c may now raise).

The banks themselves are NOT in this commit: the agent's worktree predated func_8017F9C0's bank, so
adopting its TU verbatim would have destroyed one. They get re-gated against HEAD with these tools.
2026-09-02 19:22:16 -06:00
Drew T e9220d2d8a fix(pgate): a carve left asm/ stale, and the refusal that reported it named nothing
TWO DEFECTS, ONE INCIDENT. ov_SC03_105's own SUCCESSFUL gate committed an isolation's new TUs
(src/ov_SC03_105/ov_SC03_105_jr_801813BC.c, _jr_80181C84.c) whose `INCLUDE_ASM` lines name .s files
that do not exist until a re-extract. corpus.stubs then refused — correctly, "the tree and the
source disagree" — so the NEXT gate on that binary died before doing any work, and a matched body
(func_801818E8) sat unbankable behind it.

1. THE REASON NOW TRAVELS WITH THE REFUSAL. stubs_of() returned a bare None and the caller printed
   "corpus refused in worktree": true, and naming nothing. It took a hand-built worktree to see that
   corpus had said exactly what was wrong all along. It now returns the message and the result JSON
   carries it. Verified against a TRUE reproduction (delete one .s in a scratch worktree):
   verdict REFUSED + "1 stub(s) have NO .s on disk ... src/...:4214: asm/.../func_8017F018.s".

2. THE MERGE STEP REPAIRS WHAT IT BROKE. For every binary whose carve created a new source file,
   assert corpus.stubs is satisfiable in the MAIN tree; if not, `make extract BINARY=<b>` and
   re-assert; if it is STILL unreadable, say so loudly rather than leaving a tree no tool can read
   (R32/R43). This is the R22 corollary — a config change needs a make extract, not just a make
   check — firing inside a tool's own commit.

Repaired the live instance by hand first: rm -rf asm/ov_SC03_105 + extract + build -j + check ->
BYTE-IDENTICAL d305ff6d, corpus readable again, and func_801818E8 then banked (commit:3718).

Cookbook §436-D; wave-playbook §6 carries the hand-gating version of the same warning.
2026-09-02 19:10:57 -06:00
Drew T d0389352d7 feat(decomp): parallel gate — 1 fns across 1 binaries (1 workers)
ov_SC03_105    func_801818E8
2026-09-02 19:09:29 -06:00
Drew T 0109f5bc92 feat(decomp): parallel gate — 1 fns across 1 binaries (2 workers)
md_SC07_003    func_801A09C8
2026-09-02 19:05:04 -06:00
Drew T ee2963514a fix(tools): jtbl_rodata_pads measured a .s rodata span without its trailing .align
`_s_rodata_span` summed only the DATA an included `.s` emits, so a file ending `.asciz "r"` +
`.align 2` measured 0x801A00D8..DA instead of ..DC. The island walk then landed 2 bytes short and
`--derive` aborted with `C table entry 0 at 0x801A00DA ... island layout drift` — a true statement
about a span that was never the real one. Worse, the Makefile pipes md_*/main through `--derive`
without `set -o pipefail`, so the failure could yield a short object rather than stopping the build.

Three lines: round `hi` up to the trailing align, which is what the assembler actually emits.

CONTROLS (both on UNMODIFIED sources, so this is proven byte-neutral, not argued):
  md_SC07_003  clean rm -rf + extract + build -j + check  -> BYTE-IDENTICAL 46af79a1
  gate_main --assert-baseline                              -> BASELINE GREEN 143dbb89

Found by a drafting agent (md_SC07_003/func_801A09C8) that ran its own gate reject to ground
instead of respelling its body, and proved the patch in scratch first: with the fix its draft's
.rodata is byte-identical to the green control and .text differs in exactly 1 of 6266 words — a
%lo(jtbl) carrying a section-symbol reloc that three already-green C-jtbl functions in the same
object already ship. Cookbook §436-C, with the habit that found it.
2026-09-02 19:04:41 -06:00
Drew T 7ce27c0d1f feat(decomp): parallel gate — 2 fns across 2 binaries (6 workers)
ov_SC05_010    func_8017F5B8
  ov_SC03_024    func_801830A8
2026-09-02 18:57:20 -06:00
Drew T 7c2342f87f feat(decomp): parallel gate — 5 fns across 3 binaries (4 workers)
ov_SC02_011    func_80183178
  ov_SC01_084    func_80182328
  ov_SC03_105    func_801813BC func_80181C84 func_8018624C
2026-09-02 18:49:40 -06:00
Drew T 797cb97cb7 feat(decomp): parallel gate — 9 fns across 6 binaries (8 workers)
ov_SC06_024    func_8017EC4C
  ov_SC06_029    func_8017F9C0
  ov_SC03_105    func_801867D0
  ov_SC05_010    func_80180F84
  ov_SC02_005    func_80185060 func_80185E80
  ov_SC02_011    func_80183630 func_8018418C func_80188E3C
2026-09-02 18:38:30 -06:00
Drew T f2f3103691 feat(decomp): parallel gate — 7 fns across 7 binaries (10 workers)
ov_SC05_001    func_8017FE0C
  md_SC07_003    func_801A1120
  ov_SC03_013    func_8017FAA8
  ov_SC03_011    func_80180B68
  ov_SC02_000    func_80187B40
  md_SC07_004    func_801A94A0
  ov_SC07_000    func_8017ECB4
2026-09-02 18:34:03 -06:00
Drew T 95c7b7fe0f fix(tools): two tools read a source of truth describing a different world (+ hard-gate the third)
Three independent split agents hit both defects in one session, on the tools that CERTIFY and UNDO
the work they were doing. Each is fixed, negative-controlled against the exact failing case, wired
into its siblings, and documented in the same change (cookbook §436).

1. split_indicator attributed a jump table by the STUB'S DIRECTORY PATH. `make extract` does not
   prune a re-homed subseg's `nonmatchings/<old>/` dir, so after a correct, byte-green §431 split
   both the old and new dirs hold the moved stub — and the tool printed NEEDS SPLIT for a split that
   was already correct. owners() now derives the owner from the CONFIG by address (R33), exactly as
   jtbl_carve.func_subseg already does for the identical §8b hazard, and NAMES any leftover stub in
   a `note:` line. Notes now print on an OK verdict too: hiding one behind `st != OK` is the same
   defect in the other direction — a true verdict about a narrower world than the reader believes.
   PROVEN by planting a stale stub for func_80182A00 under its old subseg: OK + the note, where the
   old code would have seen one subseg owning two spans. --self-test still PASSes both directions.

2. jtbl_carve --revert did `git checkout --` on the WHOLE splat yaml. The carve owns only the
   trailing data/.rodata region; the `c` pieces are source configuration it never writes. The blunt
   form cannot tell "carve state I just added" from "the §431 split someone added to the same
   uncommitted file", so --revert after a carve PROBE silently un-split the overlay — each agent
   recovered only because they had backed the yaml up by hand. It now splices back only its own
   region (parse_config gained an optional `lines=` so the SAME region derivation runs over the
   committed text — one derivation, two callers), refuses loudly if the committed region carves onto
   a subseg the current config no longer defines, and reports how many uncommitted `c` pieces it
   preserved. PROVEN in the ov_SC01_084 worktree: carve → revert → the uncommitted split survived
   ("PRESERVED 30 uncommitted `c` piece(s)"), carve lines gone, diff back to the 6 split lines.

   SIBLING: jtbl_family_bank.revert carried the same blunt checkout for the isolation's code pieces.
   It now keeps whatever pre-dated the attempt (the `keep_regions` signal it already trusts for
   src/) and NAMES anything it drops — an isolation region and a §431 split piece are both
   `<ov>_jr_<addr>`, so no name test can tell them apart and only that signal can.

3. NOT A DEFECT, and recorded as such: a speculative carve fails the build with `jtbl_rodata_pads:
   consumed 3 rodata jump table(s) but 9 pad spec(s) given`. That is R43 working — the pad spec is a
   CONSEQUENCE of banking, not a prediction of it — and it reproduces identically on the pristine
   unsplit config, so it is never evidence about a split.

make tools-health: split_indicator is a HARD GATE now, as its own comment promised it would become
once the last violation was split. 213 OK of 213; a new one fails the build instead of being echoed
past.

Cookbook §435 (an overlay TU split is near-free — 0/3,074, 1/2,679, 2/3,254 names crossed, because
the §8b carried decl layer re-emits externs per region so only typedefs can cross; and the gap test
between two rodata runs is "is this word a valid code address", not "is it zero") + §436 (the two
defects and the shape they share). Playbook + SETUP.md carry the emptied CARVE-BLOCKED class.
2026-09-02 18:11:48 -06:00
Drew T c7d67048a0 chore(waves): regenerate config/wave_exclude.txt — the CARVE-BLOCKED class is gone
16 of 27 entries dropped as STALE, all of them the CARVE-BLOCKED set the four §431 splits
in commit:3709 just unblocked. 11 WALL entries kept (curated, pinned by their `# WALL:`
annotation, which exclude_audit re-reads as a pin so a wall survives regeneration).

An exclude list records what the TOOLING could not do, so it becomes a list of work you
decided not to do the moment the tooling improves — regenerating it is part of the fix,
not follow-up hygiene. draw_waves --exclude-file audits and REFUSES a stale list.
2026-09-02 18:00:43 -06:00
Drew T 33c292dea5 feat(overlays): split the 4 carve-blocked subsegs at their jtbl-span TU boundaries
BYTE-IDENTICAL on all four, with NOTHING banked (clean rm -rf asm/<bin> + extract +
build -j + check), which is the whole point: the structure lands first and proves neutral,
then drafts bank against it. split_indicator: 213 OK, 0 needing attention, of 213 —
the CARVE-BLOCKED class is now EMPTY fleet-wide.

One code object contributes exactly ONE contiguous .rodata run, so a subseg owning raw
jump tables in two non-adjacent spans could carve only one of them and every switch
function in the other span was unbankable at any effort (cookbook §426/§431).

  ov_SC01_084  2 pieces  cut 0x80182A00 (0x5A8A8)
  ov_SC02_005  3 PIECES  cuts 0x80185060 (0x5CF08) + 0x80185E80 (0x5DD28)
  ov_SC02_011  3 PIECES  cuts 0x80183178 (0x5B020) + 0x80188E3C (0x60CE4)
  ov_SC03_105  2 pieces  cut 0x8018624C (0x5E0F4)

TWO OF THE FOUR NEEDED A CUT THE BRIEF DID NOT NAME, and the address evidence found it:
each already had a carve run that could not merge with span 1, separated by rodata that
is not padding — ov_SC02_005 by `0000F040 00000000` (8 bytes, twice the widest .align 3
pad the JTBL_PADS spec can emit), ov_SC02_011 by `FEBEF6AE 000002DC 0 0` (a TU's trailing
const data). A gap detector keyed on zero words would have merged them and produced an
unbuildable carve: the load-bearing test is "is this word a valid code address in this
overlay's text range", not "is it zero". ov_SC01_084's divider is real data too
(`0 FFFF0000 00080000 0 0`), while ITS span-1 gap word IS a zero .align 3 pad and merges.

OVERLAY SPLITS ARE NEAR-FREE, AND THE REASON IS STRUCTURAL — the opposite of main.
The Phase-26 §8b carried decl layer re-emits each region's externs locally, so only
typedefs cross a cut: 1 name of 2,679 (ov_SC01_084, 0 typedefs) · 5 typedefs of 44
(ov_SC02_005) · 2 names of 3,254, 0 typedefs (ov_SC02_011) · 0 of 3,074 with zero
compiler errors (ov_SC03_105). main's split moved 57 of 1,247. Every crossing typedef was
MOVED to a <bin>_shared.h, never copied, and every list came from the compiler (R33).

Carve probes (jtbl_carve --func, then reverted — carve state is added when a function
banks, never speculatively): all four subsegs now accept a carve with no fail-loud, and
jtbl_carve derived the §8e per-table pad specs the zero-word rule predicts.

Unlocks 17 open switch functions: ov_SC01_084 func_80182A00 · ov_SC02_005 func_80185060,
func_80185E80 · ov_SC02_011 func_80183178, func_80183630, func_8018418C, func_80188E3C ·
ov_SC03_105 func_801806F8, func_80180ABC, func_80180EC0, func_801813BC, func_801818E8,
func_80181C84, func_8018624C, func_801867D0 (+2 more span-1 owners).

CORRECTION, measured not assumed: config/wave_exclude.txt listed ov_SC01_084:func_80182328
as CARVE-BLOCKED and it never was — its table ABUTS the existing carve, so it always
merged into one run. Proven by control on the PRISTINE unsplit config: --func func_80182A00
exits 1 "would host NON-CONTIGUOUS .rodata carves", --func func_80182328 succeeds.
2026-09-02 18:00:28 -06:00
Drew T aa302aa050 docs(phase-31): S73 FINAL checkpoint — consolidated, verified, fresh-session ready
Supersedes S73 CLOSE and its addendum. Every number re-verified against the repo:
REAL 880/1,918, MAIN 56.5%, frontier 124 (main 36), main jtbl fns 25 -> 2.
2026-09-02 17:25:57 -06:00
Drew T a0ff94626a docs(phase-31): all 40 actively-misleading audit findings worked, not deferred
Replaces the 'known-remaining' TODO list with what was actually fixed. The four that
were not doc typos: progress.py undercounting REAL by 7 (the #else half of a
NON_MATCHING block is live code and classify() swallowed it), the silently deleted §429,
the false §265 accusation in §434, and memory-map.md:309 claiming a 'verified' extent
that overlaps the new span-B carve.
2026-09-02 17:17:44 -06:00
Drew T 78ef96f606 docs: cookbook §426/§434, memory-map extent, Makefile overlay comment
* §426 listed three localizer verdicts; there are four, and the missing TABLE REJECT is
  the dominant residual on main's switch functions (§433). Its span-B table also still
  advertised SaveLoadRoutine as an unlockable owner — it is the §434 frame pair.
* §434 quoted SaveLoadRoutine at 1139 instructions; the .s has 1165.
* docs/memory-map.md:309 recorded saveHeaderTemplate @0x80072DF0 with 'handler code ptrs
  @+0x54' at the ledger's HIGHEST confidence. 0x80072DF0+0x54 = 0x80072E44, which is
  jtbl_80072E44 — func_8002B0B4's dispatch table and the first 12 bytes of the S72 span-B
  carve. The row's extent is wrong past +0x54 and now says so; a 'verified' row that
  overlaps a carve boundary is how a future resegmentation gets talked out of itself.
* Makefile's overlay --front/--tail comment sat directly under main's --order call with
  nothing distinguishing them; now says which is which.
2026-09-02 17:17:07 -06:00
Drew T c43008a48b docs(playbook): seven stale spots — the copy-paste command was the worst
* Step-1's draw command still passed a superseded .run snapshot through the UN-AUDITED
  --exclude flag. Running it verbatim bypassed the freshness prerequisite built this
  session. Now --exclude-file config/wave_exclude.txt with a fresh --ledger.
* Both exclude populations were wrong: '96 jtbl functions build_carve refuses' is 16
  across 4 overlays (split_indicator derives it), and the seven .run/S6*_walls.txt
  ledgers are superseded by the WALL entries pinned in the canonical list.
* Entry count said 19; it is 26. Replaced with 'trust exclude_audit, never a number
  written here' — a count in prose goes stale the moment anything is added.
* Model routing still had a Sonnet band Drew abolished, and no mention that Fable is
  exhausted account-wide (three agents died on the limit in S73 at ~133k tokens each).
* Section 1c's census was pre-session: 25 of 59 main jtbl functions, 'every one now
  drawable', and SaveLoadRoutine as the flagship drawable example. It is 2 of 36, and
  those two are the §434 frame pair, excluded from draws.
* The gate step listed three gate_main verdicts; there are four, and the missing TABLE
  REJECT is the DOMINANT residual on main's switch functions (§433).
* Triage still named jr_isolate_all as the usual CARVE unblock; it does not yet produce
  an assemblable object, and §431 is the cheaper route.
2026-09-02 17:16:36 -06:00
Drew T 57e5f970c8 docs(tools): four docstrings describing pre-session behaviour
* draw_waves Usage advertised [--no-main], which argparse never defined (the flags are
  --main / --only-main, and main is excluded by default), and omitted --exclude-file,
  which is now a PREREQUISITE that refuses a stale list.
* jr_isolate's STATUS block still declared the tool BLOCKED on split_src_region with the
  blocker unbuilt. Five defects were fixed this session and it runs the full chain to
  completion; what remains is a duplicate-definition class at assembly. Says so, and
  points at §431 as the cheaper route than finishing the item model.
* ld_interleave's layout diagram — the first thing anyone reads — showed the pre-S72
  three-piece island with 6324C.data.o. main's island is SEVEN pieces driven by --order;
  --front/--tail is the overlay form now.
* jtbl_rodata_pads described a stored-spec-only filter and advertised guards that no
  longer all exist; --derive serves main since S72.
2026-09-02 17:15:28 -06:00
Drew T 9f8242d63c fix(progress): the #else half of a NON_MATCHING block is LIVE — REAL was undercounting by 7
classify() consumed everything from '#ifdef NON_MATCHING' through '#endif', swallowing
the #else half. But banking replaces the #else INCLUDE_ASM with the real body and leaves
the old attempt in the dead half — so every function banked that way landed in NO bucket:
not real, not a stub, invisible in both numerator and denominator.

Measured: CdReadStateMachine, CdReadSectorReadyCB and StreamLoadStateMachine are
byte-identical in the shipped build and counted as zero. REAL 873 -> 880, matchable
1911 -> 1918 (seven functions fleet-wide, not the three I first checked).

Now consumes only the DEAD half, then decides from the LIVE half: an INCLUDE_ASM there
still buckets as NON_MATCHING (accounting unchanged), anything else rewinds and is
classified normally.

THIRD coverage defect of this exact shape in this one function — the K&R-definition case
(~190k instructions erased) and the '#if 0' case are both documented in its own comments,
which is what pointed me at it. A scanner that walks preprocessor structure needs a test
per branch, not per directive.

Found by the S73 documentation audit, which I had written off as producing only doc typos.
2026-09-02 17:14:32 -06:00
Drew T 5ae43099e0 docs(phase-31): S73 CLOSE addendum — corrections, and the known-remaining list
Corrects three defects I introduced (deleted §429, a false accusation in §434, an
over-strong SaveLoadRoutine verdict), two wrong numbers in the block above, and records
the SETUP §6.6 gap that was the real answer to 'are the docs up to date'.

Also lists what the audit found and I did NOT fix, with file:line, so a fresh session
inherits the list instead of rediscovering it: jr_isolate/jtbl_rodata_pads/draw_waves
docstrings, several stale playbook census numbers and its step-1 command, memory-map:309,
and the pre-S72 --front/--tail descriptions in the Makefile and ld_interleave.
2026-09-02 17:06:52 -06:00
Drew T aaed146bb2 fix(docs): restore §429 which I deleted, and retract a false accusation in §434
TWO REAL DEFECTS I INTRODUCED, both found by the audit:

1. §429 WAS SILENTLY DELETED. My §428a rewrite (commit:3659) wrote t[:start]+new instead of
   t[:start]+new+t[end:], truncating everything below §428a. §429 ('every held pointer
   needs its own local') was the casualty and had been gone for the rest of the session.
   Restored verbatim from commit:3658, between §428a and §430. All of 426-434 now present;
   index 1103 sections.

2. §434 ACCUSED AN AGENT OF INVENTING ITS CITATION OF §265. §265 exists and says exactly
   what the agent said — 'THE VERBATIM-ASM BANK LANE: A FUNCTION NO -O2 C CAN EVER MATCH
   BANKS AS A RAW __asm__ BODY' — with four named byte-banked precedents. I ran
   cookbook_index --resolve 265, which resolves a LINE number not a section, and believed
   it without opening §265. Retracted in the section itself.

   The verdict also needed narrowing: gated, the §265 transcription of SaveLoadRoutine is
   BYTE-IDENTICAL for the function itself and fails only because substituting one half of
   the shared frame moves 3,989 bytes across 262 symbols. True statement: neither can bank
   SEPARATELY; the route is to transcribe/resegment the PAIR together via §265. The
   exclude entries now say 'excluded from DRAWS only' and name that route, instead of
   reading as 'unmatchable'.

I also mis-read the draft as containing INCLUDE_ASM by grepping raw text — all three hits
were in comments. Sixth instance this session of reading prose as code.
2026-09-02 17:05:44 -06:00
Drew T 0840c0991c docs(SETUP): fix four stale spots the tool-inventory update missed
I updated SETUP's tooling INVENTORY when each tool changed, but not the HUMAN-facing
procedure, and §6.6 is where a person learns the matching loop:

* :493 still said 'In src/800.c, replace the INCLUDE_ASM line with the C function body.'
  main's game code is THREE TUs since S72, and WHICH one is load-bearing for any switch
  function — one code object contributes exactly ONE contiguous .rodata run, so the TU
  decides which jump-table span the body's table lands in. Following that line for a
  span-B/C function re-creates the exact §426 double-emit this session existed to remove.
  Replaced with the vram -> TU -> asm-path -> span table.
* :759 listed main_diff_locate's verdicts as an exhaustive three — BODY / PLUMBING /
  MIXED. There are FOUR, and the missing TABLE REJECT is checked FIRST and covers
  precisely the case the PLUMBING clause claimed ('byte-identical, everything differs
  elsewhere'), routing the reader into the one chain the tool forbids for that class.
* config/wave_exclude.txt was named nowhere in SETUP despite being tracked config that
  draw_waves now requires. Added, with both entry classes and the WALL pin.
* :537 described ld_interleave as --front/--tail only; main uses --order since S72.
2026-09-02 17:00:16 -06:00
Drew T 891b240b5d chore: regenerated burndown 2026-09-02 16:45:33 -06:00
Drew T 2c1d21bb9d docs(phase-31): S73 CLOSE — R22 213/213, 23 main banked this session, main 56.5%, frontier 124
The jump-table class on main is resolved: 25 -> 2, and both survivors are the §434 frame
pair, provably unmatchable as separate C functions (resegmentation, not drafting).

Wave S73m_1 banked 9 of 9 drafts (2,413 ins). Cookbook entries written this morning
cracked functions this afternoon; two of mine were refuted by later MATCHes and rewritten.
2026-09-02 16:34:13 -06:00
Drew T f06d81a7d0 feat(main): StreamLoadStateMachine banked — all 9 wave drafts in; gate_main is preprocessor-aware
StreamLoadStateMachine (459 ins) byte-identical. That completes wave S73m_1: 9 of 9
drafts banked, 2,413 instructions.

gate_main defect this exposed, twice in one gate: resolve_conflicts scanned every 'extern'
line with no notion of the preprocessor, so a declaration parked in the DEAD half of an
'#ifdef NON_MATCHING / #else / #endif' pair read as a live constraint. It is never
compiled and constrains nothing. func_80018714 and func_800377D8 each carry a stale
'(void)' declaration in a dead branch while the LIVE definition takes a pointer and a u8
respectively; the first mis-blamed a draft, the second got a byte-verified draft DROPPED.
live_text() now blanks those branches before the scan.

The detour is instructive: I 'fixed' the draft twice to satisfy a constraint that did not
exist, and each fix made it worse — the draft's original (u8) declaration was correct all
along, because it matched the LIVE definition. Read which branch a declaration lives in
before believing it.
2026-09-02 16:28:07 -06:00
Drew T e1631eaacb feat(main): CdReadSectorReadyCB (424) + func_80035C4C (248) banked byte-identical
Both needed the §376 recovery in the DRAFT — adopt the TU's spelling for a symbol the
draft also declares:
  * CdReadSectorReadyCB dropped its own 'extern void func_800599B8(void *rect, ...)';
    the TU declares it (SpadRect_800184F0 *) at src/800.c:5480, above the insertion point.
  * func_80035C4C adopted 'extern void func_8003D650(int,int,int)' — no caller anywhere
    uses the return value, so the s32-vs-void difference was free to give up.

Also corrects src/800.c's dead-branch 'extern void func_80018714(void);' to '(void *)'.
That declaration lives inside #ifdef NON_MATCHING and is never compiled, but gate_main's
DECL scan has no notion of preprocessor guards and read it as a live conflict. The live
K&R definition at :5576 takes void *, so the correction makes the dead copy agree with
reality as well as clearing the false conflict.
2026-09-02 16:24:23 -06:00
Drew T 35307803f7 feat(main): 6 more banked byte-identical from wave S73m_1
Six of the nine drafts, one clean rebuild, 10.5s. Verified from the SOURCE (the
INCLUDE_ASM stub is gone), not from the tool's own report.
2026-09-02 16:22:10 -06:00
Drew T 383ff02d93 fix(main): align 12 forward declarations with their definitions (§376), byte-neutral
Prepares the S73 wave's 9 byte-verified drafts for gating. Five definitions have
promotion-safe params so the declaration becomes K&R no-prototype — which also keeps
func_8003388C's 'Ent388C *' typedef out of scope at the declaration site, where it is not
yet defined. CdReadSectorReadyCB's u8 is NARROW so no-proto is unsafe (§17-stop); it gets
the exact prototype, safe because that symbol is only ever passed BY ADDRESS.

Verified BYTE-IDENTICAL with no draft substituted, via a DIRECT extract+build with the
binary deleted first — NOT via gate_main --assert-baseline, whose first action is
'git checkout -- src/*.c'. I used that first and it silently reverted these very edits,
then reported GREEN for a tree that no longer contained them: a verification of the
wrong thing. Same hazard as the two banks lost this morning, from the other direction.

Six of the twelve were found by checking every draft systematically rather than trusting
the agents' notes; two were never reported.
2026-09-02 16:21:49 -06:00
Drew T e17fac9411 docs(cookbook): §430 REWRITTEN — a goto into a loop is fine; hand-hoist what it costs
I wrote §430 this morning from a NEAR agent's report: 'a source goto into a loop kills
loop.c's invariant hoisting, so duplicate the statements per arm instead.' The MATCH on
CdReadSectorReadyCB (424/424) refutes it. The goto is what the original source had —
writing it took the residual 318 -> 28 instantly with length exact — and the lost hoist
is REPAIRABLE by hand-hoisting the constants into pre-loop locals (cse cannot fold them
back because MIPS bne/sb need registers): 28 -> 13. Declaration order matters.

The corrected law is better than the guess: a disabled optimizer pass is a job you can
take over, not a wall.

The general lesson, and it is the second instance today: a law derived from a NEAR is a
hypothesis about why something did NOT work; a law derived from a MATCH is evidence about
what does. §428a needed the same correction this morning.

Also banks two more laws this function paid for: cc1 -df's ';; regs to allocate' is a
free allocno-priority oracle (q 10refs/33live beat i 7/24 for $s2; six reshapes failed,
§17 merge + a register pin fixed it), and a stale card tu= cost the last 6 instructions
(func_80018714 is K&R 'void *', not '(void)').
2026-09-02 16:01:42 -06:00
Drew T cdd4aadccc docs(cookbook): §428a sharpened — break-vs-return is a PER-ARM regalloc dial
StreamLoadStateMachine (MATCH 459/459) settled the general form of the law S72 found by
refutation. 'return 0' keeps the hard-$v0 set live inside that arm and EXCLUDES $v0 from
the allocator there; 'break' to a shared post-switch return frees it. Case 11 needs
return 0, every other zero-arm needs break — one dial, eleven positions, correct setting
is per-arm not global. func_80035C4C is the same pattern from the other side.

Completes the ladder: §3-B (fold returns) is the default because it frees the register,
§428a explains why the freed resource resolves coupled residuals, and the dial is how you
put the pin back where one arm needs it.
2026-09-02 15:57:56 -06:00
Drew T 8946b88a32 fix(exclude): actually add SaveLoadRoutine — my guard matched the substring in a comment
The previous commit's cookbook change landed but the exclude entry did not: the guard was
`assert 'SaveLoadRoutine' not in t` over the whole file, and that string already appeared
inside func_8002B0B4's WALL note. Fourth instance today of matching PROSE as if it were
structure. Now compares against parsed ENTRIES, not a substring.
2026-09-02 15:51:59 -06:00
Drew T 379b7eb1a0 docs+exclude: §434 — two symbols, one frame; SaveLoadRoutine is NOT bankable as C
SaveLoadRoutine (1139 ins) and func_8002B0B4 (76) are ONE 0x40 frame split across two
symbols, byte-verified: func_8002B0B4's jtbl_80072E44 points at SaveLoadRoutine and at
labels INSIDE its body, and SaveLoadRoutine has no prologue while owning the epilogue.
gcc-2.7.2 has no sibcall/tail-merge pass, so any C body for either gains a synthesized
prologue/epilogue the target lacks.

An agent reported SaveLoadRoutine as MATCH closeness 0; its own note says 'NOT a C
decompile' — it wrapped verbatim asm. gate_main would refuse it (contains its own
INCLUDE_ASM). NOT counted as a bank. Both now excluded.

This shrinks main's honest matchable frontier by 1,215 instructions (11%). The real fix
is a RESEGMENTATION merging the two symbols, not a draft.

Adds the 3-step frame check to run BEFORE drafting anything large; not running it cost
70k + 134k tokens this session. Also notes that the agent invented its §265 citation
while reaching a correct conclusion (R14: check both).
2026-09-02 15:51:42 -06:00
Drew T 15b0754e70 docs(cookbook): §433 — case source order is the dominant residual on switch functions
Measured across one wave: 4 of 5 consecutive main MATCHes turned on case source order
or the .rodata table. func_800316F8's .text was ALREADY exact and it still could not
bank — 18 bytes, all table. gcc emits case BODIES in source order while entry i points
at case i, so value and order are independent and only ORDER is pinned by .text, which
is the only thing match_one compares (§405-A).

Records the method every agent converged on independently: read the table order from the
.s, write bodies in that order, set values to the inverse permutation, then verify table
entries / reloc symbols / internal j destinations by hand before reporting. Pairs with
§427's TABLE REJECT verdict, which names the same class from the gate side.
2026-09-02 15:42:23 -06:00
Drew T 6c2a3bb43b docs(cookbook): §432 — defeat cse's mask merge with a shift pair that combine folds back
From main/func_8002DC68 (MATCH 198/198). The target masks one value twice (a compare,
plus a second andi that reorg steals for a beqz delay slot). Written as param_2 & 0x7F on
both sides, cse merges the two (and:SI) and the delay slot comes out EMPTY. Spelling ONE
as (param_2 << 25) >> 25 hides it from cse — different RTX — and combine's
simplify_shift_const folds it back to andi. Two masks in the RTL, one instruction each
out. Byte-verified on either side.

The inverse of the usual advice: normally you make two expressions identical so cse
merges them; here you make them different to cse and identical to combine, exploiting
pass order. Any x & ((1<<n)-1) has a shift-pair twin with this property.
2026-09-02 15:24:29 -06:00
Drew T 5b6a2e86dd fix(exclude): drop func_8005C1C0 — its INCLUDE_ASM is inside a comment; it is banked
My sweep for '§179-C documented walls' grepped raw text, so it matched an INCLUDE_ASM
line quoted inside a 'BANKING: this block REPLACES the line ...' comment. corpus.stubs
said banked, my grep said stubbed, and corpus was right.

Third instance today of one bug class — reading PROSE as CODE. The other two were
split_src_region.item_name matching a parenthesised token inside a comment, and matching
a leading extern declaration as the definition. The exclude_audit caught this one
immediately by flagging my own addition as STALE.
2026-09-02 15:19:12 -06:00
Drew T ce62b9762f fix(exclude): add the two §179-C stubs main's own source documents as unmatchable
R45 — never draw a card the pipeline cannot bank. src/800_b.c carries the explanation
directly above func_8002B0B4's stub (no epilogue; every exit is a raw j/jr into labels
inside SaveLoadRoutine's body, so gcc-2.7.2 always synthesizes an epilogue the target
lacks), and the wave drew it anyway: 70k tokens and 100s for an agent to re-derive that
paragraph and hand back the stub verbatim, reported as MATCH closeness 0.

A draft that IS its own INCLUDE_ASM is the silent-no-op class gate_main already refuses,
so nothing would have banked — but the agent slot was spent. Swept main for the class:
exactly 2 such stubs, both now excluded.
2026-09-02 15:18:37 -06:00
Drew T a5a78bc9cf fix(split_src_region, jr_isolate): five defects in the overlay TU-split path (blocked since Phase 26)
jr_isolate has been unusable since Phase 26 — its own docstring says "BLOCKED on
split_src_region". Five distinct defects, each found only after fixing the one above it:

1. split_src_region demanded an address for EVERY top-level item, but an overlay .c is
   full of address-less constructs (hoisted typedef blocks, per-function extern runs,
   comment banners). coalesce() now merges an address-less run FORWARD into the item
   below it — they are a preamble belonging to that function, which is §431's model.
2. coalesce re-derived the name from the MERGED text, so item_name matched the preamble
   instead of the function. It now carries (addr, name, text) captured before the merge.
3. item_name scanned COMMENTS as if they were code: a comment containing any
   parenthesised token won over the real definition below it.
4. item_name matched a leading "extern void (*D_x[])(void);" and returned the name
   "void" — the §192 class, which gate_main.sym_of fixed for itself and this tool never
   got. A real function was then treated as a preamble and merged into its neighbour,
   leaving its body inside another item while its own stub survived: 26 duplicate
   symbols in one overlay. It now anchors on a DEFINITION (ends in an open brace, not a
   semicolon) and refuses type keywords as names.
5. That definition anchor required column 0, so an INDENTED top-level body was invisible.

Also: jr_isolate's idempotency check keyed on the CONFIG, which it writes FIRST, so any
failure in between left a half-applied tree the tool believed was finished. It now
requires the source file too and refuses with recovery instructions. And inject accepts
"already present and textually IDENTICAL" — splat emits an empty function as C, not as a
stub — while still failing hard when the destination defines it DIFFERENTLY.

Progress on ov_SC02_005: trim went 78 kept / 231 moved -> 89 / 255; duplicate symbols
26 -> 0; the chain now runs to completion (rc=0).

NOT DONE: the object still fails to assemble on a remaining duplicate-definition class.
Tree restored, ov_SC02_005 BYTE-IDENTICAL.
2026-09-02 15:08:18 -06:00
Drew T e830e63be5 fix(draw_waves): normalise exclude rows to (binary, fn) — the list was excluding NOTHING
My own regression from the same session: exclude_audit.parse now returns 4-tuples (it
carries the WALL pin and each entry's note), and draw_waves built `skip` straight from
them, so every membership test against a 2-tuple missed and the exclude list had no
effect at all — while the run reported success.

Caught by MEASURING the pool rather than trusting the run: it came back 88 non-main + 45
main = the full frontier, when a 25-entry list should have reduced it. Now 69 and 41,
which reconciles exactly (88 - 16 carve-blocked - 3 non-main walls; 45 - 4 open main
walls, PopMatrix/PushMatrix being linked and already refused).

The silently-narrowed-scope shape again, and the third time this session that counting
the RESULT rather than trusting the REPORT is what caught it.
2026-09-02 14:55:15 -06:00
Drew T ca7102e3b6 fix(exclude_audit): pin curated WALLs so a derived classifier cannot drop them; merge 7 walls ledgers
Found by checking readiness rather than asserting it: S71's two PROVEN walls
(ov_SC03_105:func_801834A4, ov_SC06_022:func_8017DF28) were NOT in the canonical list —
they lived in a separate .run/S71_walls_found.txt the regeneration never saw. Drawing
would have spent agents re-proving them (playbook §1b: a full agent run each time).

Merging them in exposed a second defect: a WALL has no jump table, so the DERIVED logic
would classify it RE-PROBE and drop it.  in the input is now read as a PIN that
survives regeneration, and the entry's ORIGINAL note is carried through — a wall's value
is its refutation list, and replacing that with boilerplate turns evidence into a bare
'do not try'. Round-trip verified idempotent: 9 walls survive a second pass unchanged.

Merged 7 walls ledgers (S67/S68/S68_332/S69/S70/S71/S71_found) into
config/wave_exclude.txt: 25 entries = 16 CARVE-BLOCKED (derived) + 9 WALL (curated).
The audit found 8 of the merged walls already BANKED — a wall that got matched is no
longer a wall.

DELIBERATELY NOT merged: .run/t3wall_list.txt, 99 BARE function names with no binary.
R48 — the same name is a different function in another overlay, so a bare-name exclude
over-excludes silently fleet-wide.
2026-09-02 14:54:27 -06:00
Drew T ab0bbd257b docs(phase-31): S72 addendum 2 — exclude list regenerated, draw refuses a stale one
88 of 107 entries were stale one day after the list was written; 46 of them were
12,750 instructions of open drawable work including SaveLoadRoutine. Canonical list is
now config/wave_exclude.txt (19 entries), and draw_waves --exclude-file audits it as a
prerequisite.
2026-09-02 14:39:55 -06:00
Drew T 4f66709ade chore(exclude): promote the wave exclude list to config/wave_exclude.txt
There were NINE session-snapshot copies under .run/ and no way to tell which was
current — the accumulation smell behind the whole staleness problem. This is the one,
it is tracked, and it is regenerated rather than hand-edited.

.run/ is gitignored scratch, which is the wrong home for it: CARVE-BLOCKED entries are
derived and vanish when the subseg is split, but WALL entries are CURATED and cannot be
re-derived — that is precisely why the file needs to be tracked.
2026-09-02 14:39:09 -06:00