Commit Graph

3850 Commits

Author SHA1 Message Date
Drew T f030992c67 fix(psyq_identify): read .text bytes, not objdump's rendering — 10 more objects located
obj_text_pattern parsed ONE WORD PER DISASSEMBLY LINE, and objdump collapses a
run of identical words into a single `...` line. Every collapsed word was
silently missing from the pattern, so from the first run onward the pattern was
MISALIGNED against the image and find() returned None -- printed as the
confident, wrong sentence "not linked by EXE".

Measured on 2D_BG0.o (libgs): 3 `...` lines, 520 words parsed for a 526-word
object. It was listed as absent while 507 of its 507 non-relocated words match
the EXE exactly at 0x8005080C. Any object whose .text holds a run of >=3
identical words was invisible -- to the map the entire library-linking pipeline
consumes for placement.

That is why 2D_BG0.o was never linked: not excluded by a reason, just invisible.
It sits in config/splat.us.exe.yaml under a scattered-.bss exclusion that cannot
apply to it, since the object has no .bss section at all.

Reading the section bytes and taking relocation offsets from `objdump -r`
removes the pretty-printer from the loop (R33).

MEASURED: libgs goes from 36/201 to 46/201 objects located.
2026-09-03 22:35:12 -06:00
Drew T b31e499c9b fix(carve): repoint 800_b_2's INCLUDE_ASM paths to its own subseg
The 3-way split moved func_8002FDE8 and func_80032A74 into the 800_b_2 subseg,
but their INCLUDE_ASM directives still named "asm/nonmatchings/800_b". The
incremental build passed anyway because the OLD .s files were still on disk;
make clean removed them and splat now emits under 800_b_2, so a genuinely clean
rebuild died in jtbl_rodata_pads:

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

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

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

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

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

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

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

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

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

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

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:43:01 -06:00
Drew T 3005fc1239 fix(sync_tu_decls): a no-op sync is not progress, and a repeated symbol is not a blocker
replace_decl returned True whenever the PATTERN matched, even when the
substitution produced identical text. So a draft that already carries the TU's
exact spelling looped until --rounds ran out, spending ONE CLEAN REBUILD PER
ROUND, and then printed 'gave up after 6 rounds (6 synced)' — which reads as
six useful syncs.

Measured on func_8005FA94: 6 rounds, every one
'D_80072960 -> extern void (*D_80072960)(void *);', zero change to the draft,
five wasted rebuilds and a misleading report. R61(a): a no-op must not be
reported as work.

Two guards: no text change ends the loop naming the already-correct spelling
and saying the residual is elsewhere; and a symbol the gate names twice in one
run ends it too, since re-syncing it cannot help.

The comparison is LINE-NORMALISED because the pattern ends in \s*$ and the
substitution eats the matched line's newline — a byte compare called that a
change. Caught by a known-true check (identical/different/absent), not by
reading the code.
2026-09-03 20:38:59 -06:00
Drew T 8754b1a671 feat(decomp): bank main:func_800301C8 (170 ins), first-gate clean
18 -> 0 via three levers, all worth reading in the draft header: the sibling
func_8002FF0C's block-scope scalar spelling of D_800A46D2 (the array spelling
lets cse cache 'la $s1' across the call); splitting a $17 pin so only the
b*24 intermediate is pinned (expand_mult passes accum_target=target, and a HARD
target survives expand's generate-into-pseudos guard, so pinning the result
drags the whole chain); and pinning the DESTINATION for idx98, because
'addu $s0,$s1,$s0' is expand_binop swapping commutative operands to make
op0==target, not tree order.

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

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

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

gate_main: BANKED 1 of 3 after bisection, 143dbb89f34491258bbc27810d0a12ec8b43a8dd
BYTE-IDENTICAL. The other two are genuine rejects: func_8005FA94, and
func_8005D33C whose rejection shows the mass symbol shift its own agent
predicted from the jump-table rodata placement.
2026-09-03 20:24:44 -06:00
Drew T 48df1a900f config(wave_exclude): two 'compiler wall' entries were stale wrong-oracle verdicts
func_8005F0C8 and func_8005ECC0 both live in the 800c3 REORDER_TUS island,
whose real build path is reorder_passthrough + as -O2. Their S68 wall verdicts
were measured under maspsx + as -O1 — the oracle S76 fixed. Re-measured under
the correct path they are ordinary near-misses: 3 of 88 (ADDRESSING) and 2 of
35 (DELAY-SLOT), not walls.

