Commit Graph

3707 Commits

Author SHA1 Message Date
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
Drew T 983df054f2 feat(draw): audit the exclude list as a PREREQUISITE — a stale one is refused
An exclude list records what the TOOLING could not do, then gets treated as a property of
the FUNCTIONS. Nothing re-examined it, so every tool fix left behind a population that is
now tractable and still marked impossible — invisible, because the draw filters it out
before anything measures it.

MEASURED one day after .run/S71_exclude.txt was written: 88 of its 107 entries were
stale — 28 already banked, 14 linked PsyQ symbols that were never targets, and 46 whose
blocker had since been fixed. Those 46 are 12,750 instructions of open, drawable work
including main:SaveLoadRoutine (1,165), the largest function left in main.

* tools/exclude_audit.py (NEW) — classifies each entry by its CURRENT blocker
  (BANKED / LINKED / RE-PROBE / CARVE-BLOCKED / WALL), regenerates keeping only the
  still-valid classes, and --assert-fresh exits 3 on staleness.
* draw_waves --exclude-file — runs that audit and REFUSES to draw on a stale list, naming
  the counts and the regenerate command. --exclude-stale-ok still draws but prints what it
  ignores: skipping is possible, never silent. Also fixes the old --exclude parsing, which
  could not survive a '#' comment.
* .run/S72_exclude.txt — the regenerated list: 19 entries (16 CARVE-BLOCKED + 3 WALL),
  each carrying its reason, down from 107.

Verified in all three directions: stale refuses rc=1, fresh proceeds rc=0, override
proceeds and announces. The parser's own report-don't-drop design caught a bug I
introduced in it (comma-splitting before comment-stripping).
2026-09-02 14:38:39 -06:00
Drew T fd5e700cfe chore(tools-health): note when to flip split_indicator from informational to a hard gate
Informational only while 4 known violations exist; a permanently-red gate trains people
to ignore it (R54). When the last overlay is split, drop the '|| echo' so a regression
fails the gate.
2026-09-02 14:15:31 -06:00
Drew T 27c81ed0c2 docs(phase-31): S72 addendum — split_indicator found 4 more binaries (16 fns / 3,613 ins)
18% of the non-main frontier, all already in the exclude list as if unmatchable rather
than 'needs a subseg split'. Plus: 28 of that list's 107 entries are already banked, so
regenerate it before the next draw.
2026-09-02 14:10:20 -06:00
Drew T 12f64ca84e docs: regenerate the cookbook index for §426-§431
tools-health --check caught it stale (1,099 sections). My own bookkeeping — the index is
DERIVED and self-asserts coverage (R33/R32), which is exactly why the gate found it and I
did not.
2026-09-02 14:09:48 -06:00
Drew T b173d88676 feat(split_indicator): detect subsegs that MUST be split before their switch fns can bank
A code object contributes exactly ONE contiguous .rodata run, so a subseg owning raw
jump tables in >=2 non-adjacent island spans makes every switch function outside the one
carveable span unbankable at any effort. main sat in that state from Phase 7 to Phase 31
and eleven functions were written off as 'PROVEN gate-rejects' because of it. The
evidence is derivable from the raw image on day one; nothing was comparing it.

FIRST FLEET RUN: 209/213 OK, 4 overlays flagged — ov_SC01_084, ov_SC02_005, ov_SC02_011,
ov_SC03_105 — holding 16 open functions / 3,613 instructions (18% of the non-main
frontier). All 16 were already in the S71 exclude list, i.e. recorded as if unmatchable
rather than as 'needs a subseg split'. 3.7s fleet-wide.

Self-test covers all three directions: fires on main's pre-S72 island (fed
synthetically, because the real tree no longer holds that state), stays silent on main
today, and does not over-fire on a one-span subseg. Linked-library subsegs are excluded
on principle — their code comes from a .a so cc1 emits no table for them; without that
filter main reports NEEDS SPLIT on libgs6, which the self-test caught.

