Commit Graph

2374 Commits

Author SHA1 Message Date
Drew T a85733a487 feat(phase-31): S78 #3 — 13 "game code" subsegs were PsyQ objects: wired LINKED (libgte 70/30, libgs 33/7, snd 62/11); main's game-code metric corrected to 91.8%
- exact tiles, 0 tokens: libgte23-26 (MSC01/02/05/09, SMP_00, FGO_01-06, PATCHGTE), libgte9 re-derived
  as SMP_05 NormalClip (SMP_06 NormalClipS = nested sub-pattern; psyq_integrate now drops nested
  placements), libgte27-30 (the libgs-gap MTX_05/07/11, REG03+REG11), libgs7 (2D_BG0+2D_BG1), snd10
  (VM_NO1), snd11 (VM_NOWON carved off sgap_8). LINKED 959->1040, REAL 912->886 (SDK inline-asm wrappers
  re-provenanced), VERBATIM 146->85, 13 TUs deleted; splat re-emits the stub records.
- main 143dbb89 WITH and WITHOUT the SDK objects. The no-SDK fallback had been red since S7x
  (CdReadyCallback called by its SDK name while the libcd stub carried func_800435B4) — curated
  CdReadyCallback = 0x800435B4, refs unified. R22 clean fleet 213/213; tools-health OK.
- METRIC CORRECTION (R35): progress.py's "MAIN game-code weighted" sig never excluded the LINKED
  objects (its comment said it did) — ~31k linked-SDK ins sat in the denominator as unmatched game
  code. Exclusion now derived LIVE from the Makefile stub lists + yaml ranges: 91.8% (44,562/48,537),
  not 59.8%; the 3,975-ins remainder equals the open-stub sum exactly.
