Commit Graph

3782 Commits

Author SHA1 Message Date
Drew T 978a086ebb docs(cookbook): §469 — the MEM_IN_STRUCT_P alias unlock; §463's spill law confirmed independently
From the S76 func_80039308 agent (518 ins, 402 -> 154, length exact).

Writing a varying-address load as a struct member (((VMask*)q)->w rather than
*(u32*)q) sets MEM_IN_STRUCT_P, which lets true_dependence prove the load
cannot alias a scalar-global store. Both loads hoist above both stores and
three load-delay nops vanish — semantically identical C, different alias
info.

It also needed a 16-byte s16 sav[8] memory local because reload rounds every
spill slot to BIGGEST_ALIGNMENT=8 — the same law §463 derived from alter_reg
on a different function via a different agent that had not seen it. Two
independent derivations, and a second use for the law: it tells you when a
stack layout can only come from a declared local, never from spilling.
2026-09-03 16:07:52 -06:00
Drew T 823c6c798d docs(cookbook): §461 addendum — a register pin can be the defect too
main:func_80040DE8 went 86 -> 2 when §76 variable-reuse pushed o1 off $a3
onto $t0, which made the §3-C pin unnecessary — the pin had been tying ~30
instructions into $t0.

That completes a trio: a volatile launder (§461), a temporary (§462 lever 3)
and now a hard-register pin can each be the thing holding a match back.
Before adding a lever, check whether an existing one is what you are
fighting.
2026-09-03 16:06:20 -06:00
Drew T 0caf4e5c20 docs(cookbook): §468 — the %lo-fold extends to stores; masking hid a wrong operand order
From the S76 func_80181E04 agent (269 ins -> MATCH):

  1. §18's %lo-fold applies to STORES only when the symbol is declared
     extern Struct SYM[] (stride 0x50, field at +0). On a plain s32[] it
     folds for read-only symbols only — worth 13 ins here, and a real
     extension of the Phase-20 entry, which only exercised the read side.
  2. Relocation masking can HIDE a wrong operand order: the reversed
     comparison scores identically under match_one because §1c masks
     HI16/LO16 and both symbol refs mask to the same bytes. When a compare's
     operands are two different symbols the byte oracle cannot tell them
     apart — read the relocations.
  3. No biased q pointer (write off p so combine_givs picks p+0x12, else it
     mints a second anchor, +2), and keep the counted i<0x100 loop (spelling
     the bound via D_801F2A44 costs 12 ins for the same resolved address).
2026-09-03 16:06:03 -06:00
Drew T b143edb84a docs(cookbook): §467 — global-alloc ties break on declaration order; copied clobber lists cost instructions
From the S76 func_8001EFE0 agent (468 ins, 172 -> 89):

  1. When equal-priority pseudos tie in global-alloc, DECLARATION order
     breaks the tie, not assignment order — worth 36 ins here, and it changed
     control flow too (a spilled base made an arm's reload break the tail
     jump2 had been cross-jumping), so re-check branch shape after using it.
  2. A clobber list copied from a neighbour is a liability: a phantom "$2"
     clobber evicted abr from $v0 and cost 14 ins, where the real macros
     clobber only $12/$13/$14. Verify the list, not just the body.
  3. convert_to_integer shortens a narrow-looking sum to QImode and drops its
     andi; an explicit s32 temp for the sum restores it.
2026-09-03 16:05:01 -06:00
Drew T 3eea600c2a docs(cookbook): add §464, which the previous commit's message described but did not contain
The §464 append was lost to a git index-lock race: the commit landed with a
message documenting four levers from func_8005DE78 while the file held only
§465 and §466. Caught by grepping the file for each section instead of
trusting the commit I had just written.

Content unchanged from the agent's report: a volatile QI/HI load preserves
the zero-extend as its own andi; ||-vs-&& selects do_jump's drop-through arm;
a volatile STORE can never be stolen into a delay slot (resource_conflicts_p
returns 1 on any volatil resource), which is how to force a target nop after
a j; and a "memory" clobber vs a volatile read are not interchangeable
CSE-breakers — both reload the index, only the clobber leaves the addu
operand order intact.
2026-09-03 16:03:21 -06:00
Drew T 66bf1ebe62 docs(cookbook): §464-§466 — volatile levers, the ASPSX slot-hop gap, and main's -O0 address law
§464, from func_8005DE78 (141 ins -> MATCH): a volatile QI/HI load stops
combine folding the u8->s32 promotion into the lbu; ||-vs-&& selects
do_jump's drop-through arm; a VOLATILE STORE can never be stolen into a delay
slot (resource_conflicts_p returns 1 on any volatil resource) which is how to
force a target nop after a j; and a "memory" clobber vs a volatile read are
NOT interchangeable CSE-breakers — both reload, but only the clobber leaves
the addu operand order alone.

§465, from func_8005F830 (152/153 byte-exact): the target hops the head insn
of the branch's own target block into the delay slot. Ten controlled probes
show cc1's fill_slots_from_thread refuses a thread insn writing the register
the branch TESTS, and a negative control shows GNU as -O2 only swaps with the
PRECEDING insn. So it is the original ASPSX reorder doing what our
REORDER_TUS substitute structurally cannot — an assembler gap, §182/§188 one
level deeper. Also records that this function's old 'epilogue unreachable'
verdicts are stale.

§466, from matching main itself (509 ins, -O0): inside a MEMORY ADDRESS,
base + i*K expands to a (mult reg K) that force_operand emits INDEX-first;
rewriting as base + ((i*(K>>n))<<n) gives the target's BASE-first addu. Value
context is unaffected, which is why it hides. Plus five supporting -O0 idioms
(COMPONENT_REF for strided stores, pad[6] for the 0x38 frame, a dead register
var to keep $s0 live, (*(u16*)x)++ vs +=1, and MEM-operand-0 argument order).
2026-09-03 16:02:32 -06:00
Drew T bf056e179f docs(cookbook): §463 — spill slots are 8 bytes; the §41b prologue wall is refuted
From the S76 func_8001FC08 agent (400 ins, 33 -> 0 MATCH). Three laws.

