Commit Graph

1061 Commits

Author SHA1 Message Date
Drew T 5399845172 feat(psyq_bss_probe): a Phase-8 link exclusion re-derived from the bytes — 3 of 4 objects are not blocked as recorded
The yaml has excluded SYS.o/GS_001.o/2D_BG0.o/VM_NO1.o from the LINKED build
since Phase 8 for 'scattered-.bss commons ... no single NOLOAD base reproduces
it'. Every word of that is true, and it does not imply unlinkable.

psyq_bss_probe derives each object's .bss bases FROM THE BYTES (for each
HI16/LO16 pair against the bare .bss section, the object's immediates give the
addend and the game's give the resolved address, so base = resolved - addend)
and then asks the unasked question: are the offset ranges DISJOINT?

  SYS.o     3,109 ins  2 bases  0x0000-0x0044 @ 0x80078830
                                0x0148-0x0150 @ 0x800c53cc  -> SPLITTABLE at 0x148
  GS_001.o    384 ins  5 bases  interleaved                 -> the genuine wall
  2D_BG0.o    526 ins  NO .bss                              -> reason cannot apply
  VM_NO1.o    305 ins  NO .bss                              -> reason cannot apply

§9.2's escape (weaken the .bss symbol, --defsym it) really cannot reach these —
a relocation against the bare SECTION has no name to defsym — and that is what
made 'unlinkable' look like the conclusion. But a section reference only needs
the section PLACED, and a section can be split.

Completeness checked before believing it (R32): the probe counts .bss refs from
EVERY section; SYS.o's .data has zero, so the two-way split covers every
reference. Placement is derived, not configured — the object is located by
masking relocated fields and requiring a UNIQUE match, which independently
reproduced SYS.o @ 0x80059234 / 3,109 ins, agreeing with both the yaml subseg
bounds and the manifest's psyq_identify count.

Incidental: src/800c.c is 100% SYS.o (its span is exactly the object's .text
size), despite the subseg comment calling it '-O2 game code'.

Cookbook §484; yaml comment corrected in the same change.
2026-09-03 22:07:31 -06:00
Drew T 867f09221c feat(oracle): main gets its independent second oracle — contract §1.3 closed
The roadmap's completion contract requires both audit oracles green before any
100% claim on main, and main had none: audit-corpus covered overlays and
resident only, and R34 is explicit that the byte gate is a perfect CORRECTNESS
oracle and a NULL COVERAGE oracle — green whether a function was sliced right
or invented, because the .s pieces paste back either way.

sig_image gains multi-range signing, closing all three blockers
docs/second-oracle.md scoped:
  * the 0x800 PS-X EXE header -> --vram-base 0x8000F800 puts file offset 0 at
    vram, so the header falls below the first range
  * interleaved data + linked islands -> --segments derives 28 game-code ranges
    from the splat yaml's SEGMENT rows
  * one text range -> the signer loops ranges, bootstrapping INSIDE each, which
    is what stops the linear partition running through a data island and minting
    functions out of it (the detector manufacturing the class it detects)

INDEPENDENCE IS PRESERVED, NOT WORKED AROUND. Ranges come from segment TYPES,
never from splat's function boundaries; entries are still found by byte-derived
jal-closure. Seeding from splat's symbols would make every phantom look real —
the trap the design doc names. .run/sig.main.jsonl (the splat-SEEDED atlas sig)
is a different file and corpus.ORACLE_SIG keeps the audit off it.

RESULT: 986 functions signed. main audit = 0 PHANTOM, 0 TRUNCATED, 1 PAD-TAIL.
Fleet audit-corpus = 0 + 0, unchanged for resident and overlays.

NEW AUDIT CLASS, from the first real finding. func_80062144: splat .s 65 ins,
oracle 64 — the extra line is a nop one line BELOW endlabel. That is an
alignment pad the matching side already emits from C (§295; two S77 wave agents
did it on func_8005E13C and func_8005D538), not a mis-slice. Lumping it with
TRUNCATED would make the oracle's first finding look like a defect and bury the
class that is one.