Wired into make tools-health. accelerators #20 gains the when-to-split rule: split where
the BUILD forces a boundary (decidable at 0% matched), at the span-owner boundaries and
nowhere else, never on TU archaeology.
2026-09-02 14:03:48 -06:00
Drew T 09886be66b docs(phase-31): S72 FINAL — R22 213/213, 14 main banked, main 52.6%, frontier 133
The carve + the src/800.c split at the original TU boundaries. 7 of the 14 banks were
span B/C — impossible before the split. Next session starts with 11 functions /
4,479 instructions that are now merely undrafted rather than unbankable.
2026-09-02 13:54:16 -06:00
Drew T 94ca16e007 docs(accelerators): #20 — set TU boundaries at the rodata island's jtbl spans, at segmentation time
Found P31 S72; COULD have been found 2026-06-15 (Phase 7, commit:0025), which wrote the
island's contents by hand and named loadDestPtrTable as the divider. The signal needs no
matching progress — it is a property of the retail image.

The number that matters is the cost curve: at Phase 6 src/800.c had 13 externs and 0
typedefs and the split was a yaml edit; at S72 it had 2,378 externs and 175 typedefs and
cost 57 crossing declarations, a shared header and 4 stale consumers. Plus a session of
wrong conclusions (11 'PROVEN gate-rejects', 10 of which banked once the carve existed).

PREREQ recorded honestly: the binding constraint came from the OVERLAY work two phases
later, so this is knowledge that never got carried back to main — not carelessness.

General principle: segmentation is the exception to probe-before-investing. When a
decision is evidenced at t=0, cheap now, and strictly more expensive later, make it early
even though its payoff is unproven.
2026-09-02 13:49:37 -06:00
Drew T 8e8521da22 fix+docs: make every consumer aware of main's new TU layout (R36)
The split created two new TUs and a shared header; four consumers still described main's
game code as one file:

* tools/reconcile_slate.py — HARDCODED open('src/800.c'), so after the split it saw a
  THIRD of main's typedefs while reporting success (silently-narrowed scope, R32).
  Measured: 133 visible before the fix, 187 after, 0 lost. Now globs
  corpus.src_files('main') + src/800_shared.h, so a future split is already handled.
* docs/wave-playbook.md 1c — still said spans B/C/D were NOT drawable and that drawing
  one is an R45 violation. That is now false and would have STOPPED a future session
  from drawing the very targets this work unlocked.
* cookbook §426 — its 'the remaining spans need src/800.c split' paragraph now records
  that it was done the same session, and points at §431 for the method.
* config/dedup.us.yaml + src/shared/clearTbl40.h — both said dedup group I0 is
  instantiated 'at both sites in src/800.c'; both sites are above 0x80035270 and are now
  in src/800_c.c.