- VM_F.o probed SPLITTABLE at .bss 0x50c (SYS.o's class -> task #4). cookbook §488; worklist S78 #3;
  decision-log + accelerators; SETUP rows.
2026-09-04 16:26:12 -06:00
Drew T a7394f44dc feat(phase-31): S78 #12 — the 800c3 "wall" band is LIBPAD 4.2.1 + LIBAPI 4.2: 46 names applied; integrate wired by subseg range; renames via ApplySymbols
- provenance: the psx loader's per-version PsyQ signature sets place PADENTRY/PADCMD/PADPORTD/
  PADSEQD (4.2), WAITRC2 (4.3), COUNTER/C114/FIRST/PAD/PATCH/CHCLRPAD (libapi 4.2) byte-exact in
  0x8005CE48-0x8005FC68 / 800c2 -> 12 of main's 29 stubs incl. all four §332 walls are Sony's
  DualShock library in reorder mode. 46 names -> symbols.us.txt (count 1081), band TUs, verbatim
  manifest, wave_exclude; firstfile/firstfile2 (4.2 naming); CdGetToc @0x800430B8 (was the Phase-21
  xdedup mislabel DecDCToutCallback). SETUP §5.1 corrected; psyq-worklist S78; cookbook §487;
  decision-log + accelerators S78; CHECKSUMS +Psy-Q_46.zip +PSYQ_SDevTC_v4.5.zip.
- psyq_integrate: --yaml maps stub<->objects by SUBSEG RANGE with an exact-tiling check and PRINTS
  the located-but-unwired residue (libgte: 13 objs / 1,264 ins) — main's LINKED build had been RED
  at HEAD since the S77 psyq_identify fix (22 libgte blocks merged to 3; gate worktrees take the
  stub fallback so it never showed); a library object's exported symbol whose recovered address the
  curated file names differently is --redefine-sym'd (R15; A66 firstfile->firstfile2).
- Ghidra: 47 MCP renames did NOT persist through the sentinel stop (R9 caught it) -> NEW
  tools/ghidra_scripts/ApplySymbols.java + tools/ghidra_apply_symbols.sh mirror the curated file
  headless with a real save: 73 renamed, R9-verified x4. SETUP inventory rows (R21).
- lint_symbol_refs: scans verbatim __asm__ bodies (`.ent\tfunc_X` is invisible to \b and to the
  string-masked scan); negative-controlled (red on the pre-fix TUs, green on the passing tree).
- R22: clean extract-all 212/212 + check-all green on the final config; main rebuilt byte-identical
  143dbb89 after the last src-only fix -> 213/213; tools-health OK.
2026-09-04 15:57:06 -06:00
Drew T b31e499c9b fix(carve): repoint 800_b_2's INCLUDE_ASM paths to its own subseg
The 3-way split moved func_8002FDE8 and func_80032A74 into the 800_b_2 subseg,
but their INCLUDE_ASM directives still named "asm/nonmatchings/800_b". The
incremental build passed anyway because the OLD .s files were still on disk;
make clean removed them and splat now emits under 800_b_2, so a genuinely clean
rebuild died in jtbl_rodata_pads:

    FileNotFoundError: asm/nonmatchings/800_b/func_8002FDE8.s

This is exactly what R22 exists to catch, and it is the reason a byte check is
only trustworthy from a clean tree. asm/nonmatchings/800_b no longer exists at
all -- piece 1's three functions are all banked, so splat emits no directory
for it.

main rebuilds 143dbb89f34491258bbc27810d0a12ec8b43a8dd from a clean extract.
2026-09-03 22:26:10 -06:00
Drew T f4ff8267a5 feat(decomp): bank main:func_8002C410 (299 ins) — the first -O0 island in main
The body was MATCH 299/299 from the S77w wave and could not bank for want of an
-O0 object. With the 3-way carve in place it gated first try.

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 22:21:24 -06:00
Drew T a13b2a5c38 carve(main): 3-way -O0 island split of 800_b for func_8002C410
func_8002C410 MATCHES 299/299 at -O0 and DIFFs 228-vs-299 at -O2 (verified
independently with match_one --o0 vs --no-auto-o0). gcc-2.7.2 has no
per-function optimize pragma, so opt level is per FILE, and the function needs
its own object. Main had no path to one: the Makefile's -O0 wildcard covered
src/ov_*/ and src/md_*/ but NOT top-level src/*.c, and o0_subsplit.py is
overlay-shaped -- it died on config/splat.main.yaml, which does not exist.

Measured the scope first (R37): the -O0 detector flags exactly TWO open main
stubs -- this one, and func_80011380, which already lives in -O0 boot.c and is
the proved floor. So this unblocks one function, not a class.

FIVE COUPLED PIECES, which is why the carve is worth recording:
  1. splat code rows: 800_b cut 3 ways -- 800_b / 800_b_o0a / 800_b_2
  2. splat .rodata: span B SPLIT, because the 3-way cut put its two jtbl owners
     in different objects -- func_8002B0B4 into 800_b, func_800335B8 into
     800_b_2 -- and one code object may contribute exactly ONE contiguous
     .rodata run. The boundary is DERIVED, not guessed: 800_b.o's compiled
     .rodata is 0xf8 bytes, so the front run ends at 0x80072E44+0xf8. The
     build's own jtbl_rodata_pads caught the missing piece.
  3. src/800_b.c split 3 ways -- 86-line prologue duplicated, 3 defs before the
     island, 97 after
  4. Makefile -O0 glob widened to top-level src/*_o0?.c
  5. ld_interleave --order: 800_b_2.o inserted after 800_b.o. Missing this
     floated the tail rodata and shifted every data symbol by exactly its size,
     +0x204, across 704 two-byte runs -- which is how it was found.

o0_subsplit.py now REFUSES main loudly instead of dying on a missing file
(R43/R61a) and names the manual procedure.

VERIFIED BYTE-NEUTRAL BEFORE ANY BANKING: main builds
143dbb89f34491258bbc27810d0a12ec8b43a8dd with the split in place and
func_8002C410 still an INCLUDE_ASM stub.
2026-09-03 22:20:57 -06:00
Drew T 335e1d677d feat(decomp): bank main:func_8001EA14 (371 ins) from close=89
Five new levers, all in the draft header. The headline one (L5): STATEMENT
ORDER IS THE ALIAS ORDER — a mem/s local matrix store can never be hoisted over
by a mem/s varying p-> load, because true_dependence's exemption needs one side
non-struct AND non-varying. Writing the matrix init in NATURAL OFFSET ORDER
closed the whole 45-instruction init block, and the same law one scope down
removed the +1 length drift.

Also: an inline-asm "r" operand that is a bare symbol_ref has NO pseudo and is
allocated by reload ($t0); assigning it to a local first makes it a pseudo and
local-alloc gives $v0 — worth 10 instructions.

A scripted 858-candidate sweep PROVED mode/rot/shift placement inert, which is
what redirected the hunt from LUID to DAG/allocation.

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:43:01 -06:00
Drew T 8754b1a671 feat(decomp): bank main:func_800301C8 (170 ins), first-gate clean
18 -> 0 via three levers, all worth reading in the draft header: the sibling
func_8002FF0C's block-scope scalar spelling of D_800A46D2 (the array spelling
lets cse cache 'la $s1' across the call); splitting a $17 pin so only the
b*24 intermediate is pinned (expand_mult passes accum_target=target, and a HARD
target survives expand's generate-into-pseudos guard, so pinning the result
drags the whole chain); and pinning the DESTINATION for idx98, because
'addu $s0,$s1,$s0' is expand_binop swapping commutative operands to make
op0==target, not tree order.

Gated compatible on the first try — the agent had verified the spliced TU
compiles rc=0 with an instruction stream identical to the standalone compile.

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:32:57 -06:00
Drew T 79006e5f4d feat(decomp): bank main:func_8006252C (§378 chain + the double re-tie)
Took three tools in order, and the first two were wrong:
  - scope_demote_drafts BROKE it (it aliased D_80078D08 through __asm__ and the
    build failed) — the clash was never a data extern
  - the real clash was func_8006252C ITSELF: TU void(void) vs draft s32(void),
    i.e. self_decl_tu -> cast_self_callers --sync-decls, 4 call sites
  - then sync_tu_decls closed func_800625DC and func_80062644

The BODY is the interesting part and is now cookbook §482: two INDEPENDENT asm
re-ties, ordered, because one barrier fixes one residual and re-creates the
other.
2026-09-03 20:31:27 -06:00
Drew T 6f5d1ecdca plumb(main): §378 self-caller casts for func_8006252C
TU declares it void(void), the draft returns s32 — the self_decl_tu class.
4 call sites cast; baseline green with NO draft substituted (143dbb89...).
2026-09-03 20:29:47 -06:00
Drew T 8e3e084f3b feat(decomp): bank main:func_8005D588 via the §8d scope-demote
gate_main has no scope-demote rung — only gate_stage's ladder calls
scope_demote_drafts, so a MAIN draft never saw §8d. Running it by hand demoted
7 file-scope data externs to block scope (D_80072960 among them, whose TU
spelling is void(*)(void) against the draft's void(*)(void*)) and the byte gate
then accepted the body.

gate_main: BANKED 1 of 3 after bisection, 143dbb89f34491258bbc27810d0a12ec8b43a8dd
BYTE-IDENTICAL. The other two are genuine rejects: func_8005FA94, and
func_8005D33C whose rejection shows the mass symbol shift its own agent
predicted from the jump-table rodata placement.
2026-09-03 20:24:44 -06:00
Drew T 1f2ae12b5d feat(decomp): bank main:func_8001FC08 (400 ins) and func_8002FF0C (166 ins)
Both bodies were already solved in S76 and had never banked. Neither needed a
codegen change — they needed the gate to stop applying a rule cc1 does not
(§481 / the _depth0 fix): func_8001FC08 renames its struct to MTX_8001FC08 and
declares D_80074818/D_80075018 at block scope, and func_8002FF0C shadows
D_800A46D2 with a block-scope scalar because the array spelling forces la and
costs 12 mismatches.

gate_main: BANKED, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:21:22 -06:00
Drew T 5b8a0d0804 feat(decomp): bank main:func_8005D538 from the S77w wave
Plain C, no §265 verbatim needed — the pack's prior FAILED attempt had
over-thought it. Two levers: omit the forward decl for func_8005E188 so the
call is implicit K&R (fixing both an s0/s1 order swap and a spurious
sign-extend a visible prototype would insert), and close the 2 trailing pad
words with a file-scope __asm__ per the §295 class.

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:19:20 -06:00
Drew T c35a21449e feat(decomp): bank func_8005E13C and func_8005EAE8 from the S77w wave
gate_main: BANKED 2, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.

func_8005EAE8's agent resolved its own integration conflict — it adopted the
TU's declarations for func_8005DCA0 and D_80072974 and cast at the use site
rather than declaring its own incompatible externs.
func_8005E13C reproduces a trailing orphan pad nop (belonging to neither it nor
SysEnqIntRP) with a file-scope __asm__; the verbatim detector correctly does
NOT flag that as a §265 body.
2026-09-03 20:15:17 -06:00
Drew T a0c855648c feat(decomp): bank ov_SC01_084:func_80182A00 (§378 chain, 207 ins)
harvest_verify: verified 1 / failed 0, ef86fe1e403998a82ead42f4466ac4bc80f2c8d1
BYTE-IDENTICAL. The static probe had called this a `local_type' Blk16 conflict;
the real gate strips TU-provided typedefs and then named the true blocker.
2026-09-03 19:42:05 -06:00
Drew T 534979b4e7 plumb(ov_SC01_084): §378 self-caller casts for func_80182A00
harvest_verify named the step-2 signature exactly: `too few arguments to
function func_80182A00' at ov_SC01_084_jr_80182A00.c:534. 4 call sites cast.
Baseline green with NO draft substituted: ef86fe1e403998a82ead42f4466ac4bc80f2c8d1.
2026-09-03 19:41:45 -06:00
Drew T 337a040047 feat(decomp): bank main:func_80024054 via permuter ILS (DELAY-SLOT/2, 4 of 91)
Score 0 on cycle 1. gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:39:54 -06:00
Drew T a614d972c2 feat(decomp): bank main:func_80040DE8 via permuter ILS (REGALLOC-PERM/$t1>$v1, 2 of 347)
Score 0 on cycle 1. gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:28:55 -06:00
Drew T 94b528b54e feat(decomp): bank main:func_80021174 via permuter ILS (SCHEDULE-REORDER/2)
The residual was a two-instruction adjacent swap in the target's favour:

    idx 49  MINE lh   $a1, 0($sp)      TARGET sra $a2, $v1, 16
    idx 50  MINE sra  $a2, $v1, 16     TARGET lh  $a1, 0($sp)

Hand lever tried first and REFUTED by bytes: hoisting `a0 = a0 >> 16` above
the load is semantics-preserving (a0 is untouched in between) but scores
23 mismatched at 67/68 ins — it lets gcc fold an instruction away entirely.

permuter_ils --klass SCHEDULE reached score 0 on cycle 1. Its winning edit is
a clean C-level one: drop the `a1 = *(s16 *)sp;` temporary and inline the load
into both comparisons, which is what moves the sign-extend ahead of it.

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:28:06 -06:00
Drew T c91c9dffee feat(decomp): parallel gate — 12 fns across 12 binaries (8 workers)
ov_SC03_111    func_80181344
  ov_SC01_006    func_8017FBCC
  ov_SC02_041    func_801832F8
  ov_SC03_124    func_8018095C
  ov_SC01_005    func_8017FBCC
  ov_SC04_002    func_80182CBC
  ov_SC03_105    func_8017F018
  ov_SC04_005    func_80185CEC
  ov_SC04_007    func_80182358
  ov_SC04_011    func_8018985C
  ov_SC05_018    func_80181294
  ov_SC05_003    func_80181720
2026-09-03 19:18:12 -06:00
Drew T 3c34f8f4a3 plumb(overlays): §378 self-caller casts + decl sync for 12 self_decl_tu drafts
The same class that produced four banks in main, applied across the overlay
fleet. `blocker_probe` over all 26 binaries holding a stranded S76 draft found
11 whose blocker is `self_decl_tu`; harvest_verify named a twelfth
(ov_SC03_111:func_80181344, `conflicting types for func_80181344').

    ov_SC01_005 func_8017FBCC     ov_SC04_005 func_80185CEC
    ov_SC01_006 func_8017FBCC     ov_SC04_007 func_80182358
    ov_SC02_041 func_801832F8     ov_SC04_011 func_8018985C
    ov_SC03_105 func_8017F018     ov_SC05_003 func_80181720
    ov_SC03_111 func_80181344     ov_SC05_018 func_80181294
    ov_SC03_124 func_8018095C     ov_SC04_002 func_80182CBC

35 edits, 0 refusals. cast_self_callers is binary-generic — only sync_tu_decls
is main-only — so the checkpoint's "extend it or drive recover_integration per
binary" needed neither.

Every one of the 12 binaries was baseline-checked with NO draft substituted and
all 12 build their locked SHA, so the casts move zero bytes fleet-wide, exactly
as they did in main.
2026-09-03 19:15:32 -06:00
Drew T 04d9d28bb6 feat(decomp): bank ov_SC06_032:func_8017D810
Verified in-tree by harvest_verify, final SHA af117efbe4c0142d204bd243e41fd53e6ea5e350
BYTE-IDENTICAL.

Notable because parallel_gate had just reported `banked 0` for this exact
binary and this exact draft dir, in a 106s worker run — see the follow-up
investigation. The in-tree gate is the one that agrees with the bytes.
2026-09-03 19:13:28 -06:00
Drew T ef0cb64c14 plumb(main): undo-journal the plumbing for the 3 drafts that did not bank
`cast_self_callers --undo-journal .run/S77_selfcast.json --keep
func_80013154,func_8005EAC8,func_8005E3AC,func_8005E79C` — reverted 6 edits
across 2 files, kept the 4 that banked.

The three reverted are func_80015608, func_80015760 and func_80039DEC, all
proven NEARs (closeness 3, closeness 9, and 8 differing bytes at 0x80039ded
respectively) — body residuals for the DIFF lane, not plumbing. Their §378
chain is one command to regenerate when a corrected body arrives.

main rebuilds 143dbb89f34491258bbc27810d0a12ec8b43a8dd after the revert.
2026-09-03 18:58:06 -06:00
Drew T a8aa670d96 feat(decomp): bank func_8005E79C — both sync_tu_decls fixes proved
round 1: D_80072978    -> extern s32 (*D_80072978)(void);
    round 2: func_8005E804 -> extern void func_8005E804(u8 *arg0);
    BANKED func_8005E79C after 2 declaration syncs

Round 1 is the ordinary extern path; round 2 is the definition path added in
commit:3807, and the draft could not have been reached without it. The same draft
had previously reported "no declaration conflict named" — that was the gate
refusing on a dirty tree, which is the misattribution the second fix removes.

The TU spells D_80072978 as a pointer-to-function; the draft had guessed `s32`
and cast at the use site. The TU's spelling is authoritative and the cast still
folds, so the bytes are unchanged.
2026-09-03 18:57:31 -06:00
Drew T 442b3ce651 feat(decomp): bank func_8005E3AC — a definition is a declaration
sync_tu_decls looked only for an `extern … sym …;` line, so when the clashing
symbol is a function the TU DEFINES it reported

    stopping: func_8005E480 clashes with the TU itself but src/800c3.c has no
    `extern` line to copy.

and gave up, though the authoritative spelling was sitting in the definition's
own header at src/800c3.c:916. That was the terminal blocker of BOTH remaining
self_decl_tu drafts. tu_decl now reads a definition header and renders it as an
extern, preferring it over an `extern` line when both exist — it is the one cc1
checks every other declaration against.

    round 1: func_8005E480 -> extern void func_8005E480(void *arg0);
    BANKED func_8005E3AC after 1 declaration sync

Checked against cases whose answer was already known before trusting it:
definition path OK on func_8005E480 and func_8005E804, extern path still
verbatim on func_8005D734, absent symbol still None.

progress.py main: REAL 897 -> 898, INCLUDE_ASM stubs 44 -> 43.
2026-09-03 18:55:20 -06:00
Drew T a9980bdd8c feat(decomp): bank func_80013154 and func_8005EAC8 in main (§378 self-decl chain)
The first two banks off the `self_decl_tu` class: the TU declared the very
function the draft defines, with a different signature, so the draft could not
compile no matter how correct its body was.

  func_80013154  src/800.c    tu s32 (s32,s32,s32)  | def s32 (s16,s16,s16)
  func_8005EAC8  src/800c3.c  tu void (void)        | def void (void*)

func_80013154 was a §265 VERBATIM-ASM bank — it is now real decompiled C.

gate_main: 3-draft slate, bisected in 5 rebuilds, 2 banked,
143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.

progress.py main: REAL 895 -> 897, INCLUDE_ASM stubs 46 -> 44.

Rejected by the byte gate, correctly, and handed to the DIFF lane:
  func_80039DEC  8 differing bytes at 0x80039ded  (a NEAR, not a plumbing miss)
  func_80015608  sync_tu_decls refused up front: NEAR at closeness 3
2026-09-03 18:53:15 -06:00
Drew T cb1b6fc9fb plumb(main): §378 self-caller casts + decl sync for 7 self_decl_tu drafts
`blocker_probe --binary main` over the 36 stranded S76 drafts classifies 7
whose blocker is `self_decl_tu` — the TU declares the very function the draft
defines, with a different signature:

    func_80013154 src/800.c    tu s32 (s32,s32,s32)   | def s32 (s16,s16,s16)
    func_80015608 src/800.c    tu void (s32,s32)      | def void (void*,u32*)
    func_80015760 src/800.c    tu void (s32,s32)      | def void (Obj*,s32*)
    func_80039DEC src/800_c.c  tu void (void*,s16,u8) | def void (void*,s16,s16)
    func_8005E3AC src/800c3.c  tu void ()             | def s32 (Ctx*,s32)
    func_8005E79C src/800c3.c  tu void ()             | def s32 (void*,void*)
    func_8005EAC8 src/800c3.c  tu void (void)         | def void (void*)

14 edits: each call site cast to a no-proto function pointer (§20 — gcc-2.7.2
folds the cast of a known function symbol back to a direct `jal`, so the
caller's bytes do not move), then the forward declaration synced.

Verified byte-neutral BEFORE any draft is substituted: main builds
143dbb89f34491258bbc27810d0a12ec8b43a8dd with these edits alone.

Committed ahead of the gate because gate_main `git checkout`s main's TUs
before substituting and would otherwise destroy these edits. Journal at
.run/S77_selfcast.json — `--undo-journal --keep <banked>` follows the gate.
2026-09-03 18:49:56 -06:00
Drew T 1b648fcc5b Revert "plumb(main): §378 self-caller casts + decl sync for 7 self_decl_tu drafts"
This reverts commit commit:3801.
2026-09-03 18:48:28 -06:00
Drew T 54717c4b62 plumb(main): §378 self-caller casts + decl sync for 7 self_decl_tu drafts
`blocker_probe --binary main` over the 36 stranded S76 drafts classifies 7
whose blocker is `self_decl_tu` — the TU declares the very function the draft
defines, with a different signature:

    func_80013154 src/800.c    tu s32 (s32,s32,s32)  | def s32 (s16,s16,s16)
    func_80015608 src/800.c    tu void (s32,s32)     | def void (void*,u32*)
    func_80015760 src/800.c    tu void (s32,s32)     | def void (Obj*,s32*)
    func_80039DEC src/800_c.c  tu void (void*,s16,u8)| def void (void*,s16,s16)
    func_8005E3AC src/800c3.c  tu void ()            | def s32 (Ctx*,s32)
    func_8005E79C src/800c3.c  tu void ()            | def s32 (void*,void*)
    func_8005EAC8 src/800c3.c  tu void (void)        | def void (void*)

14 edits: each call site cast to a no-proto function pointer (§20 — gcc-2.7.2
folds the cast of a known function symbol back to a direct `jal`, so the
caller's bytes do not move), then the forward declaration synced to the
draft's own spelling, which emits no code once the sites are cast.

Committed ahead of the gate because gate_main `git checkout`s main's TUs
before substituting and would otherwise destroy these edits. Journal at
.run/S77_selfcast.json — `--undo-journal --keep <banked>` follows the gate.
2026-09-03 18:45:52 -06:00
Drew T 4d8493aa32 feat(decomp): bank func_8005EC00 by syncing two declarations from the TU
Same §378 class as func_8005EB28, two symbols deep: the draft declared
D_800729DC as void* where the TU says u32, and D_80072974 as void(*)()
where the TU says void(*)(void*). Copying the TU's own extern verbatim for
each, in the order the gate named them, banked it in two rounds.
143dbb89 BYTE-IDENTICAL.

The loop is the §378 chain's essence: ask the gate which symbol conflicts,
copy the TU's declaration into the draft, re-gate, repeat. No judgement
required — the gate names the symbol and the TU holds the authoritative
spelling.
2026-09-03 17:19:33 -06:00
Drew T 820189de4f feat(decomp): bank func_8005EB28 by syncing its extern to the freshly-banked callee
A CASCADE, not a new defect: banking func_8005DE78 earlier this session gave
src/800c3.c a real definition at :690 with signature s32 (s32, s32). The
func_8005EB28 draft still carried extern void func_8005DE78(void *, s32) from
when the callee was a stub, so the TU and the draft now contradicted each
other and the gate refused with a compile conflict.

Fix is the §378 shape by hand: drop the redundant extern (the TU's own
definition is above the splice point) and cast the two call sites to the
banked signature. 143dbb89 BYTE-IDENTICAL.

The general point: every bank CHANGES the declaration environment for every
later draft in the same TU. A draft that was compatible before a bank can be
incompatible after it — the same shape as the rescan-twins-after-every-bank
rule, applied to declarations instead of the twin graph.
2026-09-03 17:17:25 -06:00
Drew T c834366843 feat(decomp): bank 2 more main functions from the real-cc1 MATCH set
The recover_integration probe compiles each stranded draft in its ACTUAL TU
and reported 6 of main's 40 blocked drafts as MATCH there — i.e. not
integration problems at all, just casualties of batch-internal conflicts in
the earlier slates. Gating those 6 alone banked 2 (143dbb89 byte-identical).

The remaining 4 are a finding in their own right: gate_main's resolve_conflicts
pre-check dropped them while real cc1 accepts them. Gating them individually
next.
2026-09-03 17:13:56 -06:00
Drew T fc644dddb4 feat(decomp): bank 9 main functions, including main itself and the 670-ins giant
BANKED 9 of 28 after bisection, 143dbb89 BYTE-IDENTICAL. Verified from the
SOURCE (every stub gone), not from the gate's own count.

  main            509 ins  the game's entry point
  func_800226C0   670 ins  the largest function in the project
  func_800215F4   465
  func_800623A4    36 · func_80062434 36 · func_8005D410 42
  func_8005D4B8    14 · func_8005D4F0 18 · StopRCnt 13

Reached by iterating the gate and dropping the compile-conflict culprit it
named each round: func_8005E79C, func_8005E3AC, func_8005EAE8, func_8001FC08.
Each of those is a §376/§378 declaration conflict, not a bad body — they go to
the recovery chain, not the bin.

Several were only reachable because of this session's oracle fixes: the 800c3
functions had been recorded as §182/§188 epilogue walls by an oracle modelling
maspsx + as -O1 for a TU the Makefile builds through reorder_passthrough +
as -O2. func_800226C0 came from the §476 finding that a hard-register pin
strips nonzero_bits and reg_n_sets==1.
2026-09-03 17:05:47 -06:00
Drew T 5bb71db7a9 feat(decomp): parallel gate — 1 fns across 1 binaries (8 workers)
ov_SC06_020    func_8017D918
2026-09-03 16:49:04 -06:00
Drew T 2eac966cc5 fix(manifest): six §179-C fragments are PERMANENT-VERBATIM, not decompilable
Reverts my six src/800c.c stub conversions from commit:3772 and corrects the
manifest to match the evidence. main still builds 143dbb89.

Each of the six has NO `jr $ra` of its own: it ends mid-basic-block or
tail-jumps into a sibling's label, and the shared lw $ra / addiu $sp / jr $ra
tail lives in the NEXT symbol. gcc-2.7.2 has no sibcall pass and appends an
epilogue to every C function it compiles, so no C spelling can ever match —
cookbook §179-C, which already NAMED func_8005C1C0 as a follow-up.

I converted them anyway on a `rows == 1` filter that meant "the manifest
listed one row", not "this is an independent function", ignoring the
DECOMPILE-AS-PARENT disposition whose whole meaning is "this row is a
FRAGMENT". Three drafting agents then rediscovered §179-C independently, one
citing the very cookbook line naming its own target, before a mechanical
no-jr-$ra sweep confirmed all six at once.

Also corrects func_8017D810 and func_80181828 from UNCERTAIN: both are
handwritten GTE (SQR lane), per agents that transcribed the .s 1:1.

The guard that prevents a repeat shipped in commit:3773.
2026-09-03 15:42:56 -06:00
Drew T 37beb6420b refactor(fleet): convert 11 self-contained verbatim units to stubs
The second tranche: every unit in config/verbatim_manifest.json whose
disposition says decompile-it and whose unit is SELF-CONTAINED (one row, so
the unit_entry is the function itself, not a fragment). 6 in main's src/800c.c
plus func_80185810 (ov_SC03_105, 489 ins), func_8017DC80 (ov_SC07_002, 346),
func_80181E04 (ov_SC01_001, 269), func_80181828 (ov_SC05_005) and
func_8017D810 (ov_SC06_032).

Byte-neutral, verified per binary: main 143dbb89, ov_SC03_105 d305ff6d,
ov_SC07_002 fad71342, ov_SC01_001 a8e49bc0, ov_SC05_005 452897fc,
ov_SC06_032 af117efb.

Checked FIRST that none sits in a LINKED subseg — several carry SDK-shaped
names and a draft written into a linked subseg gates GREEN while wrong.

NOT converted: the 21 multi-row DECOMPILE-AS-PARENT units. Their rows are
FRAGMENTS of a larger unit, and converting a fragment to its own stub would
invite drafting something that is not an independent function. That needs a
parent-unit tool, not a per-function one.

Still skipped: ov_SC03_107:func_8017D878, a deliberate §265 bank per the
cookbook addendum (address-taken use forces a void(void) declaration the real
body contradicts).
2026-09-03 14:54:56 -06:00
Drew T 096fe153cc feat(decomp): parallel gate — 10 fns across 4 binaries (8 workers)
ov_SC02_005    func_8018DFC4
  md_MAIN_003    func_800D0204 func_800D0440 func_800D05B4 func_800D0664 func_800D09A0 func_800D0A7C func_800D0B1C
  ov_SC03_105    func_80180EC0
  ov_SC02_003    func_80187B40
2026-09-03 14:40:01 -06:00
Drew T d7a9f471b4 refactor(fleet): convert 26 DECOMPILE-NOW verbatim bodies to stubs
Every remaining DECOMPILE-NOW row in config/verbatim_manifest.json that was
still a §265 verbatim __asm__ body: main 13 (incl. `main` itself, 509 ins,
in src/boot.c), md_MAIN_003 11, md_MAIN_020 1, ov_SC06_010 1. They were
byte-identical by construction and completely undecompiled, and no gate or
draw could see them — draw_waves reported only 26 drawable stubs fleet-wide
while 27 more sat locked in this form.

Byte-neutral, verified per binary: main 143dbb89, md_MAIN_003 dd1b32ec,
md_MAIN_020 0990e041, ov_SC06_010 05c2d8c4.

SKIPPED ov_SC03_107:func_8017D878. The manifest marks it DECOMPILE-NOW but
the cookbook's §265 addendum documents it as a DELIBERATE verbatim bank: its
only use in the TU is address-taken, forcing a `void f(void)` declaration
the real body contradicts, and no C spelling reconciles them. Two sources
disagree; the one with the byte evidence wins.

md_MAIN_020 and ov_SC06_010 needed --asm-subdir: both are single-TU overlays
with zero INCLUDE_ASM lines left, so there is no prefix in the binary to
derive from. Spelling confirmed against a sibling overlay's own stubs.
2026-09-03 14:02:09 -06:00
Drew T 9992cab319 refactor(main): convert the last 9 DECOMPILE-NOW verbatim bodies to stubs
The 9 SDK functions the verbatim manifest marks DECOMPILE-NOW in src/800c3.c
and src/800c2_2.c were §265 verbatim __asm__ blocks: byte-identical by
construction, undecompiled, and unreachable by every gate in the project,
which splices a draft in place of an INCLUDE_ASM line these did not have.
splat also stops emitting <fn>.s for them, so they had no target asm to
match against either. Byte-neutral: main still builds 143dbb89.

verbatim_to_stub refused three of them — src/800c2_2.c has no sibling
INCLUDE_ASM to copy the subdir spelling from, and all three of its remaining
functions are verbatim, so the file can never grow the sibling the rule
wants. The tool that exists to reach unreachable functions could not reach
them. It now DERIVES the spelling and proves it: the prefix from this
binary's other TUs, the last component from the file stem, which must appear
as a "c" segment in the binary's own splat config — the same file that
decides where splat writes the .s. Still refuses when either half is
unproven; --asm-subdir is the explicit override.

The S75 checkpoint recorded this group as "20 of 21 banked, one bisection";
counted from src/, it is 9 outstanding, corroborated by an independent count
from config/verbatim_manifest.json.
2026-09-03 13:05:11 -06:00
Drew T 261b8a4fd3 feat(decomp): bank 20 SDK-C-REORDER functions — the "§332/§188 wall" was the reorder island
BANKED 20 of 21 after bisection in 11 rebuild(s)
    143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL
    rejected: ['func_8005E13C']

src/800c3.c INCLUDE_ASM stubs 36 -> 16. These are libapi/libcard C functions
that had been banked as §265 verbatim __asm__ blocks and were unreachable by
every gate (a verbatim body has no INCLUDE_ASM to substitute).

The chain that unblocked them, all this session:
  * the triage identified the class and showed the "§332/§188 wall" is the §332b
    -O2 reorder island, which landed 2026-09-01 -- one day BEFORE four of these
    were banked as assembly, with "6 of 53 at closeness 0" drafts in hand;
  * REORDER_TUS was extended to 800c2_2/800c2_3 ($(filter) is an exact stem
    match, so 800c2 never covered them), proven byte-neutral by --assert-baseline;
  * verbatim_to_stub converted 20 bodies back to stubs, byte-neutral;
  * gate_main was fixed twice -- it destroyed uncommitted work, and my own first
    guard sat inside the bisection loop where it tripped on the gate's own
    substitution.

Drafts were already on disk from waves m04-m16 and s67m1; not one needed
redrafting. This is the §451 lesson paying out: the evidence was recorded, and
what was missing was a tool able to reach it.
2026-09-03 12:12:06 -06:00
Drew T f0eea33381 refactor(main): convert 20 SDK-C-REORDER verbatim bodies to INCLUDE_ASM stubs — byte-neutral
These are libapi/libcard C functions in src/800c3.c that were banked as §265
verbatim __asm__ blocks. The triage (.run/S75/triage/report.md) established they
carry the §188 shape (`jr $ra` with `addiu $sp,$sp,+N` in the slot) and that the
"§332/§188 wall" is the §332b -O2 reorder island, which landed 2026-09-01 --
one day BEFORE four of them were banked as assembly, with the commit itself
recording "6 of 53 at closeness 0" stored drafts.

Converting them to stubs makes them reachable by every gate again (a verbatim
body has no INCLUDE_ASM to substitute, so gate_main drops it as "resolved to NO
stub") and is the honest accounting: a stub counts as OUTSTANDING WORK, a
verbatim body counted as banked.

BYTE-NEUTRAL, PROVEN: make extract + make build BINARY=main ->
143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL. INCLUDE_ASM pastes the
same assembly the block transcribed, so it must be -- but "must" is a claim and
this was gated, not assumed.

Metric moves honestly: VERBATIM __asm__ bodies 173 -> 148, INCLUDE_ASM stubs
34 -> 53.

20 of 24 converted. The 4 refusals are reported, not silent: func_800623A4 /
func_80062434 / func_8006252C (800c2_2) and func_8005D8A0 -- the last being the
row all three of asm_in_c's detectors missed, which is its own finding.

Also fixes verbatim_to_stub to CASE-NORMALISE the address. splat's convention is
func_%08X but analysis artifacts carry lowercase (the triage taxonomy does), and
asking for func_8005ed4c found 0 of 24 blocks that were all sitting right there.
An address is a NUMBER; matching it as a case-sensitive string is R48 in its
case-sensitivity form.
2026-09-03 10:44:32 -06:00
Drew T a68197b32d feat(decomp): SaveLoadRoutine DECOMPILED to real C — 1,179 ins, and the §434 wall was a symbol boundary
The largest open function in the project, carried as the §434 WALL since Phase
31 opened, is now real C. main SHA1 143dbb89... BYTE-IDENTICAL.

THE WALL WAS NOT A PROPERTY OF THE CODE. A whole-binary census of every .s for
the interior labels and raw addresses 0x8002B154..0x8002C31C found EXACTLY TWO
sources, and both are func_8002B0B4 itself: its own beq/j to .L8002C2A8 /
.L8002C2AC / .L8002BFE4, and its own jtbl_80072E44 (36 entries, entry 0 =
0x8002B154). Nothing else in the binary references the range.

So func_8002B0B4 (40 ins, prologue + dispatch) and SaveLoadRoutine (1,139 ins,
epilogue) are ONE function sharing one 0x40 frame -- entry func_8002B0B4, five
overlay callers, all s32 f(s32, s32, void *). SaveLoadRoutine is `case 0:` of
its state switch; .L8002C2A8/.L8002C2AC are the switch exit and .L8002BFE4 the
outer `case 1:` body. The "no epilogue of its own / must stay file-scope
__asm__" note that parked this for a phase was describing a SPLAT SYMBOL
BOUNDARY, not a code structure. The predicted "middle path" (decompile one while
siblings stay asm) was moot: there are no siblings.

The three "save/load handler code pointers at saveHeaderTemplate+0x54" in
docs/memory-map.md are jtbl_80072E44[0..2] -- confirmed against
dumps/ram_savescreen.bin (0x80072E44/48/4C = 0x8002B154/1AC/BEA4). The S73
correction to that row is right; no further RAM capture was needed.

Verified three ways: match_one MATCH (1179 ins) on a merged target .s; `make
extract && make build BINARY=main` -> 143dbb89...; and gate_main's own
clean_build() sequence driven from Python -> "sha1 143dbb89... == check.us.sha
(BYTE-IDENTICAL)". Independently re-checked here: tools/asm_in_c.py reports
NEITHER symbol as assembly-posing-as-C, so this is genuine C (the 94 __asm__
occurrences in the TU are §3a zero-byte cross-jump barriers, which are C).

TWO NEW TOOL DEFECTS, logged not fixed (main is busy; next session):
  * gate_main.py:710 runs `git checkout -- <main TUs>` immediately BEFORE
    substitute(). For a function whose current form is a hand-written __asm__
    block that restores the block NEXT TO the C, the TU then carries 9 jump
    tables instead of 5, and gate_main REJECTS a byte-identical bank. It cannot,
    by construction, bank anything in the verbatim class.
  * gate_main's error filter (error|undefined|conflict|...) does not match
    jtbl_rodata_pads' sys.exit refusal, so that failure shows only warnings.

HAZARD, and why this is committed immediately (R42): any gate_main run on main
wipes this bank via that same line 710.

Ten measured levers for the body are in .run/S75/slr_c/cookbook_448.md pending a
cookbook merge, headed by: when a frame check says "no prologue / no epilogue",
build the MERGED .s and decompile the pair as one function before excluding
anything.
2026-09-03 01:06:22 -06:00
Drew T 70de5a8611 feat(tools): verbatim_target_s.py — the 147 asm-posing-as-C functions are workable again; bank func_8017DB98
THE BLOCKER. asm_in_c.py found 147 GAME functions that are §265 verbatim
__asm__ bodies. NONE of them could be worked on: splat emits
asm/nonmatchings/<subseg>/<fn>.s only for functions that are still INCLUDE_ASM
stubs, and a verbatim body is not a stub -- so splat stops emitting its .s,
while match_one and rtu_match BOTH consume one. Measured: 1 of 147 had a target
on disk. The class was unworkable because the information was in the wrong FORM,
not because it was missing.

verbatim_target_s.py regenerates a splat-format target .s from the EXTRACTED ROM
IMAGE -- never from the __asm__ block in our own source, because the block is
the thing under test and a target derived from it would agree with the candidate
by construction (R34). 146 of 147 emitted; the 1 refusal is REPORTED.

TWO DEFECTS CAUGHT BY CHECKING AGAINST A KNOWN-TRUE CASE, both of which would
have shipped ~147 silently-wrong targets:

  * BYTE ORDER. splat writes the four bytes as they sit in the image
    (`C8FFBD27` for instruction 0x27BDFFC8) and masked_diff.insns_from_s reads
    the column with struct.unpack("<I", bytes.fromhex(...)). objdump prints the
    VALUE, so reversing double-swaps: 91 of 1139 words agreed with splat's own
    .s for the same function. The LENGTH matched perfectly, so nothing except a
    word-level cross-check could have caught it.
  * `-z` / --disassemble-zeroes. objdump ELIDES runs of zero bytes as `...`, and
    a MIPS nop IS 0x00000000 -- so every nop vanished. func_80049610 (three
    nops) disassembled to ZERO instructions; func_80047D3C 31 of 36. The length
    assertion caught all of them, which is the only reason this was not shipped
    as ~30 quietly-truncated targets.

Verification: regenerated SaveLoadRoutine target is 1139/1139 words IDENTICAL to
the .s splat itself emitted for the same function.

ALSO BANKED: ov_SC06_025:func_8017DB98 (122 ins). Its body was byte-exact on
disk since S71 and the blocker was one word: the TU declared
`extern void func_8017DB98(s32, s32)` where the epilogue is `addu $v0,$s3,$zero`
-- must be `extern s32`, and the caller discards the result so the change is
byte-neutral. That line number and fix were recorded in the agent journals the
whole time; frontier_classify only surfaced it once journal_notes was wired in
as a second oracle earlier this session.
2026-09-03 00:18:49 -06:00
Drew T f5f4c2eeec feat(decomp): bank func_801806F8 + func_80180ABC (498 ins) + frontier_classify reads the journals
Two banks from the S75 redraft workflow (7 overlay functions, one agent each,
every claimed MATCH re-verified by an independent agent instructed to refute
it). Both were carried as F-FAR "a draft exists but is materially wrong":

  func_801806F8  ov_SC03_105  241 ins   (recorded closeness 235)
  func_80180ABC  ov_SC03_105  257 ins   (recorded closeness 250)

Neither needed a better model. Both needed the recorded closeness not to be
believed -- see below.

frontier_classify.py, THREE fixes, each caught by testing against a case whose
answer was already known:

1. BEST closeness, not LAST. .run/backlog.jsonl is append-only, one row per
   attempt across every lane and session, so the last row is evidence about
   THAT LANE'S SEED, not about the function. Caught func_80180B3C (best 125,
   last 287) and moved func_80181294 from "redraft" to "permuter" (best 19).
   The draft that ACHIEVED the best score is kept, not the last one written.

2. journal_notes.py wired in as a SECOND, DISAGREEING oracle (R34). The backlog
   does not have what the agent journals have. Measured on func_8017DB98:
   backlog best == last == 115, so best-vs-last could not help, while the
   journal holds "Attempt 2 (MATCH · closeness 0) ... MATCH 122/122 ... BANK
   BLOCKER is TU plumbing, not the body (§376/§378)" WITH the draft path and the
   exact declaration to change. Reclassified 37 functions; G-DRAFTED-UNKNOWN
   fell 47 -> 10 and a new C-PLUMBING class holds 16 functions / 1,547 ins whose
   BODIES ARE PROVEN and are blocked only by the TU.

3. A consuming-regex bug in my own extractor -- the session's signature defect,
   committed a third time in the tool written to find it. The first cut used
   `re.finditer(r'\*\*Attempt \d+\*\* \(([^)]*)\)(.{0,400})', ..., re.S)`, whose
   400-char body window SWALLOWS THE NEXT ATTEMPT'S HEADER, so every record
   following another was invisible. On func_8017DB98 it hid attempts 2 AND 6,
   both `MATCH · closeness 0`, and returned attempt 1's NEAR (2) as the best --
   exactly the records the oracle exists to find. Now splits on the marker
   rather than consuming past it. A regex that consumes an unbounded body cannot
   enumerate the items after the first.

Rows now carry attempts, closeness_last, journal_closeness, and a
!!WARMSTART-REGRESSION flag when a later attempt scored materially worse than
the best -- the shape a wave's warm-start regression makes, which from inside
the wave is indistinguishable from an unsolved function.

Gate ledger for the batch of 7: 2 banked, 3 near, 2 failed. func_800CB00C failed
despite being adversarially upheld -- it owns a jump table, and both matchers
compare .text only, so a verified .text MATCH proves nothing about table
placement (the agent's own write-up says so).
2026-09-03 00:12:07 -06:00
Drew T 1bac13b664 fix(jtbl): the pad walk cannot see a verbatim-asm rodata block — SaveLoadRoutine banks (bytes, not a decompile)
tools/jtbl_rodata_pads.py --derive walks a TU's rodata emission against the
retail island and validates only what it can SEE. A §265 verbatim-__asm__ body
emits its tables as `.section<TAB>.rodata` + `jtbl_xxxxxxxx:`, and the walk
missed BOTH spellings:
  * the rodata directive was matched as the literal one-space string
    ".section .rodata" / ".rdata", so a tab-spelled directive never entered
    rodata at all;
  * inside rodata the anchor regex accepted only `D_xxxxxxxx` (the S74 dlabel
    fix was one prefix short), so a `jtbl_xxxxxxxx:` label was invisible.

Consequence, traced: the walk skipped the block as if it were .text, every
later C table walked 104 bytes behind its retail address, EVERY WORD in that
range happens to be a valid code address so the entry guard never fired, and
the walk stopped short of the island's single zero word -- so the one trailing
pad was never emitted and the image linked 4 BYTES SHORT. That produced 3,989
differing bytes on a body the verdict layer had already called byte-identical.

Fixed: tokenised directive match (.rdata / .section .rodata, tabs and commas);
anchors keyed on the ADDRESS IN THE NAME for `D_` or `jtbl_`, with an
address-suffixed label of any other prefix now REFUSING loudly (R43) instead of
becoming a silent hole; and `.align N` modelled SECTION-RELATIVE from the walk
origin, as `as` does -- needed for `.align 3` when a section starts = 4 mod 8,
which span B (0x80072E44) does.

Negative control: old vs new derive over ALL 162 md_*/main derive-path TUs ->
160 byte-identical post-derive streams with identical exit codes, 0 DIFF; 2 SKIP
(800c2/800c3 are REORDER TUs with no derive stage).

main SHA1 143dbb89... BYTE-IDENTICAL, 413,696 bytes, cmp identical to retail.

WHAT THIS IS NOT. SaveLoadRoutine is banked as a §265 verbatim __asm__ block --
BYTES, NOT A DECOMPILE. Its 1,165 instructions are byte-correct and unexplained.
tools/progress.py correctly REFUSES to count it, reporting `UNPLACED (parse
hole)` rather than inflating REAL (which moved 880 -> 881 on func_8005DCA0
alone). Two such blocks already exist in this TU, documented as necessary
because those functions have no epilogue and fall into shared tails. A real C
decompile is now being attempted separately; this commit is the revertible
byte-green base for it.
2026-09-02 23:34:01 -06:00
Drew T 23cd3a43a5 feat(decomp): bank main:func_8005DCA0 (118 ins) — and SaveLoadRoutine's body is BYTE-IDENTICAL
One clean-rebuild gate_main pass over main's stored drafts. 35 drafts on the
slate -> 20 compatible after in-TU declaration resolution -> 1 byte-correct,
found by bisection in 24 rebuilds. main SHA1 143dbb89... BYTE-IDENTICAL.

  banked: func_8005DCA0  src/800c3.c  118 ins

THE HEADLINE IS NOT THE BANK. gate_main's per-function verdicts separate a BODY
reject from a PLUMBING reject, and they say:

  SaveLoadRoutine: PLUMBING REJECT — SaveLoadRoutine is BYTE-IDENTICAL; all 3989
  differing bytes are ELSEWHERE IN CODE. The substitution perturbed other
  functions (§376 — a stale forward declaration changes caller codegen).

SaveLoadRoutine is 1,165 instructions -- the largest function left in the
project, 9.2% of everything remaining, and carried as the §434 WALL. Its body is
already correct. What rejects it is a forward declaration perturbing OTHER
functions' codegen, which is exactly what fix_arity_callers -> cast_self_callers
exist to repair. That is a plumbing job, not a matching job.

Three genuine BODY rejects, correctly distinguished by the same verdict layer
(divergence confined to the function itself): func_8001EFE0 (495 bytes),
func_80015B6C (151), func_80011380 (8). Those drafts are really wrong.

Honest read of the yield: 1 of 20 substituted drafts was byte-correct, so main's
stored drafts are mostly NOT right. This tempers the "the endgame is integration,
not drafting" line from the previous commit -- true for the overlays, only
partly true for main, where several drafts need redrafting or permuter work. The
distinction is now measured per function rather than assumed.

Deferred to the reconcile chain, not discarded (11 dropped for in-TU decl
conflict + 4 dropped across rounds to let the TU compile at all): func_800226C0
(670), func_800215F4 (465), func_8001FC08 (400), func_8005F290 (61) and the
gate's own 11. All drafts remain on disk.
2026-09-02 22:51:29 -06:00
Drew T abea9f0ac2 feat(decomp): bank func_8016AE5C (85 ins) + frontier_classify — the frontier is not a drafting problem
func_8016AE5C (ov_SC03_108) was logged "match_one MATCH but the whole-binary
gate rejected -- CAUSE NOT DETERMINED". Determined: the body is byte-perfect (0
differing words inside the function; all 1,168 diffs are uniform +0x20 shifts
outside it) and it emits an 8-entry jump table that was never carved. It banked
unchanged the moment the §446 jtbl_carve per-table bound landed.