COVERAGE ASSERTED both ways before trusting it (R32): all 30 game-code stubs
fall inside a range, and 0 of 199 addr-parseable LINKED stubs do.
2026-09-03 21:58:17 -06:00
Drew T 4a0f9a3049 docs: regenerate the cookbook index for §477-§483
tools-health caught this red: seven sections added this session without
regenerating the index. Exactly the sibling-update the health gate exists to
enforce.
2026-09-03 21:16:24 -06:00
Drew T 46097c2339 feat(permuter_sweep): hand a wave's NEARs to the permuter, and correct §479 a second time
THE GAP: a drafting agent is briefed to STOP at a plateaued permuter-class
residual — right, since an agent grinding a register permutation burns tokens
for nothing — so every SCHEDULE-REORDER/DELAY-SLOT/REGALLOC-PERM residual lands
unattempted while the local permuter costs no tokens. In S77 the hand-off
happened only when I remembered.

THE CORRECTION THIS TOOL FORCED. §479 v2 claimed the predictor of a permuter win
was 'prior-attempt history: all 3 winners were drafts nobody had worked'.
Building the selector on that claim refuted it immediately: journal_notes
reports prior attempts for ALL EIGHT known runs, winners included (2, 3, 3).
What I had eyeballed was the DRAFT HEADER narrative, a different corpus — the
winners came from a recovery pile whose files carry no header journal. That is
provenance, not evidence.

So the tool selects on the two NECESSARY conditions only (small residual, a
match_one class the permuter can search), prints prior-attempt counts as
information, and puts the unvalidated filter behind --skip-ground, off by
default so it cannot silently discard good work (R39).

AND A BUG IN THE NEW TOOL, caught by cross-checking against known-true numbers:
wave_results globbed journals across EVERY session and did last-write-wins on a
bare function name, so an older wave's row won and carried its stale
draft_path — the sweep reported func_8002AC98 at closeness 73 and func_80015608
at 65 while both drafts measure 1 and 3. R48 inside a brand-new tool. Journals
are now read newest-last and rows are kept only when the draft lives under this
wave's directory. After the fix all seven cross-checkable residuals agree with
what the agents independently reported (9, 8, 7, 3, 3, 1, 1).

§479 now states the honest position: ~3 in 8 at <=4, no validated predictor, and
a note that a yield table is evidence while a story about why is a hypothesis
needing its own negative control before it goes in the cookbook.
2026-09-03 21:09:59 -06:00
Drew T ec258ff75a feat(recover_route): route a gate DROP to the tool that applies, and wire it into gate_main
gate_main printed ONE recovery chain for every dropped draft, and it was the
SELF chain (fix_arity_callers --any-proto + cast_self_callers) regardless of
what the clashing symbol actually was. Two of the three classes are not that
chain:

  CALLEE — §378 does not transfer; cast_self_callers reads the return type off
           the draft and cannot cast a callee, so --any-proto runs unprotected
           over every call site. S69 measured 60 decls no-protoed, binary RED.
  DATA   — neither tool in the printed chain touches a data extern at all.

Measured cost of the wrong route THIS session: func_8006252C was dropped on a
clash with itself; following the shape of the printed chain I reached for
scope_demote_drafts first, which aliased D_80078D08 through __asm__ and BROKE
the build. The real blocker was one --sync-decls away. Three tools, wrong
order, one destructive — because the report named a chain instead of a route.

A route is an ORDERED LADDER, not a prediction: for a DATA clash the choice
between adopting the TU's spelling and demoting to block scope depends on
whether the draft can live with the TU's type, which no classifier can know.
The byte gate remains the sole arbiter (G3/P9). Refusals come first (R43/R61a):
a verbatim draft and a NEAR are not declaration problems.

NEGATIVE CONTROL (R39): all 7 S77 drops whose winning tool was already known
route correctly — 2 SELF (cast_self_callers), 1 CALLEE (sync_tu_decls via a
definition header), 4 DATA — and the DATA ladder's order matches which rung
actually won in each case (sync for D_80072978, demote for D_80072960 and
D_80074818). Verbatim draft refused; real-C draft not refused.

Playbook §4b and SETUP updated in the same change.
2026-09-03 21:05:50 -06:00
Drew T bfc0f43c92 docs(phase-31): S77 CLOSE — 30 banked, main 57.1%->59.4%, ten instrument defects, R61
S77w wave: 30 workflows, 30/30 reported, 9 banked, 21 NEAR, 0 errors.
R22 clean-fleet 213/213 (fourth run this session). Cookbook §477-§483.
2026-09-03 20:52:15 -06:00
Drew T 984b50215f docs(cookbook): §483 the S77w wave harvest — six levers, four from banked bodies 2026-09-03 20:46:54 -06:00
Drew T 3b959596f6 docs(cookbook): correct §479's yield curve — the permuter is 3/8 at <=4, not 3/3
The first version of §479, written earlier this session on 3 data points, said
the permuter is a one-shot at <=4 mismatched. Five more runs make it 3 of 8,
and the failures are not marginal: a residual of 1 failed while a residual of 4
banked, so mismatch count predicts nothing.