Both stay excluded because neither is SOLVED (permuter_ils reached 3 and 5, not
0), but the reason now says UNSOLVED rather than impossible. An exclude list
records what the tooling could not do; a wrong reason is how real work gets
filtered out permanently.

Found because a wave agent noted that six prior 'IMMOVABLE §177/§188 epilogue'
verdicts on func_8005FA94 were all wrong-oracle artifacts — the same
provenance, so the same suspicion applied to the neighbouring entries.
2026-09-03 20:21:42 -06:00
Drew T 1f2ae12b5d feat(decomp): bank main:func_8001FC08 (400 ins) and func_8002FF0C (166 ins)
Both bodies were already solved in S76 and had never banked. Neither needed a
codegen change — they needed the gate to stop applying a rule cc1 does not
(§481 / the _depth0 fix): func_8001FC08 renames its struct to MTX_8001FC08 and
declares D_80074818/D_80075018 at block scope, and func_8002FF0C shadows
D_800A46D2 with a block-scope scalar because the array spelling forces la and
costs 12 mismatches.

gate_main: BANKED, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 20:21:22 -06:00
Drew T 39ac37a808 fix(gate_main): the in-TU clash pre-check must only compare FILE-SCOPE declarations
DECL is `^\s*extern`/MULTILINE, so it matched an INDENTED extern inside a
function body, and the pre-check then compared that block-scope declaration
against the TU's file-scope spelling — making the checker STRICTER THAN CC1.

Per cookbook §481, gcc-2.7.2 raises `conflicting types' as an ERROR only in the
SAME scope; across scopes it degrades to `type mismatch with previous external
decl', a WARNING the build already emits elsewhere. So a block-scope extern
cannot clash, and dropping on one refuses correct work.

Measured in a single gate: func_8001FC08 (400 ins — a deliberately renamed
MTX_8001FC08 at block scope, which is the ONLY legal fix there because two
anonymous struct typedefs in one TU are never compatible in C89) and
func_8002FF0C (166 ins — a deliberate block-scope scalar shadow of
D_800A46D2). 566 instructions of byte-correct body refused by a rule the
compiler does not apply.

_depth0() blanks brace-nested regions, string literals and comments before
DECL runs, on both the TU side and the draft side.

NEGATIVE CONTROL (R39): on a synthetic TU it keeps both file-scope decls and
excludes the block-scope, in-string and in-comment ones; on the two real drafts
it removes EXACTLY the three disputed symbols (D_80074818, D_80075018,
D_800A46D2) and leaves all 23 other declarations in each untouched.

R39 governs the direction: a check that discards good work is worse than one
that lets a failure through, and a real conflict still surfaces via the
COMPILE-conflict path plus a byte gate that cannot be fooled. R61(b).
2026-09-03 20:20:56 -06:00
Drew T 5b8a0d0804 feat(decomp): bank main:func_8005D538 from the S77w wave
Plain C, no §265 verbatim needed — the pack's prior FAILED attempt had
over-thought it. Two levers: omit the forward decl for func_8005E188 so the
call is implicit K&R (fixing both an s0/s1 order swap and a spurious
sign-extend a visible prototype would insert), and close the 2 trailing pad
words with a file-scope __asm__ per the §295 class.

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