tools/frontier_classify.py (NEW) — classify every open stub by its TRUE BLOCKER
from artifacts already on disk (R33/offline-tooling-first: zero tokens, no
agents, no builds). "69 functions left" is a stub count, not a difficulty
measure, and routing drafting agents at carve or plumbing problems wastes them.

    A-TWIN-REMAP   3    302  a byte-identical copy is already banked elsewhere
    B-CARVE       11  3,301  owns a switch jump table -> the §446 class
    D-NEAR         2    106  closeness <=25 -> permuter fuel, not drafting
    F-FAR          3    223  draft materially wrong -> redraft
    G-DRAFTED-UNK 49  8,724  drafted before, no usable verdict on record
    H-VIRGIN       1      1  never drafted (and it is a DATA BLOB, not a function)

68 of 69 remaining functions already have a draft on disk. The endgame is a
verification/integration problem, not a drafting one.

TWO SELF-INFLICTED DEFECTS FOUND BY CHECKING AGAINST KNOWN-TRUE CASES, both the
session's recurring shape (a scan narrower than the claim it supports, R32):
  * The sig directory is NOT the fleet. Alongside the 213 real binaries `.run/`
    holds `SLUS_007.26` (a STALE duplicate of main under the ROM filename),
    `resident_image`, and two CROSS-BUILD binaries (`sep8_SLUS_007.26`,
    `aug31_USA_DEMO.EXE`). Counting them as peers reported 38 fns / 7,516 ins of
    free twin-remaps -- mostly main "already banked" in ITSELF, the rest proven
    in a PROTOTYPE that R13 forbids as evidence. Now derives the fleet from the
    Makefile and prints what it ignored. True figure: 3 fns / 302 ins.
  * The draft scan globbed `.run/S7*` only, missing `.run/S69m2`, `.run/S68m1`,
    `.run/s67m1`, `.run/wave_ds2`, `.run/gate_lane`, `.run/backlog_drafts`. All
    32 drafted main functions read as "never drafted", which would have sent
    agents to redraft 6,328 instructions that already have drafts. Now one
    pruned os.walk of .run (worktrees excluded -- 7.4 GB of duplicate sources).