The real predictor is prior-attempt history. All three winners were drafts
nobody had worked. Every failure was a body an agent or prior wave had already
optimised (5, 6 and 4 prior levers respectively). A draft a competent search has
plateaued is plateaued for the permuter too — its wins come from unexplored
neighbourhoods, not from small numbers.

Same predictor as §479's triage paragraph, reached from the opposite direction.
2026-09-03 20:43:32 -06:00
Drew T f7702eac69 docs(cookbook): §482 two independent re-ties, ordered — a re-tie is a scheduling barrier with a position 2026-09-03 20:31:47 -06:00
Drew T 9df0cd29dd docs(cookbook): §481 conflicting types is a SAME-SCOPE error; across scopes it degrades to a warning
The escape hatch for the declaration-conflict class the reconcile/sync ladder
cannot reach — two anonymous struct typedefs in one TU are never compatible in
C89, so no duplicate spelling works, but block scope turns the error into the
warning the build already emits elsewhere. From main:func_8001FC08 (400 ins),
whose body was solved in S76 and had never banked because nobody asked why.
2026-09-03 20:17:57 -06:00
Drew T f9f446449e feat(claude_wave_packs): wire neighbor_ref into every pack, and resolve its names to the source spelling
playbook §2b has called neighbor_ref the biggest measured cost lever in the
wave since S68 (~20x token swing) and documented it as a MANUAL per-card
command wired into nothing — so it ran for approximately zero cards. Packs now
carry an ALREADY-MATCHED NEIGHBOURS block, same additive never-fail contract as
the past-attempt notes. First run: 30/30 targets had a matched neighbour.

It also shipped with a defect that would have silently un-done it:
neighbor_ref reports the SYMBOL-TABLE name, and for an unnamed function that is
Ghidra's FUN_8003a0e4 — which appears nowhere in src/*.c, where the function is
func_8003A0E4. An agent sent to read FUN_8003a0e4 finds nothing and concludes
there is no neighbour. _src_name resolves against the destination TU's own text,
falls back to the address, and shows the symbol-table spelling in parentheses.
Measured: 150 of 150 neighbour names needed resolving; 0 primary names remain
Ghidra-style. Checked against known-true cases first (resolves FUN_8003a0e4,
leaves func_8003A0E4 alone, leaves an unknown name untouched).

R61(b): the pack was asserting a name true of the symbol table and false of the
world the agent works in.
2026-09-03 20:08:27 -06:00
Drew T 9d15598b5a docs(phase-31): S77 FINAL checkpoint — 21 banked, self_decl_tu closed, the permuter yield curve, eight instrument defects 2026-09-03 19:51:02 -06:00
Drew T be966bf095 docs(cookbook): §479 the permuter's measured yield curve (one-shot at <=4, plateau above ~10); §480 a static blocker class the real pipeline removes is a phantom 2026-09-03 19:45:55 -06:00
Drew T b5751c7c1e docs(phase-31): S77 checkpoint — 17 banked, the self_decl_tu lane closed, six instrument defects
T11 4/7, T12 13 banked of a 34-draft pool, T13 R22 213/213 twice (a green
baseline before the overlay banks and again after all 17).

main REAL 895 -> 899, stubs 46 -> 42. Fleet stubs 82 -> 65, distinct-code
99.3% -> 99.4%, MAIN game-code 57.1% -> 57.3%.
2026-09-03 19:25:02 -06:00
Drew T c61c7ed93f docs(cookbook): §477 the self_decl_tu lane is mechanical (16/16 banked); §478 a verbatim draft is the strongest false signal a scoper can emit 2026-09-03 19:19:29 -06:00
Drew T e0229af908 docs: restore §462/§463, record S76 tooling in SETUP, add the gate-triage step to the playbook
Three gaps found by auditing instead of asserting.

§462 and §463 were MISSING from the cookbook although their commits are
ancestors of HEAD and added 37 and 34 lines. Same silent loss as §464, which
I caught only because I happened to re-check the three sections I had just
written. Both restored from their own commits; all of §460-§476 now verified
present one by one.

SETUP.md had no record of either new tool (R21). Added gate_main_parallel and
sync_tu_decls, plus the oracle corrections a reader needs in order to
re-judge older verdicts: the REORDER_TUS routing in match_one/rtu_match, the
draw_waves --main no-op, the verbatim-draft refusals at three points, and the
§179-C conversion guard.