A 4-byte gap in an otherwise 4-packed frame is a SPILL SLOT, not a pad:
reload's alter_reg calls assign_stack_local(mode,size,-1), and align==-1
means BIGGEST_ALIGNMENT=8 with CEIL_ROUND, so every 4-byte spill occupies
eight bytes. Worth 11 ins, and modelling them as spills is what evicts both
from local-alloc so reload picks $t0.

§41b's 'a global load cannot float above the RTL prologue' is NOT a wall — it
is an $a0 anti-dependence, because the param copy addu $s0,$a0,$zero reads
$a0. Get the value out of $a0 AND make the load first and it floats to idx 0.
Either move alone is worthless (statement-first alone measured 33 -> 50);
together 22 -> 4.

Argument POSITION decides a guard value's hard register: passing it as arg 1
gives the pseudo a qty_phys_copy_sugg toward $a1, unreachable by local-alloc's
scan-from-$v0. The siblings that don't pass it stay $v0 — the control.

Also records the bank-time typedef hoist this function needs in src/800.c.
2026-09-03 16:00:33 -06:00
Drew T bb36198eea docs(cookbook): §462 — four levers from func_80024054 (74/53/32 -> 4)
From the S76 agent, none previously recorded:

  1. array[var-K] folds K into the symbol LO16/lhu displacement, and naming
     an intermediate idx does NOT stop it (the fold is front-end/combine,
     before any steerable register choice). A zero-byte opacity barrier on
     idx, one per use site, is what defeats it.
  2. The fused sll 16 / sra 15 sign-extend-scale needs the index declared
     s16 — confirms §241's recipe reproduces on a fresh case.
  3. A mask-then-compare LOCAL cross-jump-merged two case tails and flipped
     branch polarity to bne; switching on the expression directly fixed both
     and matched the target's forward-beq. The temporary was the defect —
     §461 from the other direction.
  4. A pointer parameter's SIGNEDNESS decides how -1 is materialized:
     s16* gives addiu -1, u16* gives ori 0xffff, because gcc-2.7.2
     canonicalizes the RHS constant against the lvalue's signedness when
     picking the load-immediate opcode. Invisible in the C, one instruction
     in the asm.

Residual is one permuter-class DELAY-SLOT diff two prior attempts also hit.
2026-09-03 16:00:05 -06:00
Drew T 371a18b6de docs(cookbook): §461 — laundering can be the defect; Residual A is single-op only
From the S76 func_80039B20 agent (79 ins, prior best 16 -> 10). Two findings.

A volatile-asm launder on the WRONG loop invariant displaced the address
chain and cost an entire cluster (16 -> 81 with it present); the matched
sibling func_8003A0E4 uses the plain idiom. Another invariant in the same
loop genuinely needs its launder. So the lever is per-invariant, not
per-loop, and it can go backwards.

Scope correction to Residual A (L875): the first-dying-operand / source-order
fix works on a SINGLE binary op and does NOT transfer to a PLUS chain —
measured byte-identical output when swapping operands on a 3-term chain,
because fold.c canonicalizes associative PLUS before combine sees it. Worth
recording as a negative result so nobody re-derives it.
2026-09-03 15:50:31 -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 dc4412b1de fix(verbatim_to_stub): refuse a §179-C epilogue-less fragment
A function with no `jr $ra` of its own falls into a sibling's shared
epilogue. gcc-2.7.2 has no sibcall/tail-merge pass and appends an epilogue to
every C function it compiles, so no C spelling can ever match — converting one
to an INCLUDE_ASM stub just puts an unbankable target into the drawable
frontier.

I did exactly that to six functions in src/800c.c, 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 entire meaning
is "this row is a FRAGMENT". Three drafting agents then rediscovered §179-C
from scratch, one citing the cookbook line that names its own target.

The symptom is one grep, so nobody should pay an agent to find it again.

TWO THINGS THIS COST, both caught only by testing a known-true case:
  * the first version read the function's .s — but splat stops emitting <fn>.s
    for a verbatim body, so it had nothing to read and returned False: inert
    for precisely the case it guards. It now reads the verbatim block itself.
  * my first negative control was CloseEvent, a libapi trampoline that
    genuinely has no `jr $ra` — a "false positive" that was the correct
    answer. Re-controlled on VectorNormal (verbatim, has jr $ra, guard stays
    silent) vs func_80047E58 (verbatim, no jr $ra, guard fires).

Census of main's verbatim blocks: 37 have jr $ra, 100 do not.
2026-09-03 15:02:50 -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 c982efada3 docs(playbook): record the S76 draw and oracle fixes as procedure
The playbook IS the procedure, so the five instrument fixes have to land in
it or the next session repeats them: --main drawing zero main functions,
the ledger reporting an empty frontier, the reorder-island oracle
manufacturing a §188 wall, and verbatim-asm drafts refused at three points.

Each entry carries the check to run rather than the fix that was made — the
'main: N stub(s) reached the pool' line, the ledger NOTE, and the rule that a
draw disagreeing with corpus.stubs is the thing that is wrong.
2026-09-03 14:49:29 -06:00
Drew T 1dbbdbbcd2 docs(decision-log): S76 — five more instrument defects, one shape
Records the session's through-line while the evidence is live (R31): every
wall examined was the measuring apparatus. The verbatim trap behind three
doors, the reorder oracle behind two, and draw_waves --main never iterating
main at all.