func_8005EAE8's agent resolved its own integration conflict — it adopted the
TU's declarations for func_8005DCA0 and D_80072974 and cast at the use site
rather than declaring its own incompatible externs.
func_8005E13C reproduces a trailing orphan pad nop (belonging to neither it nor
SysEnqIntRP) with a file-scope __asm__; the verbatim detector correctly does
NOT flag that as a §265 body.
2026-09-03 20:15:17 -06:00
Drew T 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 e059f85298 config(wave_exclude): pin the 4 §332 delay-slot walls wall_sweep names in main
wall_sweep --emit-exclude lists 9 fleet-wide; the list carried 5 of them.
func_8005D734, func_8005D8B4, func_8005ED4C and func_8005F450 each have a
%lo in a branch/jal delay slot — the second half of an assembler macro gcc
emits as ONE atomic insn, so no C can place it there. An agent handed one
returns a NEAR with an unexplainable tail, which is indistinguishable from a
hard function; the playbook measured a main wave spending 4 of 7 slots that way.
2026-09-03 20:03:15 -06:00
Drew T 90042c111d rules(R61): not-judged is not a verdict; a draft-judging tool must model the real pipeline
Drew ratified in-session after the S77 census: eight instrument defects, all one
shape — a tool asserting about a DRAFT what was true only of the HARNESS.
2026-09-03 20:01:28 -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 a0c855648c feat(decomp): bank ov_SC01_084:func_80182A00 (§378 chain, 207 ins)
harvest_verify: verified 1 / failed 0, ef86fe1e403998a82ead42f4466ac4bc80f2c8d1
BYTE-IDENTICAL. The static probe had called this a `local_type' Blk16 conflict;
the real gate strips TU-provided typedefs and then named the true blocker.
2026-09-03 19:42:05 -06:00
Drew T 534979b4e7 plumb(ov_SC01_084): §378 self-caller casts for func_80182A00
harvest_verify named the step-2 signature exactly: `too few arguments to
function func_80182A00' at ov_SC01_084_jr_80182A00.c:534. 4 call sites cast.
Baseline green with NO draft substituted: ef86fe1e403998a82ead42f4466ac4bc80f2c8d1.
2026-09-03 19:41:45 -06:00
Drew T 337a040047 feat(decomp): bank main:func_80024054 via permuter ILS (DELAY-SLOT/2, 4 of 91)
Score 0 on cycle 1. gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:39:54 -06:00
Drew T a614d972c2 feat(decomp): bank main:func_80040DE8 via permuter ILS (REGALLOC-PERM/$t1>$v1, 2 of 347)
Score 0 on cycle 1. gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:28:55 -06:00
Drew T 94b528b54e feat(decomp): bank main:func_80021174 via permuter ILS (SCHEDULE-REORDER/2)
The residual was a two-instruction adjacent swap in the target's favour:

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

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

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

gate_main: BANKED 1, 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL.
2026-09-03 19:28:06 -06:00
Drew T 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 c91c9dffee feat(decomp): parallel gate — 12 fns across 12 binaries (8 workers)
ov_SC03_111    func_80181344
  ov_SC01_006    func_8017FBCC
  ov_SC02_041    func_801832F8
  ov_SC03_124    func_8018095C
  ov_SC01_005    func_8017FBCC
  ov_SC04_002    func_80182CBC
  ov_SC03_105    func_8017F018
  ov_SC04_005    func_80185CEC
  ov_SC04_007    func_80182358
  ov_SC04_011    func_8018985C
  ov_SC05_018    func_80181294
  ov_SC05_003    func_80181720
2026-09-03 19:18:12 -06:00
Drew T 408f826f29 fix(blocker_probe): a verbatim draft is not a decompile
A §265 verbatim draft — the target's own asm in a file-scope __asm__ —
assembles to the bytes it was copied from, so BOTH of this tool's oracles emit
the strongest possible signal: static `none`, real cc1 `MATCH`. The routing
then reads "byte-correct body, nothing blocking it", the byte gate refuses it
for free, and progress.py moves by exactly zero.

Measured: of the 13 MATCH rows in the S77 overlay pool, SIX were verbatim
(md_MAIN_003 x3, md_MAIN_020, ov_SC05_005, ov_SC06_010). The whole 13-draft
cohort gated 0, and the probe had scoped it as the highest-value work
available.

S76 closed exactly this hole in gate_main, harvest_verify and
api_agent.prior_draft. This is the fourth consumer — and the one that SCOPES
the work, so it is the one whose blindness costs a session's plan. Uses the
gate's own detector (DP.is_verbatim_asm_draft) so the two cannot drift (R33),
and a VERBATIM row is excluded from the agreement arithmetic rather than
counted as a match.