The playbook had nothing on what to do when a gate banks far less than it
staged — which is exactly what happened this session. Added the triage step:
probe first (CC1-FAIL 16 / DIFF 18 / MATCH 6 on main's 40), sync declarations
for the plumbing class, hand self_decl_tu to cast_self_callers, and expect a
cascade because every bank changes the declaration environment for the drafts
that follow it.
2026-09-03 17:41:43 -06:00
Drew T 920f8bac35 docs(cookbook): §476 — a hard-register pin strips nonzero_bits and reg_n_sets==1
From the S76 Fable agent on func_800226C0 — 670 instructions, the largest
function in the project, matched at closeness 0.

Explains WHY pins so often hurt, completing the arc of §461/§462/§471:

  (a) A pinned hard register carries no nonzero_bits, so combine cannot fold
      sext(HImode t) into a copy — which is exactly what the target's 228E4
      addu/beqz/addu chain is, with cse2 reusing it as the loop multiplier.
      The $18 pin that looked obvious was what prevented the fold; one plain
      uninitialised s16 t (mul left an unpinned pseudo) unlocked it.
  (b) A pin makes reg_n_sets != 1, so birthing_insn_p refuses the §199-A
      boost and the value is placed first — a whole-block schedule shift.
      Unpinning o/col/sh23/abr fixed the prologue order and two ties.

Rule: if a residual involves a sign/zero-extend fold or a first-in-block
placement, REMOVE pins before adding them.
2026-09-03 16:29:55 -06:00
Drew T 3db8b2b117 docs(cookbook): §475 — the "memory" fence as a cse invalidator; (b*3)<<3 over b*24
From the S76 func_8002FF0C agent (166 ins -> MATCH, verified in-TU with a
spliced src/800_b.c compiling rc=0 and all 63 relocs matching).

__asm__ __volatile__("" ::: "memory") is a CSE MEMORY-TABLE invalidator, not
only a scheduling fence, and the colon-less __asm__("") does NOT substitute:
it forces D_800A46D2 to be re-read rather than folded to sign_extend(r), and
without it the function is exactly two instructions short. Pairs with §464
lever 4 — same two spellings, register half there, memory half here.

Write (b*3)<<3, not b*24: expand_mult never honours its target, so b*24
leaves a move copy that survives into the join block and costs a sixth
callee-saved register plus a 0x30 frame. A top-level LSHIFT_EXPR expands into
the variable's own pseudo. General for any constant multiply factoring as
odd<<n.

Independently confirms §470's 'two distinct locals for the same b*24' on a
different function via a different agent — treat as established.

And the house array spelling can be the defect: D_800A46D2 must be scalar at
block scope; extern s16 D_800A46D2[] forces la for both accesses and costs 12
mismatches. A fleet-consensus declaration is a prior, not a law.
2026-09-03 16:21:35 -06:00
Drew T 77dc52c633 docs(cookbook): §474 — a PROVED -O0 floor from split_tree + stupid.c
From the S76 func_80011380 agent, which upgraded an empirical closeness-6
plateau to a floor proved from the gcc sources in tools/reference/.

The target needs MULT(MULT(i,2),2) unmerged, but fold-const.c:882 split_tree
decomposes any MULT whose op1 is TREE_CONSTANT — all 20 spellings measured
collapse to one sll 2, and STRIP_NOPS eats NON_LVALUE_EXPR so the usual |0
+0 *1 &~0 ^0 >>0 shields cannot protect it.

Both escapes cost an instruction, each for a named reason: a stmt-expr gives
the exact 5-insn RTL but its BLOCK_END note breaks the adjacency that
stupid.c:497-508 needs for a copy to conflict with its source, so the copy
self-coalesces and final.c deletes it; and (t = i*2)*2 with register s32 t
reaches exact length and shape but expand_decl's zero-byte (use) brackets
make t the longest interval, seizing $v0 and rotating the register ring.

Clinching fact that the target has no variable there: its 4th insn
sll $v1,$a0,1 reads $a0, not insn 2's dest.

Bonus: expand_binop allocates the PLUS dest before force_reg'ing the symbol,
so the symbol pseudo loses stupid_reg_compare's tie-break — that is the
la-on-$a0 colour.

Recorded as the TEMPLATE for a wall claim: name the pass, cite file and line,
measure each escape, and give the byte fact ruling out the alternative. A
wall asserted without that is a belief (§473).
2026-09-03 16:20:59 -06:00
Drew T a4d6ec498b docs(cookbook): §473 — §265's handwritten verdict for func_8017DC80 is refuted
From the S76 agent: 324 -> 89, from a 20-attempt LENGTH-DRIFT/-33 wall to -2.
The interleaved sw/def prologue this file cited as proof of hand-written
assembly is ordinary gcc-2.7.2 MIPS RTL.

Moved by §30's /s-dep lattice (plain scalar sxy stack locals + COMPONENT_REF
packet stores through a POLY_G4/LINE_G2 struct pointer), un-cached
*(s32*)(c+0xB) reloads, and a recomputed OT pointer.