Honest negative result: resident:func_800D06E8 (344 ins) did NOT bank. I
predicted the carve fix would clear it; it did not. Its blocker is still open.
2026-09-02 22:35:50 -06:00
Drew T cb948a6bbc feat(decomp): the ov_SC01 reloc-only cluster + its 5th latent victim — 5 fns, 1,301 ins
S74 handed this forward as "1,116 instructions behind one question": family_remap
on ov_SC01_004/005/006/008 gated DIFF 4/4 against the banked exemplar
ov_SC01_009:func_8017EB08, and the class had been carried as a codegen wall since
S70. The four bodies were byte-identical to the exemplar the entire time.

Word-level classification vs the exemplar, computed independently twice (a Fable
agent's script, then mine from scratch against the retail images), identical:

    nins=279   EQ 213 · RELOC-HI16 23 · RELOC-LO16 24 · INTERNAL-J 19 · CODEGEN 0

Zero register-allocation, instruction-selection or scheduling differences.

ROOT CAUSE — tools/jtbl_carve.py reserved ONE WORD TOO MANY per table:
  * spimdisasm runs an island's LAST `jtbl_` dlabel one word into the following
    NON-ZERO data (string bytes 0x696F760A / 0x000013FF / 0x62647020), so the
    zero-word trim cannot see it; and
  * the over-span clamp that would have caught it was guarded by
    `len(sltiu_bounds) == 1` -- but `sltiu` is ALSO how gcc emits an unsigned
    range check ((u32)(x-lo) < n, I1). These four carry five distinct sltiu
    immediates, so the guard silently disabled itself on precisely the functions
    that needed it.
  0x2C reserved for a 0x28 table => image 4 bytes short => ~850 %lo immediates
  shift => whole-binary DIFF about a function whose own bytes are perfect.
Fixed with a PER-TABLE bound: gcc-2.7.2's dispatch is a fixed idiom, so the
`sltiu` nearest ABOVE that table's own %hi(jtbl_X) is unambiguous whatever else
the function tests. Second defect stacked behind it: a carve span whose
JTBL_PADS line lacks a `tables=` comment lost its existing table's start on
merge and refused "table starts do not fit the span" -- which harvest_verify
then "repaired" with a needless jr_isolate_all that walked back into the first.

THE NEGATIVE CONTROL IS THE STORY. Run over every other open table-bearing stub
fleet-wide, the fixed bound changed exactly one more table: ov_SC06_022/
func_80185B80 (185 ins), a FIFTH victim nobody had drafted against. A guard that
disables itself on a common idiom does not fail once -- it fails quietly across
the whole corpus.

Banked, each with its own byte-gate verdict (--no-propagate, clean re-gate):
  func_8017EB30  ov_SC01_004  279
  func_8017F2D4  ov_SC01_005  279
  func_8017F2D4  ov_SC01_006  279
  func_8017EC68  ov_SC01_008  279
  func_80185B80  ov_SC06_022  185

Also here:
  * dedup_propagate: memoize find_site's mask (lru_cache) -- 54 ms of masking
    per call over the whole source, recomputed though it depends only on the
    text. 2x on that loop (58.3 -> 33.0 ms/call), NC identical on 120 addrs.
    Scoped honestly: that loop is ~2.4 min of a 30-min run; the profiler puts
    43% in family_remap._alias_decl_for, which is NOT fixed here.
  * Makefile: `clean` says out loud that BINARY= is ignored and it is fleet-wide
    (cookbook §445) -- it silently deleted asm/ for all 213 binaries this session.
  * Cookbook §446 (the carve law: when a standalone-MATCH jtbl draft gates DIFF,
    diff the carve extent against 4 x sltiu before touching the body), §445, and
    SETUP rows for both tools (R21).
  * CURRENT_PHASE: the S75 log, incl. the measured fleet dedup-hygiene census
    (~2,073 fns / ~12,116 items, all ALREADY MATCHED -- cleanup, not work) and
    Drew's decision to leave it and gate --no-propagate from here.
2026-09-02 22:15:20 -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