Byte-neutral: dedup.us.yaml parses, gate_main --assert-baseline BYTE-IDENTICAL.
SETUP.md gains a row describing the layout and the rule it implies: never hardcode
src/800.c, glob corpus.src_files('main').
2026-09-02 13:48:00 -06:00
Drew T 430f40576a docs(cookbook): §431 — widen the gate-reverts-src rule from declaration edits to BANKS
I wrote the rule for declaration edits and then lost two byte-proven banks to the same
mechanism an hour later. gate_main's opening 'git checkout -- src/*.c' destroys anything
uncommitted in src/, banks included. R42 is 'commit before the next command that can
touch src/', not 'commit at a good stopping point' — and count banks from the SOURCE,
which is the only oracle that caught it.
2026-09-02 13:43:11 -06:00
Drew T 8c72831177 fix(main): recover func_8002EED8 + func_8002F248 — I destroyed them with my own R42 violation
Both banked byte-identical earlier this session ('BANKED 2 of 3'), then sat UNCOMMITTED
while my very next action was another gate. gate_main.try_batch's first step is
`git checkout -- src/*.c`, which reverted them; the following commit captured only the
third function. I reported 14 banks; the source said 12.

Caught by counting banks from the SOURCE (the INCLUDE_ASM stub's absence) rather than
from my own account of what I had done — the oracle the project already mandates.

I had written this exact hazard into cookbook §431 an hour earlier, for DECLARATION
edits, and did not apply the same reasoning to BANKS. R42 is not 'commit at a good
stopping point', it is 'commit before the next command that can touch src/'.
2026-09-02 13:42:30 -06:00
Drew T 468c3751c3 docs(decision-log): S72 addendum — the split decision, and the effort estimate I got wrong
Quoted '2,318 scattered externs' as measured-not-guessed. It was measured, and it
measured the wrong quantity: what a split costs is declarations used OUTSIDE their
region (57 of 1,247), not the total. R41's denominator discipline applied to an EFFORT
estimate. Also records the correction to the '1998' premise: the spans prove at least
three TU boundaries, not that the devs' files were exactly these three.
2026-09-02 13:37:38 -06:00
Drew T 3e8138819a docs(cookbook): §431 — splitting a 27k-line TU at its original boundaries
Where to split: the jtbl spans (tables pack tight within a TU, separated across TUs), and
that is also the MINIMUM — a TU with no switch emits no table and is invisible, so what
you recover is a lower bound on the original structure, not the structure.

What crosses: ask the compiler. 2,318 externs is the scary number and the wrong one; only
57 of 1,247 declared names cross a boundary, 19 of them typedefs with one definition each
and zero shape conflicts. Fix TYPES first — a missing typedef cascades into dozens of
parse errors that all evaporate at once.

Plus the general defect it exposed (a typedef stripper must read the destination's
includes) and the operational rule it cost twice (gate_main reverts src/*.c first, so
commit alignment edits before gating).
2026-09-02 13:37:11 -06:00
Drew T 40bbd07c45 feat(main): func_80036260 banked — 10 of the 11 'PROVEN gate-rejects' are now banked
Its blocker was an extern the DRAFT carried for a different symbol
(func_8004355C declared (s32, void*) against the TU's (s32, u8*)). Adopting the TU's
spelling verbatim — the documented §376 recovery — banked it byte-identical in 10s.

Only func_800316F8 of the eleven remains, and it is a TABLE REJECT: .text
byte-identical, 18 bytes wrong in its own jump table (§405-A).
2026-09-02 13:36:20 -06:00
Drew T 00e5bfe57c feat(main_diff_locate): TABLE REJECT — the third verdict class, and the one this session is about
func_800316F8's .text was BYTE-IDENTICAL and all 18 differing bytes were its own jump
table; the tool called it a PLUMBING REJECT and routed it to the §376 declaration chain,
advice that would never have fixed it. classify() now separates a .rodata-only
divergence and names it §405-A: match_one compares .text ONLY, so a draft sits at
closeness 0 while emitting a wrong table — gcc emits case BODIES in source order while
entry i points at case i, so case value and case order are independent and only the
order is pinned by .text.

Attribution reads one symbol LOW for a cc1-emitted table (once the function is C its
table is a $L label, not a jtbl_ data symbol, so the bytes land in the PRECEDING table's
extent) — the OBJECT name is what identifies it, not the symbol name. Shared by
gate_main so both report the same four verdicts.

Controls: self-test PASS, green build IDENTICAL, and the known func_800316F8 case now
classifies TABLE REJECT.
2026-09-02 13:35:17 -06:00
Drew T 9de9514249 feat(main): 3 more banked via the §376 alignment — func_8003602C, func_80038FFC, func_80039C70
Byte-identical. 13 main functions banked this session.

func_800316F8 rejected with a precise, new-class verdict: its .text is BYTE-IDENTICAL
and all 18 differing bytes are its own jump table at 0x800730C4 (attributed to the
preceding jtbl_800730AC because the table is now a cc1 $L label, not a data symbol).
That is §405-A in the flesh — match_one compares .text ONLY, so a draft can sit at
closeness 0 while emitting a wrong table. Not a body reject and not plumbing either.
2026-09-02 13:34:26 -06:00
Drew T 11dda014ee fix(main): align 5 forward declarations with their definitions (§376), byte-neutral
Each of these five had a TU forward declaration that contradicted the real signature,
which is what made gate_main drop their byte-correct drafts:

  func_800316F8  void f(void*)            -> void f(s32)        + cast at the one call
  func_8003602C  void f(void)             -> void f(s32)          (use is address-taken)
  func_80036260  void f(void)             -> int  f(void)         (use is address-taken)
  func_80038FFC  void f(u8**)             -> s32  f(s32*)       + cast at the one call
  func_80039C70  void f(void*,s16,u8)     -> void f(u8*,s16,s16)

Verified byte-identical with NO draft substituted, so any later gate failure is
attributable to the draft and not to this edit.

OPERATIONAL NOTE: gate_main's first action is , so an
UNCOMMITTED declaration edit is silently discarded and the gate then judges the drafts
against the old declarations. Commit alignment work before gating (R42's shape, seen
from the tool's side).
2026-09-02 13:32:32 -06:00
Drew T 483e2514a3 feat(main): 3 span-B functions banked; gate_main now sees header-provided typedefs
func_8002EED8 · func_8002F248 · func_80031988 — byte-identical, the first banks that
span B's carve made possible.

gate_main defect the split exposed: defs_above scans the destination .c ALONE, so a
typedef the TU gets through #include is invisible to strip_dup_typedefs and every draft
carrying its own copy dies with 'redefinition of X'. Latent until src/800.c's split moved
19 shared typedefs into src/800_shared.h, at which point func_80031988 — byte-correct,
and one of the eleven — failed to compile for that reason alone. header_defs() now walks
the destination file's quoted includes transitively and seeds defs_above with what they
provide, so an identical copy is stripped and a different shape is renamed, exactly as
for in-file definitions.

func_80031988 had TWO stacked blockers: this one, and the struct-tag false conflict in
typesig fixed earlier today. Neither was a property of the function.
2026-09-02 13:30:20 -06:00
Drew T 7df4895e7b feat(main): split src/800.c at the jtbl-span TU boundaries — spans B and C now carve
BYTE-IDENTICAL with NO function banked (gate_main --assert-baseline, clean rebuild),
which is the whole point: the structure lands first and proves neutral, then drafts bank
against it.

One code object contributes exactly ONE contiguous .rodata run, and 800.o's is span A,
so spans B and C each needed their own object:

  800    vram 0x800123F0-0x8002B0B4  -> .rodata span A (0x80072A38-0x80072C70)
  800_b  vram 0x8002B0B4-0x80035270  -> .rodata span B (0x80072E44-0x80073140)
  800_c  vram 0x80035270-0x8003A444  -> .rodata span C (0x800732A0-0x8007344C)

The span owners' address ranges are disjoint and ordered — tables pack tight WITHIN a
TU and are separated by other data ACROSS TUs — so these are (at least some of) the
original translation-unit boundaries. Splitting here is both the fix and the minimum;
any extra split would be speculation.

main's island is now a 7-piece data->rodata sandwich, so ld_interleave moves from
--front/--tail to --order.

THE SPLIT WAS CHEAP, AND MY FIRST ESTIMATE WAS WRONG. I costed it at '2,318 scattered
extern lines' — that is the TOTAL; what matters is how many CROSS a boundary, and that
is 57 of 1,247 declared names (4.6%), of which 19 are typedefs with exactly one
definition each and zero shape conflicts. Zero file-local statics. src/800_shared.h
carries exactly those, derived from the COMPILER's own errors rather than a regex model
of C (R33), and each typedef was MOVED, never copied.

Unlocks 17 functions / 4,471 instructions = 39% of what is left in main, incl.
SaveLoadRoutine (1139) and func_8003388C (663).
2026-09-02 13:27:29 -06:00
Drew T f0148e6822 docs(phase-31): S72 CLOSE — R22 green 213/213, 7 main banked, main past 51.8%, frontier 140
The 11 'PROVEN gate-rejects' were one missing rodata carve, not bad bodies. Next
session starts at the span B/C carve: 18 jtbl functions left in main's frontier holding
most of its remaining instruction mass, blocked on splitting src/800.c at 0x8002B0B4
and 0x80035270 — the original TU boundaries the jtbl spans reveal.
2026-09-02 12:58:44 -06:00
Drew T 174f7aaf7a docs(cookbook): §430 — a shared tail is a late cross-jump merge, not a source goto
Counterweight to §3-B, with a precise discriminator. When two arms converge on a shared
block, that block is usually a late cross_jump merge of per-arm DUPLICATED statements
(§298). Spelling it as a real goto is not equivalent when the label sits INSIDE a loop:
the goto becomes a jump into the loop body, jump.c's mark_loop_jump marks it
loop_invalid, cc1 -dL prints 'Loop at N ignored due to multiple entry points', and
loop.c silently drops invariant hoisting — measured: the 1/0x80 constant hoist into
$a0/$a1 vanished, 2 insns plus a spurious andi.

Discriminator: shared tail outside every loop -> fold it (§3-B, and you may free a hard
ABI register). Shared tail inside a loop body -> duplicate per arm and let cross_jump
merge. A -dL line is a free oracle for this.

Also records CdReadSectorReadyCB's three remaining residual clusters as pack fuel so the
next attempt starts from the draft, not from the .s.
2026-09-02 12:53:19 -06:00
Drew T 8c40fa3ebd feat(main): 4 more span-A switch functions banked byte-identical (wave S72m_1)
CdReadStateMachine (385 ins) · func_80024448 (362) · func_80026D64 (189) ·
func_8001B0D4 (86). One clean rebuild, 10.8 s, 4 of 4 accepted.

Wave shape: 5 targets, one agent per workflow, 5 concurrent. 4 MATCH / 1 NEAR.
Every agent verified its jump table and reloc stream past match_one's .text-only blind
spot (§405-A) because the packs carried the §426 carve note.

Three new laws banked from their notes: §428 (zero-byte cross-jump barrier), §429
(every held pointer needs its own local), §428a (two residuals moving in opposite
directions share one starved resource — which refuted my own prediction).

func_80024448 was recovered from disk after its Fable agent was killed by a rate limit
and the workflow reported NO-DRAFT; match_one on that file: closeness 0 (playbook §5b).
2026-09-02 12:52:39 -06:00
Drew T c0724cb59e docs(cookbook): §428a REWRITTEN — my prediction was refuted; the real law is better
I predicted §428's UID barrier would resolve func_8001B0D4's fence<->over-merge
coupling, reasoning that it changes no liveness. The escalation that tested it did not
use §428 at all. §3-B did it: seven in-block 'return 0;' -> 'goto L_ret0;' to one shared
tail removed the priority-1 hard-$v0 sets, freeing $v0 for the D_800747E4 reload and
$v1 for CdQueueBusy's result, AND fired all three cross-jumps (92->86). One edit, both
residuals.

The real law is the opposite of my framing: two residuals moving in opposite directions
under every lever are usually not in tension — they are two symptoms of ONE starved
resource, and every lever so far was paying for one with the other. Ask what they are
both competing for, and inventory the hard register sets the source forces. A repeated
'return <const>;' in switch arms is the commonest way to pin $v0 many times over.

The wrong prediction is kept in the section as the refutation (R14).
2026-09-02 12:38:49 -06:00