Fourth wall refuted this session, after the §182/§188 reorder oracle, §41b's
prologue hoist (§463) and the S75 nine — three of the four were recorded as
properties of the CODE and were properties of an instrument or a model.

Manifest consequence: this function's UNCERTAIN row resolves toward
decompilable, not PERMANENT-VERBATIM; converting it to a stub was correct and
it belongs in the drawable pool.
2026-09-03 16:18:49 -06:00
Drew T e6c0fad818 docs(cookbook): §472 — §148-A's hoist threshold is 29 with a call, not 58
From the S76 func_8001EA14 agent (371 ins, 349/303 -> 89, length exact),
cracked with cc1 -dL.

The loop.c hoist threshold is call-dependent: 29 when the loop contains a
call, not the 58 this file has been quoting. And the inputs are not what
their names suggest — savings is the COUNT of matched movables, lifetime is
their SUM. Anyone applying §148-A to a loop with a call has had the wrong
constant.

MEM_IN_STRUCT_P runs both ways: §469 set it to unblock hoisting, here it must
stay CLEAR (plain casts, not a struct) to reproduce the target's alias-blocked
schedule. Decide which direction the target needs first.

The COND_EXPR 'X ? A op B : A' singleton fold is escaped only by making the
arms structurally different TREES, not merely different values.

Spill slots follow DECLARATION order — completing the frame model with §463
(8-byte rounding), §469 (layouts only a declared local can give) and §471
(the §172 USE-orphan): a slot nothing reads is a spill or an orphan, never
padding.
2026-09-03 16:13:34 -06:00
Drew T 0c74457f27 docs(cookbook): §471 — a launder's cost is an allocno, and $t0 belongs to reload
From the S76 func_80032A74 agent (422 ins, 408 -> 12).

Refines §153: the launder was necessary but created an allocno outranking the
value it was protecting; the cure was pinning the launder itself to $10 — and
NOT $8, which evicts reload's $t0 parameter reloads. So '§461: the launder is
the defect' has a third resolution beyond remove-it or move-it: pin it, and
choose the register with reload's own needs in mind.

New general fact: $t0 is unreachable from C because reload owns it — the
target's table bases are reload rematerialisations of a reg_equiv_constant
there. A residual of the form 'the target uses $t0 and I cannot' is a reload
artifact, not an unfound spelling.

Also pairs with §463/§469: a frame slot nothing reads is either an 8-byte
rounded spill or a §172 combine USE-orphan — both reproducible, neither
padding.
2026-09-03 16:12:14 -06:00
Drew T 9467db409b docs(cookbook): §470 — four CSE/sched levers from func_800301C8 (133 -> 18)
The counterintuitive one: use TWO distinct locals for the same b*24, because
cse resets at the if-join and the original recomputes the product into a
second register — one shared local cannot reproduce it, and writing it inline
is worse still (cse hoists the %hi/%lo address into a pseudo and changes the
addressing mode). Duplicating a subexpression can be the correct decompile.

Plus: a store-then-read-back turns a redundant load into the target's
register copy; a zero-byte fence stops sched1 hoisting two '= 0' stores into
the load-delay slot; and writing three repeated tails out separately lets
cross_jump merge them, where funnelling them through one variable emits the
arms inverted.

Residual is three allocation facts, incl. a $17 pin that is REQUIRED (else k2
splits across two callee-saved regs and costs a fourth) but drags the shift
chain into $s1.
2026-09-03 16:11:08 -06:00
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 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 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 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 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 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 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 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 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 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 0c2b37a287 fix(gate): overlays.mk carve-state snapshot was singular, so the revert half-restored
Found by running one reject to ground. After a gate that REJECTED
resident:func_800D06E8, config/overlays.mk had a 4th JTBL_PADS entry and had
LOST `--pre hdr.rodata.o` (the §440 resident leading-rodata sandwich). The
binary then would not build at all -- "consumed 3 rodata jump table(s) but 4 pad
spec(s) given -- table-count drift vs the carve" -- while src/ was perfectly
clean, which is the only place anyone looks before building.