Keeps the measurements a fresh session cannot reconstruct: 1,099 of 704,375
draft files are verbatim-asm; 0 false positives across 45,898 controls;
closeness 5/36 vs 2/35 on the same draft under the two oracles; 0 -> 55 main
stubs in the pool. And the cost that is not in any count — a large part of
the 800c3 cluster's recorded wall history is instrument error, and the
journal has been feeding those false walls forward into new waves.
2026-09-03 14:48:47 -06:00
Drew T 505a50a9b6 fix(draw_waves): --main was a no-op; every mixed draw saw ZERO main functions
bins is built from src/* DIRECTORIES, and main has no src/main/ — its TUs are
top-level src/*.c. So "main" was never in the list, and the filter that keeps
it could only ever preserve a "main" already present. --only-main worked
solely because it overwrote the list; --main contributed nothing, in every
mixed draw this project has ever run.

The tool meanwhile printed "main: refusing 49 LINKED subseg(s)" whenever
--main was passed, so it announced it was handling main while main was never
iterated. A flag that changes nothing is worse than a missing flag: it
answers the question you asked.

Measured: 0 -> 55 main stubs reach the pool. This is why S76y's 47 main
targets had to be assembled by hand from corpus.stubs — the draw could not
see the actual frontier. Coverage is now ASSERTED (R32): --main with zero
main stubs exits 4 and names itself a defect rather than reporting an empty
population as a fact.
2026-09-03 14:47:46 -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 b4b2400d65 docs(cookbook): §460 — read the scheduler's ready list with -dS
From the S76 func_80180B3C agent (297 ins, 82 -> 23). Three prior attempts
steered sched1 by reordering source and inferring the cost model from .sched
RTL order; cc1 -dS prints the ready list WITH priorities, so it can be read
instead of reconstructed.

Two reusable findings: register pins beat schedule-chasing when the diff
walks a register chain (four pins carried 44 -> 23 after three attempts had
treated the chain as downstream of the schedule) — and statement order was
inert BEFORE the pins and live after, so an 'order does nothing' measurement
is only valid for the allocation it was taken under. Second, sched1's
birthing boost was proven to be the dial and is still unturnable here:
every spelling making the mask single-set lets combine fold the subreg and
lose four instructions. A dial you can prove and cannot turn is permuter
fuel, not a wall.
2026-09-03 14:34:54 -06:00
Drew T 1778556b6d fix(draw_waves): a ledgered stub that is still OPEN is still work
After two S76 draws the tool reported 'population: 0 open stubs' with 51
open stubs on disk. True, and about a scope far narrower than the reader
believes — the session's dominant defect class. The draw ledger records what
was ATTEMPTED, not a property of the function, so a stub still open after
being drawn (the draft was never gated, or the blocker has since been fixed)
was filtered forever while the work remained.

This is the S72 exclude-list lesson in a second place, and the fix is the
same shape: --redraw-open includes them, and the population line now always
names how many were filtered for that reason alone, saying explicitly when
an empty pool means 'the ledger has seen them all', not 'the frontier is
empty' (R41 — a number ships with its denominator).
2026-09-03 14:26:57 -06:00
Drew T 63d9a36f8d fix(rtu_match): route the reorder-island TUs through as -O2, like match_one
The fourth copy of one defect. The Makefile pipes REORDER_TUS through
reorder_passthrough.py into as -O2; rtu_match hardcoded maspsx + as -O1, so
for those TUs it reported a phantom +1 epilogue instruction and
recover_integration --probe-only booked it as a real DIFF.

Found by a drafting agent on func_8005D4B8: the already-fixed match_one said
MATCH 14/14 while rtu_match said 15/14, and the agent correctly identified
its own oracle as the liar rather than the draft. Derived from the Makefile,
never copied (R51).
2026-09-03 14:25:26 -06:00
Drew T 9df4ae32f6 fix(api_agent): never warm-start a pack from the target's own assembly
Third door of one defect, and the one that mattered. A §265 verbatim body is
stored as <fn>.c like any draft, so prior_draft offered it under 'a previous
attempt left this body behind, keep what matches' — an invitation to
resubmit it. match_one then says MATCH, the gate goes green, nothing is
decompiled.

Measured today: gate_main banked 9 such bodies with progress.py moving by
exactly zero; harvest_verify had no guard at all; and with BOTH gates fixed,
two relaunched agents (func_8005E79C, func_8005EAC8) STILL returned verbatim,
because the pack handed it to them and they reasonably reported 'the prior
draft is already MATCH closeness 0'. It is — that is the problem. Fixing the
consumers is not the same as fixing the supply.

Verified on func_8005EAC8: 2 verbatim candidates now rejected with a named
reason (R32, never a silent drop) and the warm start falls back to a real C
body from wave_m05/shard31. Shared by claude_wave_packs, so every future
Claude wave gets it too.
2026-09-03 14:20:44 -06:00
Drew T 186a8b1548 fix(match_one): model the reorder island, not maspsx, for its four TUs
REORDER_TUS := 800c2 800c2_2 800c2_3 800c3 are piped through
reorder_passthrough.py into as -O2 by the Makefile — the mode that fills
delay slots and emits the jr/addiu epilogue. That island landed 2026-09-01
and banked 20 functions. match_one, the oracle every drafting agent scores
against, still compiled those TUs through maspsx + as -O1, so it reported a
phantom LENGTH-DRIFT in the epilogue and an extra instruction.

Measured on one plain-C draft of func_8005ECC0:
  maspsx + as -O1   closeness 5, 36 ins vs 35   'the §188 wall'
  reorder + as -O2  closeness 2, 35 ins vs 35   epilogue identical

Cost, in the S76w wave alone: seven of eleven main agents produced correct C,
saw the phantom tail, correctly identified the §182/§188 shape, consulted
oracle_reorder.py — which told them 'file IMMOVABLE, no C-level work can ever
close it' — and each submitted a §265 verbatim-asm body instead. They all
reasoned correctly from a false premise the knowledge base gave them.

The TU list is DERIVED from the Makefile, never a second copy (R51 — a
derived property stored as config goes stale, which is this defect exactly).
oracle_reorder.py's docstring is corrected and the cookbook carries the
§182/§188 correction with the byte evidence.
2026-09-03 14:17:53 -06:00
Drew T dcbcb04bf1 fix(harvest_verify): refuse a verbatim-asm draft, same as gate_main
One defect, two doors. gate_main gained this refusal earlier today after 9
main functions round-tripped verbatim -> stub -> verbatim and 'banked' with
progress.py moving by exactly zero. The S76w wave then produced verbatim
submissions for md_MAIN_003 and ov_SC06_010 — which reach the tree through
harvest_verify, not gate_main, so the guard I added would never have fired
on them.

This is the §442/S74 sibling-provisioner lesson again: a fix made in one of
two paths is a fix in neither. Both gates now call the same
draft_prechecks.is_verbatim_asm_draft, and harvest_verify SKIPs with a named
reason rather than silently dropping (R32/R43).
2026-09-03 14:14:11 -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 9ab0d9eb67 fix(gate_main): refuse a verbatim-asm draft at slate load
I converted 9 main SDK functions from §265 verbatim bodies to INCLUDE_ASM
stubs so they could be decompiled, then 'banked' all 9 from stored drafts
that were those same verbatim asm blocks. match_one printed closeness 0 nine
times and the whole-binary gate went BYTE-IDENTICAL — both truthfully, since
a raw asm blob assembles to the bytes it was copied from. Nothing was
decompiled. progress.py caught it by not moving: REAL 882, VERBATIM 164,
INCLUDE_ASM 37, identical before and after. The banks are reverted.

The cookbook's closing paragraph, written last session, describes this exact
trap. I read it and hit it anyway ~4 hours later, because the rule was
addressed to 'any burst over this class' and I was hand-picking stored
drafts, and because 'no byte gate can catch it' reads as unpreventable. The
byte CHECK cannot; a slate-load refusal can.

draft_prechecks.is_verbatim_asm_draft: a file-scope __asm__ naming the fn via
.ent/.globl/label AND no C definition of it. Both spellings of .ent handled
(inside a C string it is a backslash-t, not a tab — five censuses of this
class disagreed until that was fixed). gate_main refuses such a slate beside
its existing INCLUDE_ASM no-op refusal (R43).

Census of the draft store: 1,099 of 704,375 .c files are verbatim-asm drafts
under ordinary <fn>.c names. Negative control: 0 false positives across
45,898 drafts carrying both a C definition and an inline __asm__ (R39).
2026-09-03 13:20:19 -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 08d49c1715 feat(tools): gate main's slates in parallel worktrees, arbiter unchanged
gate_main is the only trustworthy EXE verifier and is strictly serial: one
flock, one tree, a full clean rebuild per bisect step. The serialization is
an artifact of the SHARED TREE, not of the verification, so this runs the
REAL gate_main inside N git worktrees and hands the union of what they prove
to ONE authoritative gate_main in the real tree. Workers discover; only the
final serial pass banks. Two chunks that each pass alone can still fail
together, which is exactly why that pass exists (G3 — the arbiter never moved).

The non-obvious hazard is asm/: parallel_gate symlinks all 442 MB because an
overlay gate only reads it, but main's verification RUNS make extract, which
writes it. main owns 6.2 MB of that, so this copies main's subtree per worker
and symlinks the other 214 binaries read-only.

Two defects the negative control caught, both mine:
  * .run/obj40 (11 MB of SDK objects) was never provisioned. The Makefile says
    a tree without them 'builds byte-identically via the stubs'; that is no
    longer true for main, whose decompiled src/800_c.c CALLS CdReadyCallback —
    the link failed outright with an empty build/psyq/.
  * make_worktree reads parallel_gate's module-level WT_ROOT, so the first run
    put its worktree in .run/pgate/wt0 — the slots parallel_gate force-removes.

Measured: one gate cycle is 16s in both trees, so MAX_STEPS=24 is ~6.4 min
serial and ~90s across four workers. The docstring's original '1-2 min per
step' was my assertion, not a measurement, and is corrected in the file.
2026-09-03 13:00:16 -06:00
Drew T d564b4b4e7 feat(gate_main): persist every proven verdict the moment it exists
try_batch is stateless and the bisect loop held `good` only in memory,
writing .run/gate_main_banked.json once at the very end. A 34-minute
bisection killed by a timeout, a Ctrl-C or a supervisor therefore lost
every match it had already PROVEN — and each of those proofs cost a full
clean EXE rebuild. The S75 checkpoint named this the single highest-value
gate improvement available.

Adds an atomic .run/gate_main_progress.json written after every verdict,
and a resume that reuses it. Three guards, each a way it could silently
lie: the journal must belong to this slate; entries are re-keyed against
`kept` so a draft dropped by resolve_conflicts cannot sneak back; and the
draft's content hash must still match (R56 — a verdict measures those
bytes). Resumed sets are re-verified as one batch anyway, so a wrong reuse
costs one rebuild and can never bank anything unproven. --no-resume opts out.

Negative-controlled on six cases incl. a changed draft, a foreign slate and
a half-written journal.
2026-09-03 12:52:09 -06:00
Drew T 20fea212df fix(config): drop the stale SaveLoadRoutine symbol + its two wall entries
SaveLoadRoutine is `case 0:` inside func_8002B0B4, not a function of its
own (S75). The lingering `= 0x8002B154; // func` declaration kept splat
emitting asm/nonmatchings/800_b/SaveLoadRoutine.s, which progress.py
reported as the single UNPLACED parse hole. The two config/wave_exclude.txt
WALL entries described the same misconception.

UNPLACED 1 -> 0. Build stays byte-identical at 143dbb89.
2026-09-03 12:30:05 -06:00
Drew T 163c91e420 docs(phase-31): correct the S75 checkpoint's figures and record the one-line UNPLACED fix
Verified at close rather than asserted: 25 commits, src/config/tools clean, main
green at 143dbb89..., and HEAD genuinely carries the func_8002B0B4 C (0
INCLUDE_ASM for SaveLoadRoutine, 1 real definition). A 47-minute bisection left
several mid-run readings that looked like regressions and were not, so the
figures are now stated from a settled tree.

Adds START HERE item 0: the UNPLACED parse hole is one line --
config/symbols.us.txt:27 still declares SaveLoadRoutine = 0x8002B154 // func, so
splat keeps emitting a .s for a symbol that is now case 0: inside func_8002B0B4.
Delete, re-extract, rebuild. config/wave_exclude.txt lines 14-15 are stale for
the same reason. Left undone only because a gate held main's tree at close.

T10 checklist now carries the S75 line.
2026-09-03 12:26:26 -06:00
Drew T af4912e624 docs(phase-31): S75 FINAL checkpoint — 32 banked, nine instrument defects, the verbatim class mapped
Written for a fresh session. Headline: every codegen wall examined this session
was an instrument defect, and the two largest results came from deleting a
belief rather than writing better C -- SaveLoadRoutine's §434 wall was a splat
symbol boundary (it is case 0 of func_8002B0B4, one function on one frame), and
the '§332/§188 wall' was the reorder island, which banked 20 functions whose
drafts had been on disk since waves m04-m16.

Records what I got wrong so it is not inherited: five regex censuses
(116/112/108/178/199), contiguity mistaken for fragmentation, a build-config gap
called compiler-inexpressible, and two bursts drawn at fragments because the
triage came after the draw instead of before it.

R22 clean-fleet NOT run; the checkpoint says so at the top.
2026-09-03 12:13:02 -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 fc7caf599b refactor(tools): retire asm_in_c.py — the taxonomy is DATA now, not a regex census
R33, "the best outcome is a DELETED SCANNER, not a fixed regex". asm_in_c.py
existed to DISCOVER the §265 verbatim class by parsing __asm__ blocks. That job
is done, and regex was the wrong instrument: five successive censuses returned
116 -> 112 -> 108 -> 178 -> 199, and the classification was worse than the count
-- it called 154 rows "game code" where the authoritative answer is 24.

The real answers came from evidence a regex cannot see:
  * the <OBJ>_OBJ_<hex> naming key -- every one is placed_object.text_start +
    hex, so those symbols are OFFSETS INTO LIBRARY OBJECTS, not functions;
  * the PsyQ archive symbol tables in .run/obj40/, which keep statics as W
    symbols, so for a byte-identical object the archive IS the function map
    (checkRECT = SYS.o+0x52C = func_80059760, and NONE of the 44 SYS_OBJ_*
    symbols in SYS.o is a function).

So:

config/verbatim_manifest.json (NEW, committed) -- the authoritative census.
200 rows, derived once from the ROM image + archives + naming key, each with a
class and a DISPOSITION:
    PERMANENT-VERBATIM   69 rows / 57 units   hand asm; never decompilable
    DECOMPILE-AS-PARENT  57 rows / 23 units   a FRAGMENT; decompile unit_entry,
                                              never the fragment itself
    DECOMPILE-NOW        41 rows / 41 units
    DECOMPILE-LOW-VALUE  20 rows /  4 units
    UNCERTAIN             5 / NOT-VERBATIM 7 / NOT-CODE 1

tools/verbatim_check.py (NEW) -- a GUARD, not a census. Detects verbatim bodies
(the cheap part, and the only part regex is good at), diffs the NAMES against the
manifest, and reports NEW / GONE / MOVED. A NEW row means someone banked assembly
and it is about to become invisible work; it is never allowed to inherit a
disposition by default. It deliberately does not classify or count units.
Compares case-insensitively on the hex, because an address is a NUMBER (R48).

tools/verbatim_target_s.py -- put on the MANIFEST LEASH. It used to enumerate
every verbatim SYMBOL, and 62 of those are not functions (fragments, bare
epilogue tails, padding, trampolines). Emitting per-symbol targets for them is
what sent two drafting bursts at things no C function can express. It now takes
only DRAFTABLE dispositions: 66 targets emitted, 134 skipped and SAID SO.

tools/verbatim_to_stub.py -- repointed to verbatim_check for detection, so there
is ONE detector in the tree rather than three copies.

tools/asm_in_c.py -- REMOVED.
2026-09-03 12:05:37 -06:00
Drew T 254feb8ee4 fix(gate_main): the uncommitted-work guard belonged OUTSIDE the bisection loop
I added the guard to try_batch() an hour ago. try_batch runs REPEATEDLY during
bisection, and its own first substitution makes main's TUs dirty -- so on
iteration two the guard could not tell the operator's unsaved work from the
gate's own in-flight edit, and aborted the run:

    M src/800c3.c
    gate_main: aborting with an UNVERIFIED substitution in main's TUs — reverting

It failed safely (reverted, no bank lost, and said so), but it made the gate
unusable for any batch larger than one.

Hoisted to assert_main_tus_clean(), called ONCE from main() before any
substitution. The lesson is worth the line it costs: A GUARD MUST BE ABLE TO
DISTINGUISH THE STATE IT PROTECTS FROM THE STATE IT CREATES. Placed inside the
loop it was checking its own footprints.

Negative-controlled both directions: a genuinely dirty src/800c3.c is refused by
name before anything is substituted, and a clean tree now proceeds into the
bisection (currently running 21 drafts).
2026-09-03 11:37:07 -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 5172df5f0b fix(build): REORDER_TUS missed 800c2_2/800c2_3 — $(filter) is an exact stem match
`$(filter $*,$(REORDER_TUS))` matches the TU stem EXACTLY, so `800c2` never
covered `800c2_2` or `800c2_3`. Those two TUs went through maspsx while their
siblings went through reorder_passthrough | as -O2 (the §332b island, landed
2026-09-01).

That gap is why func_80062388's `lui at / jr ra / sw a0,lo(at)` was written up
as COMPILER-INEXPRESSIBLE in cookbook §452: a probe (`void f(int v){D=v;}` ->
cc1 -> reorder_passthrough | as -O2) emits exactly that sequence. It was a
build-config gap, not a gcc-2.7.2 define_delay limit. §452 corrected.

Byte-neutrality PROVEN the right way -- gate_main --assert-baseline builds the
committed tree with NO draft substituted:
    BASELINE GREEN — 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL

This unblocks the 24 SDK-C-REORDER units, four of which were banked as verbatim
assembly on 2026-09-02 off a wall list that predated the fix by one day, with
closeness-0 drafts already in hand.
2026-09-03 10:41:57 -06:00
Drew T 590fb37447 fix(gate_main): refuse to destroy uncommitted main work; name a tool refusal instead of hiding it
TWO DEFECTS, both found by the SaveLoadRoutine decompile and both of which made
this gate unable to bank a whole class of function.

1. try_batch() opens with `git checkout -- <main TUs>`. Correct for the normal
   flow (restore stubs, re-extract, substitute drafts) and CATASTROPHIC for
   anything uncommitted. Measured twice today: the SaveLoadRoutine decompile
   (1,179 ins, byte-identical) sat uncommitted while a gate ran and survived only
   because it was committed first; and a §265 verbatim body converted to a stub
   is UNCOMMITTED BY CONSTRUCTION, so this line restored the __asm__ block NEXT
   TO the substituted C -- 9 jump tables instead of 5, jtbl_rodata_pads refused,
   and the gate REJECTED a byte-identical bank. gate_main could not bank anything
   in the verbatim class, by construction.

   Now refuses when main's TUs are dirty, printing the offending paths and
   telling the operator to commit (R42) or stash. --allow-dirty /
   GATE_MAIN_ALLOW_DIRTY=1 is the deliberate override. A destructive step that
   cannot be undone must ASK, not assume.

2. The failure analysis looks for error/undefined/conflict/..., and
   jtbl_rodata_pads aborts via sys.exit with a message containing none of them --
   so a carve REFUSAL surfaced as "only warnings" and the real cause of a
   rejected bank was invisible. Refusals from jtbl_rodata_pads / jtbl_carve /
   corpus / jr_isolate_all are now named explicitly as the cause.

Negative-controlled both directions: the guard fires on a dirty src/800_b.c
naming the file, and --assert-baseline on a clean tree still reports
BASELINE GREEN 143dbb89... BYTE-IDENTICAL.
2026-09-03 10:41:13 -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 8009f83b40 fix(tools): verbatim_target_s wrote targets into asm/, which the Makefile globs as build objects
`build/asm/%.o: asm/%.s` globs the ENTIRE asm/ tree, so the 146 regenerated
target .s files I emitted to asm/verbatim/ were picked up as BUILD OBJECTS and
main went red:

    make: *** [Makefile:696: build/asm/verbatim/main/func_80052430.o] Error 1

Default --out moved to .run/verbatim_targets/, which is outside every build
glob. main verified green again: 143dbb89... BYTE-IDENTICAL.

The lesson belongs with the others from this session: a generator's OUTPUT
LOCATION is part of its contract, and asm/ is not a scratch directory. The
failure was invisible until a full build ran -- the tool itself succeeded, the
targets were correct, and nothing about them was wrong except where they sat.
2026-09-03 00:47:04 -06:00
Drew T 1e86617ff8 docs(cookbook): §450-§452 — verbatim pipeline, evidence sources, and a correction to §448
§450 — regenerating a target .s for a function that is no longer a stub. The
source must be the ROM IMAGE, never the __asm__ block: the block is the thing
under test, and a target derived from it agrees with the candidate by
construction. Two silent defects caught by ONE known-true cross-check: splat
writes BYTE-order hex where objdump prints the VALUE (reversing double-swaps --
91/1139 words agreed, and the LENGTH was perfect so only a word-level compare
could catch it), and objdump ELIDES runs of zero bytes so every MIPS nop
vanished (-z is load-bearing; there the length assertion did catch it). Plus
verbatim_to_stub: to gate this class, put the function back into the form every
tool already understands rather than writing a parallel gate.

§451 — your evidence has more than one source, and the one you query is probably
the worse one. BEST not LAST from the append-only backlog (a last row is
evidence about that lane's seed, not about the function); journal_notes as a
second, DISAGREEING oracle (37 functions reclassified, G-DRAFTED-UNKNOWN 47->10,
and func_8017DB98's 122 ins banked from a one-word declaration fix the journal
had recorded all along); and a regex that consumes an unbounded body cannot
enumerate the items after the first -- a 400-char window swallowed the next
attempt's header and hid BOTH of that function's MATCH records.

§452 — CORRECTION to §448's headline. A burst against the ten smallest verbatim
bodies returned 0 banks and refuted the "154 functions of real decompilation
work" framing. Four classes are legitimately verbatim: fragments of a SPLIT
function sharing one stack frame (SYS_OBJ_604/640/func_80059760 are the compiled
output of ONE original C function; a bare epilogue tail cannot be decompiled
alone), hand-written GTE assembly from 1998, compiler-inexpressible forms (a
symbolic store in a jr-ra delay slot, which gcc-2.7.2's define_delay cannot
emit), and no-return tails. 154 is an UPPER BOUND, not a work queue, and the
four tells are cheap to check.

Also banked: one agent submitted the verbatim __asm__ block itself as its
"decompile", and match_one truthfully printed MATCH -- a raw asm blob
byte-matches its own source by construction. The adversarial verifier refuted
it. Any burst over this class MUST carry that check: the trivially-passing draft
is not hypothetical here, it is the default thing to produce, and a byte gate
cannot tell the difference.
2026-09-03 00:34:17 -06:00
Drew T ce79a3e432 docs(cookbook): §449 — four compiler dials from the S75 redraft wave, two byte-gate confirmed
Provenance stated per row (CONFIRMED = banked through the whole-binary gate;
CLAIMED = the agent's own measurement on a function that did not bank), because
one of these came from a function that was adversarially upheld and then FAILED
the real gate.

A. reg_n_sets is a one-line scheduling dial (CONFIRMED, func_80180ABC 257 ins).
   sched1 schedules backward; a pseudo set exactly once gets the birthing_insn_p
   launch boost (priority = 7f000001 in cc1 -dS), which drags its load LATE.
   Splitting the RMW as 't = t + 1; *p = t;' makes reg_n_sets 2, suppresses the
   boost, and floats the load to the block head -- the block-local dual of §350's
   shared temp, WITHOUT the global-allocno penalty that costs the in-place addiu.
   Companions: 180 legal statement permutations all scored identically while one
   cc1 -dS dump named the cause (diagnose, don't permute); a pin-free fix for
   paired-register inversion; and gcc frame slot order is NOT declaration order
   (BLKmode aggregates go in order at expand_decl, an addressable scalar is
   forced to the stack later -- declare 's32 x[2]' to place a slot between two
   aggregates).

B. A single-set local's VALUE is visible at a switch join and erases a
   zero-extension (CONFIRMED, func_801806F8 241 ins). combine.c:10035 lets
   get_last_value bypass the label_tick guard when reg_n_sets == 1, so all seven
   narrowing spellings emit nothing. Diagnostic: a visible extension in the
   target means the variable has MORE THAN ONE SET in the original source.
   Verified against a matched sibling: andi is the multi-set zero-extend and
   sll;srl is NEVER reachable from a single expression.

C. CORRECTION to §439 -- the sll 16; srl 16 pair lands AFTER the jal, not before
   it (sched1 sinks the ashift past the call), and it works even for a KNOWN
   CONSTANT, because the call-split defeats folding structurally rather than by
   hiding the value.

D. An offline jtbl-rodata placement audit (CLAIMED, func_800CB00C -- did not
   bank, which is the point: both matchers compare .text only, so a jtbl
   function's MATCH says nothing about its table).
2026-09-03 00:30:55 -06:00
Drew T 72fa482027 feat(tools): verbatim_to_stub.py — put the 147 asm-posing-as-C functions back where the gates can reach them
THE GAP. Every gate in this project substitutes a draft in place of an
INCLUDE_ASM line. A §265 verbatim __asm__ body has none, so:

  gate_main.substitute()    resolves each entry through the STUB map; a verbatim
                            function is reported "resolved to NO stub" and dropped
  gate_stage/harvest_verify same splice, same gap
  splat                     stops emitting <fn>.s once a function is not a stub

So all 147 were undecompilable AND ungateable -- not for want of information,
but because the information was in a form nothing consumes.

The fix is not a parallel gate (R33 -- one implementation). It is to put the
function back into the form every existing tool already understands: replace the
__asm__ block with INCLUDE_ASM. That is also the HONEST representation --
INCLUDE_ASM pastes the very same assembly the block transcribes, so the bytes
are identical either way, but a stub counts as OUTSTANDING WORK in progress.py
while a verbatim body counted as banked. The conversion moves a function from
"silently done" to "visibly to do".

Two refusals rather than guesses (R43), because both failure modes are silent
and destructive:
  * the block is located by brace/paren MATCHING via asm_in_c.asm_blocks, never
    regex-sliced -- these blocks are full of braces and parens inside string
    literals and an approximate cut corrupts a file that currently builds;
  * the asm subdir for the new INCLUDE_ASM is copied from a sibling stub IN THE
    SAME FILE. Subsegs are per-file, so a neighbouring file's spelling names a
    different subseg -- a stub with the wrong subdir compiles happily and
    includes ANOTHER FUNCTION'S ASSEMBLY. With no sibling to copy, it refuses.

--gate rebuilds and asserts the SHA is unchanged, restoring the file if not:
byte-neutrality here is a claim, and this tool exists to enable a byte gate, so
it declines to be the one link that goes unchecked.

Dry-run verified on main:func_80047E58 -> src/800b.c, 9-line block, subdir
correctly derived as asm/nonmatchings/800b. The --gate proof is deferred only
because another agent is mid-build on main right now.
2026-09-03 00:22:24 -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 8e3ce81a69 docs(phase-31): S75 addendum — the frontier is 154 game functions bigger than reported
tools/asm_in_c.py finds 199 functions that are assembly posing as C (154 game
code, 171 in main). They were in no progress.py bucket, so main's REAL% was
overstated: 45.88% -> 42.15% once counted. Nothing regressed; the denominator
was missing 173 functions of real remaining work.

Also records the counting cautionary tale (five hand counts, 116->112->108->178
->199, each wrong the same way) and the tool design that answers it. Cookbook
§448.
2026-09-03 00:02:02 -06:00
Drew T c05ea15cbe feat(tools): asm_in_c.py — 154 game functions are assembly wearing a .c extension
A .c file in src/ looks decompiled. 199 functions are not: they are the target
assembly pasted into a C string literal (§265), byte-identical BY CONSTRUCTION
and completely unexplained. 45 are PsyQ/CRT routines where that is defensible;
154 are GAME CODE, 171 of the 199 in main, the largest being SaveLoadRoutine at
1,165 instructions.

They were invisible because progress.py's classify() matched INCLUDE_ASM,
INCLUDE_RODATA and C definitions, and a file-scope __asm__ block is none of
those -- so each landed in NO bucket, either swallowed by a surrounding
construct or surfacing as the single `UNPLACED (parse hole)` line the tool has
been printing all along.

progress.py gains a VERBATIM __asm__ bodies line: counted byte-identical (it is,
by construction) but NEVER as REAL. main's headline moves 45.88% -> 42.15%.
Nothing regressed and no work was lost -- the denominator was missing 173
functions that are real remaining work.

THE COUNTING LESSON IS THE REUSABLE PART. Counting these by hand went
116 -> 112 -> 108 -> 178 -> 199 across five attempts in one session, every
intermediate number reported confidently. All five errors were one shape, a
pattern narrower than the claim it supported:
  * the sources use BOTH ".ent\tNAME\n" and ".ent NAME\n" -- anchoring on either
    silently drops every instance of the other;
  * a bare ".ent\t" fragment yields a phantom function literally named `t`, six
    times, which is the only reason the error was noticed;
  * __asm__ appears in 3,182 of 4,224 sources, almost all the §3a barrier, so
    counting files or counting __asm__ measures nothing;
  * `.globl NAME` + `NAME:` proves EXPORT, not CODE -- the first real run
    reported jtbl_80072ED4/EEC/F0C/F24 as four "functions";
  * a hand-written SDK name list reported 170 game functions because it did not
    know VectorNormalSS / SquareRoot12 / OuterProduct12 are libgte.

So the tool does not trust one regex: THREE independent detectors that must
agree with disagreement reported as a defect (R34 -- that is what caught the
jump tables); SDK-ness DERIVED from the 14 shipped PsyQ archives via nm (2,227
symbols) rather than a list (R33); coverage asserted so a definition-shaped
block no detector claims fails loudly (R32/R43); and --selftest carrying a
known-true case of every spelling plus the phantom `t` and the jtbl regression.

Cookbook §448, SETUP row. Law: when a count comes from a text pattern, the
pattern has a denominator too -- validate it against one known-true case of
every FORM the corpus contains before quoting the number.
2026-09-03 00:01:11 -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 6d56972a4b docs(R31): S75 decision log — seven walls were seven instruments; leave the 12,000
Two entries. (1) The session's through-line: seven 'codegen walls' examined,
seven instrument defects, none the compiler -- and the reusable law that a
verdict class which CANNOT FIRE is worse than one that does not exist, because
it turns 'I don't know' into confident wrong advice. SaveLoadRoutine's 1,165
instructions are the price: a byte-identical body behind a verdict naming the
wrong subsystem, for a whole phase.

(2) Drew's decision not to convert the ~2,073 functions / ~12,116 duplicate
copies of dedup-hygiene backlog, on the sotn precedent our cookbook records
('sotn writes duplicate funcs explicitly'), with the caveat that the claim rests
on one parenthetical of ours rather than sotn's repo. Gate --no-propagate from
here; the backlog is orthogonal to completion %.
2026-09-02 23:18:58 -06:00
Drew T ac55da69d1 docs(phase-31): S75 checkpoint — 7 banked, seven instrument defects, SaveLoadRoutine is a carve
Full 🛑 block for a fresh session: state, the seven defects (§442-§447), the
measured frontier map, Drew's dedup decision, and the harness lessons.

The headline for whoever picks this up: SaveLoadRoutine (1,165 ins, the §434
wall, 9.2% of all remaining work) has a BYTE-IDENTICAL body and is blocked by a
jump-table carve -- which the verdict tool could not say because that verdict
class was unreachable by construction on main.

R22 clean-fleet is NOT yet run for S75 and the checkpoint says so.
2026-09-02 23:11:35 -06:00
Drew T cf7f837231 fix(gate): main's TABLE REJECT verdict was unreachable — SaveLoadRoutine is a CARVE, not plumbing
SaveLoadRoutine (1,165 ins) is the largest open function in the project, 9.2%
of all remaining work, and has been carried as the §434 WALL. Gated alone
through gate_main, with the §376/§378 chain already applied, the verdict layer
says: "SaveLoadRoutine is BYTE-IDENTICAL; all 3989 differing bytes are
ELSEWHERE". The body has been correct the whole time.

What rejects it is where its FOUR jump tables (jtbl_80072ED4/EEC/F0C/F24) land:

    .data/.rodata (jump tables)  3,787 bytes   94.9%
    .text (perturbed code)         202 bytes    5.1%

and the built image is 4 bytes SHORTER than retail (413,692 vs 413,696) --
§446's first diagnostic, firing on a function §446 was not written about.

main_diff_locate.classify() already HAD a TABLE REJECT class, added in S72 under
a docstring reading "THE THIRD CLASS EXISTS BECAUSE THE FIRST TWO MISLABELLED
IT". It could not fire here for two independent reasons:

  * it keyed on the literal string `(.rodata)`, but main's section_order is
    [.rodata, .text, .data, .bss] -- its rodata sits BELOW .text and its jump
    tables live in `.data` objects, so TABLE REJECT was UNREACHABLE BY
    CONSTRUCTION on the binary with the most jump-table functions left. A
    section NAME is not a section ROLE.
  * it demanded purity (ro == outside), so 5% perturbed code defeated an
    all-or-nothing test and dropped the verdict through to PLUMBING REJECT --
    whose advice (fix_arity_callers -> cast_self_callers) addresses the 5% and
    cannot touch the 95% that is data. That chain was run on this function
    TWICE today and fixed nothing, exactly as the evidence predicts.

Now: table bytes counted in (.data) OR (.rodata), and the test is DOMINANCE
(>=60%) rather than purity, reporting the split and naming which part is the
carve problem and which the declaration problem.

Negative control over all five pre-existing verdict shapes (pure BODY, pure
PLUMBING, pure TABLE, MIXED, NOT FOUND) plus the S75 shape: 5 of 6 verdicts
UNCHANGED, only the SaveLoadRoutine shape flips PLUMBING REJECT -> TABLE
REJECT (MIXED).

Cookbook §447. The law: a class that cannot fire is worse than a class that does
not exist -- it converts "I don't know" into confident, specific, wrong advice.
When a verdict names a subsystem, check that subsystem owns the MAJORITY OF THE
BYTES before acting on it.
2026-09-02 23:05:27 -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