NEGATIVE CONTROL (R39): md_MAIN_020's verbatim draft now reports
`verbatim_asm / VERBATIM (not a decompile)`; ov_SC04_018's two real-C drafts
still report `none / MATCH` exactly as before.
2026-09-03 19:16:51 -06:00
Drew T 3c34f8f4a3 plumb(overlays): §378 self-caller casts + decl sync for 12 self_decl_tu drafts
The same class that produced four banks in main, applied across the overlay
fleet. `blocker_probe` over all 26 binaries holding a stranded S76 draft found
11 whose blocker is `self_decl_tu`; harvest_verify named a twelfth
(ov_SC03_111:func_80181344, `conflicting types for func_80181344').

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

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

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

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

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

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

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

The TU spells D_80072978 as a pointer-to-function; the draft had guessed `s32`
and cast at the use site. The TU's spelling is authoritative and the cast still
folds, so the bytes are unchanged.
2026-09-03 18:57:31 -06:00
Drew T 454d3878bc fix(sync_tu_decls): a definition is a declaration; a gate refusal is not a verdict
Two defects, both found by driving the last two self_decl_tu drafts to a bank.

1. tu_decl looked only for an `extern … sym …;` line, so when the clashing
   symbol is a function the TU DEFINES it stopped with

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

   though the authoritative spelling was in the definition's own header at
   src/800c3.c:916. This was the terminal blocker of BOTH remaining drafts
   (func_8005E3AC on func_8005E480, func_8005E79C on func_8005E804). The
   definition is now preferred over an extern when both exist — it is the one
   cc1 checks every other declaration against. Banked func_8005E3AC in one
   round. Checked against known-true cases before being trusted: definition
   path on func_8005E480/func_8005E804, extern path still verbatim on
   func_8005D734, absent symbol still None.

2. gate_main refuses outright on a dirty src/ or a red baseline and never
   reaches a per-draft opinion. The round loop matched neither DROP_RE nor
   COMPILE_RE in that output and fell through to "no declaration conflict
   named; stopping after 0 sync(s)" — reporting a HARNESS refusal as a property
   of the DRAFT (R40). Measured on func_8005E79C, whose gate was refused
   because the bank one command earlier had left src/ uncommitted. The refusal
   is now surfaced and exits 3.
2026-09-03 18:56:21 -06:00
Drew T 442b3ce651 feat(decomp): bank func_8005E3AC — a definition is a declaration
sync_tu_decls looked only for an `extern … sym …;` line, so when the clashing
symbol is a function the TU DEFINES it reported

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

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

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

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

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

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

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

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

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

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

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

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

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

Committed ahead of the gate because gate_main `git checkout`s main's TUs
before substituting and would otherwise destroy these edits. Journal at
.run/S77_selfcast.json — `--undo-journal --keep <banked>` follows the gate.
2026-09-03 18:49:56 -06:00
Drew T c04d5e0093 fix(cast_self_callers): --sync-decls must emit a declaration the TU can parse
`--sync-decls` copied the draft's parameter list verbatim into the TU. A draft
names types that are not in scope where the declaration sits, and both forms
of that broke the COMMITTED baseline build in one apply:

    src/800.c:2631   extern void func_80015760(Obj_80015760 *obj, s32 *ot);
                     -> the type is draft-local; the TU has never heard of it
    src/800c3.c:866  s32 func_8005E3AC(Ctx *s, s32 size);
                     -> `Ctx' is typedef'd at line 941, 75 lines BELOW the decl

    src/800c3.c:866: parse error before `*'
    src/800.c:2631: parse error before `*'

Caught by gate_main's BASELINE RED check with no draft substituted, so the
failure was attributed to the plumbing and not to seven innocent drafts.

THE FALLBACK FOLLOWS THE TOOL'S OWN DOCTRINE. Once the call sites are cast, the
declaration emits no code; it only has to be COMPATIBLE with the definition and
PARSE. `<ret> fn();` satisfies both without naming a type, and C89 6.5.4.3
makes it compatible with a prototyped definition exactly when no parameter is
affected by the default argument promotions. So the draft's own spelling is
still preferred — it is the byte-proven behaviour and it keeps the declaration
informative — and the no-proto form is used ONLY where that spelling cannot
parse at that line. Where it cannot parse AND a narrow parameter forbids
no-proto, the tool refuses loudly and names the type (R43).

NEGATIVE CONTROL (R39) over all 40 main recovery drafts: 68 edits before and
after, 65 byte-identical. The three that changed are exactly the declarations
naming an out-of-scope type — Obj_80015760, Ctx, and Slot54/Rec14 — and no
already-correct declaration is churned.

BASELINE PROOF: with all 14 plumbing edits applied and NO draft substituted,
main builds 143dbb89f34491258bbc27810d0a12ec8b43a8dd — byte-identical. The
casts move zero bytes, as the §20 fold predicts.
2026-09-03 18:49:56 -06:00
Drew T 1b648fcc5b Revert "plumb(main): §378 self-caller casts + decl sync for 7 self_decl_tu drafts"
This reverts commit commit:3801.
2026-09-03 18:48:28 -06:00