Root cause is a silent narrowing in the classic shape. harvest_verify snapshots
ONLY the gating binary's own overlays.mk block on purpose (the file is shared by
every parallel gate; a whole-file restore resurrects other binaries' lines --
the S62 defect). But _mk_block_span was SINGULAR: the first `# --- <binary>`
header through the next `# --- `. A binary whose carve state spans more than one
block was half-snapshotted and silently half-restored. It returned a TRUE span
for a scope smaller than the caller believed, and nothing compared the two (R32).

Blast radius measured before costing (R37): 1 of the 142 binaries that have a
block -- resident, which has exactly two (§8e pad spec, §8f leading-rodata
sandwich) and still holds 587 instructions of open stubs.

_mk_block_spans (plural) snapshots a LIST, restores tail-first so earlier spans
stay valid, collapses to the snapshot when the header count changed rather than
leaving half-state, and RE-READS and compares the result -- the defect it
replaces was a reported success. _mk_block returns None (not []) for the 71
binaries with no block, so the caller's guard keeps its meaning.

Negative control, three ways:
  * snapshot -> restore is a NO-OP on 142/142 binaries with a block;
  * the real S75 damage is fully undone;
  * the OLD single-block restore provably does NOT undo it -- the positive
    control that proves the fix is load-bearing, not decorative.

Cookbook §444 also records the two other findings from the same reject: the
classified ledger stores the LADDER'S FINAL verdict (the recorded CC1-FAIL came
from a late sig_unify rung; the raw draft compiles and fails on BYTES), and
match_one MATCH + rtu_match MATCH is still not bankable -- func_800D06E8's real
blocker is a jump table (built binary 20 bytes longer, 0x800CEDFC holds a table,
69,571 words shift), because neither matcher LINKS.
2026-09-02 20:33:21 -06:00
Drew T cdd535e45b fix(tools): reconcile_tu's cc1 premise, the &-cast arms, and the worktree sig gap
The S74 checkpoint's "one unfixed defect that is actively costing banks"
(reconcile_tu manufacturing declaration conflicts), run to ground — plus the
harness gap that produced a false carve-corruption verdict.

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

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

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

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

Docs ship with the change (R21): cookbook §442/§443, index regenerated (1,112
sections), 3 docs/SETUP.md rows, CURRENT_PHASE S75 log.
2026-09-02 20:30:22 -06:00
Drew T eb0329b3a4 docs: the five agent-authored tool changes that shipped with a commit message and no docs
I answered Drew's yes/no honestly — NO — and this closes it. Every gap had the same shape: a tool
change that came from a SUBAGENT arrived as a report, I merged the code and wrote it up in the
commit message, and a commit message is not the knowledge base. The six changes I made myself were
documented inline; these five were not.

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

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

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

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

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

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

Cookbook §438 (the law: a lead is fuel only if it carries the cheapest fact that can refute it —
size refutes a homonym for free and nobody had asked) and §439, the S74 lever set: MEM_IN_STRUCT_P
as a two-way alias-oracle dial (four agents converged on it independently); `goto`-into-a-shared-tail
vs longhand as a REGALLOC dial because gcc-2.7.2 cross-jumps after allocation; `for` -> do/while as a
length-changing scheduling dial; allocno PRIORITY via a non-volatile asm at a loop head, with the
measurement that register pins are actively harmful for that class; the -O0 global-RMW rule
(`x++` emits the copy-back quartet, `x = x+1` does not); why `sll 16; srl 16` survives only across a
CALL; `sltiu N` without `addiu -1` proving an empty `case 0` is mandatory; block-scoped temps in
duplicated bodies; two `register asm` vars cannot share a hard reg; and `x*32` vs `x<<5` emitting
lh vs lhu — which match_one's %lo mask HIDES, so it must be checked with objdump.
2026-09-02 19:38:56 -06:00
Drew T 3a886b9652 fix(tu-split): a block comment a construct OPENS MID-LINE and WRAPS defeated every peeler
FIVE independently-MATCHed ov_SC06_029 bodies were rejected by a `parse error before '#'` in a file
the GATE ITSELF generates, at a line no draft contains. The isolation emitted, into the §8b carried
decl layer:

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

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

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

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

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

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

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

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

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

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

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

Cookbook §436-D; wave-playbook §6 carries the hand-gating version of the same warning.
2026-09-02 19:10:57 -06:00