BANKED 20 of 21 after bisection in 11 rebuild(s)
143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL
rejected: ['func_8005E13C']
src/800c3.c INCLUDE_ASM stubs 36 -> 16. These are libapi/libcard C functions
that had been banked as §265 verbatim __asm__ blocks and were unreachable by
every gate (a verbatim body has no INCLUDE_ASM to substitute).
The chain that unblocked them, all this session:
* the triage identified the class and showed the "§332/§188 wall" is the §332b
-O2 reorder island, which landed 2026-09-01 -- one day BEFORE four of these
were banked as assembly, with "6 of 53 at closeness 0" drafts in hand;
* REORDER_TUS was extended to 800c2_2/800c2_3 ($(filter) is an exact stem
match, so 800c2 never covered them), proven byte-neutral by --assert-baseline;
* verbatim_to_stub converted 20 bodies back to stubs, byte-neutral;
* gate_main was fixed twice -- it destroyed uncommitted work, and my own first
guard sat inside the bisection loop where it tripped on the gate's own
substitution.
Drafts were already on disk from waves m04-m16 and s67m1; not one needed
redrafting. This is the §451 lesson paying out: the evidence was recorded, and
what was missing was a tool able to reach it.
These are libapi/libcard C functions in src/800c3.c that were banked as §265
verbatim __asm__ blocks. The triage (.run/S75/triage/report.md) established they
carry the §188 shape (`jr $ra` with `addiu $sp,$sp,+N` in the slot) and that the
"§332/§188 wall" is the §332b -O2 reorder island, which landed 2026-09-01 --
one day BEFORE four of them were banked as assembly, with the commit itself
recording "6 of 53 at closeness 0" stored drafts.
Converting them to stubs makes them reachable by every gate again (a verbatim
body has no INCLUDE_ASM to substitute, so gate_main drops it as "resolved to NO
stub") and is the honest accounting: a stub counts as OUTSTANDING WORK, a
verbatim body counted as banked.
BYTE-NEUTRAL, PROVEN: make extract + make build BINARY=main ->
143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL. INCLUDE_ASM pastes the
same assembly the block transcribed, so it must be -- but "must" is a claim and
this was gated, not assumed.
Metric moves honestly: VERBATIM __asm__ bodies 173 -> 148, INCLUDE_ASM stubs
34 -> 53.
20 of 24 converted. The 4 refusals are reported, not silent: func_800623A4 /
func_80062434 / func_8006252C (800c2_2) and func_8005D8A0 -- the last being the
row all three of asm_in_c's detectors missed, which is its own finding.
Also fixes verbatim_to_stub to CASE-NORMALISE the address. splat's convention is
func_%08X but analysis artifacts carry lowercase (the triage taxonomy does), and
asking for func_8005ed4c found 0 of 24 blocks that were all sitting right there.
An address is a NUMBER; matching it as a case-sensitive string is R48 in its
case-sensitivity form.
The largest open function in the project, carried as the §434 WALL since Phase
31 opened, is now real C. main SHA1 143dbb89... BYTE-IDENTICAL.
THE WALL WAS NOT A PROPERTY OF THE CODE. A whole-binary census of every .s for
the interior labels and raw addresses 0x8002B154..0x8002C31C found EXACTLY TWO
sources, and both are func_8002B0B4 itself: its own beq/j to .L8002C2A8 /
.L8002C2AC / .L8002BFE4, and its own jtbl_80072E44 (36 entries, entry 0 =
0x8002B154). Nothing else in the binary references the range.
So func_8002B0B4 (40 ins, prologue + dispatch) and SaveLoadRoutine (1,139 ins,
epilogue) are ONE function sharing one 0x40 frame -- entry func_8002B0B4, five
overlay callers, all s32 f(s32, s32, void *). SaveLoadRoutine is `case 0:` of
its state switch; .L8002C2A8/.L8002C2AC are the switch exit and .L8002BFE4 the
outer `case 1:` body. The "no epilogue of its own / must stay file-scope
__asm__" note that parked this for a phase was describing a SPLAT SYMBOL
BOUNDARY, not a code structure. The predicted "middle path" (decompile one while
siblings stay asm) was moot: there are no siblings.
The three "save/load handler code pointers at saveHeaderTemplate+0x54" in
docs/memory-map.md are jtbl_80072E44[0..2] -- confirmed against
dumps/ram_savescreen.bin (0x80072E44/48/4C = 0x8002B154/1AC/BEA4). The S73
correction to that row is right; no further RAM capture was needed.
Verified three ways: match_one MATCH (1179 ins) on a merged target .s; `make
extract && make build BINARY=main` -> 143dbb89...; and gate_main's own
clean_build() sequence driven from Python -> "sha1 143dbb89... == check.us.sha
(BYTE-IDENTICAL)". Independently re-checked here: tools/asm_in_c.py reports
NEITHER symbol as assembly-posing-as-C, so this is genuine C (the 94 __asm__
occurrences in the TU are §3a zero-byte cross-jump barriers, which are C).
TWO NEW TOOL DEFECTS, logged not fixed (main is busy; next session):
* gate_main.py:710 runs `git checkout -- <main TUs>` immediately BEFORE
substitute(). For a function whose current form is a hand-written __asm__
block that restores the block NEXT TO the C, the TU then carries 9 jump
tables instead of 5, and gate_main REJECTS a byte-identical bank. It cannot,
by construction, bank anything in the verbatim class.
* gate_main's error filter (error|undefined|conflict|...) does not match
jtbl_rodata_pads' sys.exit refusal, so that failure shows only warnings.
HAZARD, and why this is committed immediately (R42): any gate_main run on main
wipes this bank via that same line 710.
Ten measured levers for the body are in .run/S75/slr_c/cookbook_448.md pending a
cookbook merge, headed by: when a frame check says "no prologue / no epilogue",
build the MERGED .s and decompile the pair as one function before excluding
anything.
THE BLOCKER. asm_in_c.py found 147 GAME functions that are §265 verbatim
__asm__ bodies. NONE of them could be worked on: splat emits
asm/nonmatchings/<subseg>/<fn>.s only for functions that are still INCLUDE_ASM
stubs, and a verbatim body is not a stub -- so splat stops emitting its .s,
while match_one and rtu_match BOTH consume one. Measured: 1 of 147 had a target
on disk. The class was unworkable because the information was in the wrong FORM,
not because it was missing.
verbatim_target_s.py regenerates a splat-format target .s from the EXTRACTED ROM
IMAGE -- never from the __asm__ block in our own source, because the block is
the thing under test and a target derived from it would agree with the candidate
by construction (R34). 146 of 147 emitted; the 1 refusal is REPORTED.
TWO DEFECTS CAUGHT BY CHECKING AGAINST A KNOWN-TRUE CASE, both of which would
have shipped ~147 silently-wrong targets:
* BYTE ORDER. splat writes the four bytes as they sit in the image
(`C8FFBD27` for instruction 0x27BDFFC8) and masked_diff.insns_from_s reads
the column with struct.unpack("<I", bytes.fromhex(...)). objdump prints the
VALUE, so reversing double-swaps: 91 of 1139 words agreed with splat's own
.s for the same function. The LENGTH matched perfectly, so nothing except a
word-level cross-check could have caught it.
* `-z` / --disassemble-zeroes. objdump ELIDES runs of zero bytes as `...`, and
a MIPS nop IS 0x00000000 -- so every nop vanished. func_80049610 (three
nops) disassembled to ZERO instructions; func_80047D3C 31 of 36. The length
assertion caught all of them, which is the only reason this was not shipped
as ~30 quietly-truncated targets.
Verification: regenerated SaveLoadRoutine target is 1139/1139 words IDENTICAL to
the .s splat itself emitted for the same function.
ALSO BANKED: ov_SC06_025:func_8017DB98 (122 ins). Its body was byte-exact on
disk since S71 and the blocker was one word: the TU declared
`extern void func_8017DB98(s32, s32)` where the epilogue is `addu $v0,$s3,$zero`
-- must be `extern s32`, and the caller discards the result so the change is
byte-neutral. That line number and fix were recorded in the agent journals the
whole time; frontier_classify only surfaced it once journal_notes was wired in
as a second oracle earlier this session.
Two banks from the S75 redraft workflow (7 overlay functions, one agent each,
every claimed MATCH re-verified by an independent agent instructed to refute
it). Both were carried as F-FAR "a draft exists but is materially wrong":
func_801806F8 ov_SC03_105 241 ins (recorded closeness 235)
func_80180ABC ov_SC03_105 257 ins (recorded closeness 250)
Neither needed a better model. Both needed the recorded closeness not to be
believed -- see below.
frontier_classify.py, THREE fixes, each caught by testing against a case whose
answer was already known:
1. BEST closeness, not LAST. .run/backlog.jsonl is append-only, one row per
attempt across every lane and session, so the last row is evidence about
THAT LANE'S SEED, not about the function. Caught func_80180B3C (best 125,
last 287) and moved func_80181294 from "redraft" to "permuter" (best 19).
The draft that ACHIEVED the best score is kept, not the last one written.
2. journal_notes.py wired in as a SECOND, DISAGREEING oracle (R34). The backlog
does not have what the agent journals have. Measured on func_8017DB98:
backlog best == last == 115, so best-vs-last could not help, while the
journal holds "Attempt 2 (MATCH · closeness 0) ... MATCH 122/122 ... BANK
BLOCKER is TU plumbing, not the body (§376/§378)" WITH the draft path and the
exact declaration to change. Reclassified 37 functions; G-DRAFTED-UNKNOWN
fell 47 -> 10 and a new C-PLUMBING class holds 16 functions / 1,547 ins whose
BODIES ARE PROVEN and are blocked only by the TU.
3. A consuming-regex bug in my own extractor -- the session's signature defect,
committed a third time in the tool written to find it. The first cut used
`re.finditer(r'\*\*Attempt \d+\*\* \(([^)]*)\)(.{0,400})', ..., re.S)`, whose
400-char body window SWALLOWS THE NEXT ATTEMPT'S HEADER, so every record
following another was invisible. On func_8017DB98 it hid attempts 2 AND 6,
both `MATCH · closeness 0`, and returned attempt 1's NEAR (2) as the best --
exactly the records the oracle exists to find. Now splits on the marker
rather than consuming past it. A regex that consumes an unbounded body cannot
enumerate the items after the first.
Rows now carry attempts, closeness_last, journal_closeness, and a
!!WARMSTART-REGRESSION flag when a later attempt scored materially worse than
the best -- the shape a wave's warm-start regression makes, which from inside
the wave is indistinguishable from an unsolved function.
Gate ledger for the batch of 7: 2 banked, 3 near, 2 failed. func_800CB00C failed
despite being adversarially upheld -- it owns a jump table, and both matchers
compare .text only, so a verified .text MATCH proves nothing about table
placement (the agent's own write-up says so).
tools/jtbl_rodata_pads.py --derive walks a TU's rodata emission against the
retail island and validates only what it can SEE. A §265 verbatim-__asm__ body
emits its tables as `.section<TAB>.rodata` + `jtbl_xxxxxxxx:`, and the walk
missed BOTH spellings:
* the rodata directive was matched as the literal one-space string
".section .rodata" / ".rdata", so a tab-spelled directive never entered
rodata at all;
* inside rodata the anchor regex accepted only `D_xxxxxxxx` (the S74 dlabel
fix was one prefix short), so a `jtbl_xxxxxxxx:` label was invisible.
Consequence, traced: the walk skipped the block as if it were .text, every
later C table walked 104 bytes behind its retail address, EVERY WORD in that
range happens to be a valid code address so the entry guard never fired, and
the walk stopped short of the island's single zero word -- so the one trailing
pad was never emitted and the image linked 4 BYTES SHORT. That produced 3,989
differing bytes on a body the verdict layer had already called byte-identical.
Fixed: tokenised directive match (.rdata / .section .rodata, tabs and commas);
anchors keyed on the ADDRESS IN THE NAME for `D_` or `jtbl_`, with an
address-suffixed label of any other prefix now REFUSING loudly (R43) instead of
becoming a silent hole; and `.align N` modelled SECTION-RELATIVE from the walk
origin, as `as` does -- needed for `.align 3` when a section starts = 4 mod 8,
which span B (0x80072E44) does.
Negative control: old vs new derive over ALL 162 md_*/main derive-path TUs ->
160 byte-identical post-derive streams with identical exit codes, 0 DIFF; 2 SKIP
(800c2/800c3 are REORDER TUs with no derive stage).
main SHA1 143dbb89... BYTE-IDENTICAL, 413,696 bytes, cmp identical to retail.
WHAT THIS IS NOT. SaveLoadRoutine is banked as a §265 verbatim __asm__ block --
BYTES, NOT A DECOMPILE. Its 1,165 instructions are byte-correct and unexplained.
tools/progress.py correctly REFUSES to count it, reporting `UNPLACED (parse
hole)` rather than inflating REAL (which moved 880 -> 881 on func_8005DCA0
alone). Two such blocks already exist in this TU, documented as necessary
because those functions have no epilogue and fall into shared tails. A real C
decompile is now being attempted separately; this commit is the revertible
byte-green base for it.
One clean-rebuild gate_main pass over main's stored drafts. 35 drafts on the
slate -> 20 compatible after in-TU declaration resolution -> 1 byte-correct,
found by bisection in 24 rebuilds. main SHA1 143dbb89... BYTE-IDENTICAL.
banked: func_8005DCA0 src/800c3.c 118 ins
THE HEADLINE IS NOT THE BANK. gate_main's per-function verdicts separate a BODY
reject from a PLUMBING reject, and they say:
SaveLoadRoutine: PLUMBING REJECT — SaveLoadRoutine is BYTE-IDENTICAL; all 3989
differing bytes are ELSEWHERE IN CODE. The substitution perturbed other
functions (§376 — a stale forward declaration changes caller codegen).
SaveLoadRoutine is 1,165 instructions -- the largest function left in the
project, 9.2% of everything remaining, and carried as the §434 WALL. Its body is
already correct. What rejects it is a forward declaration perturbing OTHER
functions' codegen, which is exactly what fix_arity_callers -> cast_self_callers
exist to repair. That is a plumbing job, not a matching job.
Three genuine BODY rejects, correctly distinguished by the same verdict layer
(divergence confined to the function itself): func_8001EFE0 (495 bytes),
func_80015B6C (151), func_80011380 (8). Those drafts are really wrong.
Honest read of the yield: 1 of 20 substituted drafts was byte-correct, so main's
stored drafts are mostly NOT right. This tempers the "the endgame is integration,
not drafting" line from the previous commit -- true for the overlays, only
partly true for main, where several drafts need redrafting or permuter work. The
distinction is now measured per function rather than assumed.
Deferred to the reconcile chain, not discarded (11 dropped for in-TU decl
conflict + 4 dropped across rounds to let the TU compile at all): func_800226C0
(670), func_800215F4 (465), func_8001FC08 (400), func_8005F290 (61) and the
gate's own 11. All drafts remain on disk.
func_8016AE5C (ov_SC03_108) was logged "match_one MATCH but the whole-binary
gate rejected -- CAUSE NOT DETERMINED". Determined: the body is byte-perfect (0
differing words inside the function; all 1,168 diffs are uniform +0x20 shifts
outside it) and it emits an 8-entry jump table that was never carved. It banked
unchanged the moment the §446 jtbl_carve per-table bound landed.
tools/frontier_classify.py (NEW) — classify every open stub by its TRUE BLOCKER
from artifacts already on disk (R33/offline-tooling-first: zero tokens, no
agents, no builds). "69 functions left" is a stub count, not a difficulty
measure, and routing drafting agents at carve or plumbing problems wastes them.
A-TWIN-REMAP 3 302 a byte-identical copy is already banked elsewhere
B-CARVE 11 3,301 owns a switch jump table -> the §446 class
D-NEAR 2 106 closeness <=25 -> permuter fuel, not drafting
F-FAR 3 223 draft materially wrong -> redraft
G-DRAFTED-UNK 49 8,724 drafted before, no usable verdict on record
H-VIRGIN 1 1 never drafted (and it is a DATA BLOB, not a function)
68 of 69 remaining functions already have a draft on disk. The endgame is a
verification/integration problem, not a drafting one.
TWO SELF-INFLICTED DEFECTS FOUND BY CHECKING AGAINST KNOWN-TRUE CASES, both the
session's recurring shape (a scan narrower than the claim it supports, R32):
* The sig directory is NOT the fleet. Alongside the 213 real binaries `.run/`
holds `SLUS_007.26` (a STALE duplicate of main under the ROM filename),
`resident_image`, and two CROSS-BUILD binaries (`sep8_SLUS_007.26`,
`aug31_USA_DEMO.EXE`). Counting them as peers reported 38 fns / 7,516 ins of
free twin-remaps -- mostly main "already banked" in ITSELF, the rest proven
in a PROTOTYPE that R13 forbids as evidence. Now derives the fleet from the
Makefile and prints what it ignored. True figure: 3 fns / 302 ins.
* The draft scan globbed `.run/S7*` only, missing `.run/S69m2`, `.run/S68m1`,
`.run/s67m1`, `.run/wave_ds2`, `.run/gate_lane`, `.run/backlog_drafts`. All
32 drafted main functions read as "never drafted", which would have sent
agents to redraft 6,328 instructions that already have drafts. Now one
pruned os.walk of .run (worktrees excluded -- 7.4 GB of duplicate sources).
Honest negative result: resident:func_800D06E8 (344 ins) did NOT bank. I
predicted the carve fix would clear it; it did not. Its blocker is still open.
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.
resident:func_800D00E4/func_800D02D0/func_800D0488 + ov_SC07_002:func_80180248, all byte-verified
from clean rebuilds (resident 8e17e02f, ov_SC07_002 fad71342) and counted from the SOURCE.
ov_SC06_029's two are re-gated separately against HEAD — this agent's worktree predated five banks
there, so its numbers for that binary no longer apply.
TWO OF THE SIX NEEDED NO CARVE WORK AT ALL, AND CARVE-REFUSED WAS AN INSTRUMENT VERDICT.
ov_SC07_002:func_80180248's table is ALREADY inside a carve bound to its own subseg: in stub state
spimdisasm migrates the table into the fn's .s and the object fills the piece exactly, so banking
just swaps that block for cc1's identical one. `island_probe` classified it `tail` on the table's
ADDRESS, `apply()` routed it to build_carve, which resolves spans out of the RAW data asm where a
carved table no longer is -> "not found in the raw data asm" -> harvest_verify booked CARVE-REFUSED.
A verdict about the route we chose, not about the function (R43). jtbl_carve now has a `covered`
verdict (table inside an existing carve bound to the fn's OWN subseg) and a `covered-tpad` wall (the
retail copy carries a trailing §8a pad the matched body won't emit — bankable, needs a `0t<n>`
entry); a fully-covered batch is a no-op before either route.
THE RESIDENT CAN CARVE LIKE AN OVERLAY. Its three tables are adjacent and lead the island
(0x450e0..0x451ac, one span, all in subseg `resident`). The genuinely new part: the resident opens
with `- [0x0, rodata, hdr]`, a 1-word .rodata header BEFORE the code, so its layout is
rodata -> text -> data -> rodata(carve) -> data, which `ld_interleave --order` cannot express (every
listed piece lands after TEXT_START, and hdr.rodata.o would fall into the unchecked `empties` bucket
and be parked after the text, moving every byte). New `--pre` places a leading-rodata piece ahead of
the text; resident_JTBL_INTERLEAVE uses it.
NEW LAW, BYTE-PROVEN (§8b was over-strict — EXTEND the carve, do not isolate): a .rodata carve piece
binds to a code SUBSEG, not a function, and the object's .rodata is the address-ordered
concatenation of cc1's tables for BANKED functions and still-stubbed functions' MIGRATED tables. So
a span may legitimately hold a MIX, and extending a carve across an align-pad word and two unrelated
STILL-STUBBED tables was byte-identical with nothing banked — where the tooling demanded a
jr-isolation. Corollaries, all measured: migrated tables self-align (spimdisasm emits `.align 3` iff
the table's SPAN-RELATIVE offset is 8-aligned), so stubbed tables need no spec; JTBL_PADS counts cc1
tables only, so a mixed span's spec GROWS as each sibling banks; and the zero-word rule is INVALID
across a migrated boundary, because that zero is supplied by the preceding migrated block.
ALSO REPORTED, NOT FIXED (harness gap worth its own change): verify_worktree.provision omits
`.run/sig.<bin>.jsonl` — main clone 259 files, provisioned worktree 0 — and jr_isolate_all's
carve-ownership scan swallows the resulting FileNotFoundError in a bare `except: continue`. Measured:
2603 of 2603 functions raised, the scan found 0 owners, and the run aborted with a CONFIDENT FALSE
verdict ("committed .rodata carve ownership is not 1:1 — stranded/duplicated carve"). Both resolve
instantly once the sigs are present. Any worktree-run isolation before that is fixed reports a
corruption that is not there.
Clean rebuild BYTE-IDENTICAL 87ac0de3; corpus.stubs('md_SC07_004') 10 -> 0, counted from the SOURCE.
THE §376/§378 CHAIN WAS NEVER NEEDED. Zero declaration edits: no fix_arity_callers, no
cast_self_callers, no --any-proto, no --sync-decls, no undo journal. `git diff -U0` on the TU removes
exactly the 10 INCLUDE_ASM stubs plus one hoisted typedef. Every recorded "declaration conflict" was
an INSTRUMENT defect. Four of them, all named, three patched here:
1. `CC1-FAIL(no-diagnostic)` was neither cc1 nor no-diagnostic. The failing stage was
`jtbl_rodata_pads --derive`, which prints to stderr AFTER cc1 exits 0 quietly. `_items` matched a
rodata anchor only as `D_xxxxxxxx:`, but a block written as inline `__asm__` in C arrives in the
labels.inc macro form `dlabel D_xxxxxxxx` — so an 8-byte hole opened in the walk and every C jump
table after it died with "island layout drift". The harness label was wrong twice: it said CC1
when the failure was a post-maspsx filter, and no-diagnostic when there was a precise one.
2. Same file, UNALIGNED ANCHOR: the ctable branch read `word(pos)` without first stepping the
sub-word zero gap, so a preceding `.asciz` ending at an odd address made it refuse a correct
layout. Now reuses the same zero_gap the anchor branches already use — a no-op wherever pos is
already aligned, i.e. everywhere that builds green today.
3. `harvest_verify` computed the typedef strip-set UNSCOPED: `cdecl.typedef_names(path)` without
`above=fn`, which that function supports for exactly this. A typedef declared BELOW the splice
point got stripped out of the draft that needed it -> `parse error`, logged as PLUMBING and
indistinguishable from a real conflict. One line.
4. NOT PATCHED, AND THE MOST IMPORTANT ONE: `reconcile_tu.py` (gate_stage's `-rc` stage) MANUFACTURED
both remaining "conflicts". The same drafts gate 9/9 byte-identical through harvest_verify and
7/9 through gate_stage. Isolated stage by stage, `-rc` (a) rewrites deliberately BLOCK-SCOPED
externs to a file-scope spelling whose typedef is declared ~2,800 lines lower — overwriting the
TU's own byte-proven house style, which three already-banked functions in that file use; and
(b) substitutes identifiers TEXTUALLY, including inside comments and inside `&`-expressions,
emitting `*(T *)&((s32 *)&D_800AE620)`. A correct draft using the block-scope-extern idiom
currently CANNOT survive gate_stage. Left for a deliberate fix: `--stages` should be able to skip
reconcile_tu, or a draft should be able to opt out.
The two surviving non-stub source edits are byte-neutral (proven by the SHA above): a §304
migrated-rodata re-emission (`D_801A01EC`, the exact form this TU already uses three times, needed
because banking the body deletes the .s that carried the island word), and one typedef moved up so a
function above it can see it (typedefs emit no bytes).
Also banked the 10th stub (func_801ADA10) that defect 3 had been silently blocking.
Regression-checked by the agent: main, md_MAIN_003, md_SC07_003, md_SC03_073, md_MAIN_011 all
rebuild BYTE-IDENTICAL. A full R22 follows before this session closes.
md_MAIN_011:func_800CF28C · md_MAIN_003:func_800D0268/func_800D0740/func_800D0C50, all byte-verified
from a clean rebuild and counted from the SOURCE: md_MAIN_011 is now FULLY MATCHED (0 open stubs),
md_MAIN_003 is down to 1 (func_800CF3E8).
THE PREMISE I HANDED THE AGENT WAS WRONG, AND IT SAID SO. md_MAIN_011 is already a whole-object -O0
module — no carve was needed. Its real blocker was tools/jtbl_rodata_pads._s_rodata_span ignoring a
trailing `.align`, the SAME defect this session fixed for md_SC07_003 from the other direction: two
agents converged on it independently. Adopted this agent's stricter form (only a TRULY trailing
align rounds `hi`; an interior one is followed by data that sets `hi` higher anyway).
Note WHY it stayed latent: `derive`'s zero_gap self-corrects a 1-3 byte undershoot whenever the next
stream item is an anchor. A C jump table has NO anchor — so the bug can only fire the moment someone
banks a switch function into such an object, and when it fires it accuses the CARVE ("island layout
drift"), not itself.
md_MAIN_003 needed one new -O0 object, and the boundary I proposed (0x1f74 -> 0x1e58) was both too
narrow and off by 0x2B8. The carve made is `md_MAIN_003_o0e` at 0x1308 (vram 0x800D0100) running to
the existing o0c boundary: everything in that span is a §265 verbatim __asm__ body or an INCLUDE_ASM
stub — zero optimizable C — so the whole tail flips with one cut. Proved byte-identical with NOTHING
banked first (§431 discipline), then the three drafts gated one at a time.
TWO MORE GENERAL DEFECTS FIXED IN jr_isolate_all, both of which silently mis-place a boundary:
* an item-less CLOSING region emitted a duplicate `- [off, c, …]` line and the validator refused;
the empty-region skip covered only region 0, and `_partition`'s empty `footer` made the closing
region look non-empty.
* A §265 VERBATIM __asm__ BODY IS PREAMBLE, AND PREAMBLE IS ASSUMED BYTE-NEUTRAL. It is not — it
emits bytes. `parse_overlay_c` has four addressed-anchor forms and a verbatim body is none of
them, so it attaches to the NEXT anchor: cutting at func_800D0268 would have moved 0x168 bytes of
other functions into the new object while the yaml claimed the region starts higher. New
`_region_emit_start()` derives the yaml offset from the region's CONTENT (item addresses + every
.globl/.ent the text names that resolves inside the object) and takes min(cut, emit), so a
boundary can only move DOWN. Where no verbatim asm is in play it equals the cut — every existing
isolate is unchanged.
BLAST RADIUS PROVEN, not argued: jtbl_rodata_pads is in the build path (`--derive` for md_*/main),
so the agent rebuilt main + all 70 md_* from scratch (71/71) and then ran the full fleet:
**make check-all 213/213 passed, 0 failed**, main 143dbb89 BYTE-IDENTICAL.
CENSUS, denominator asserted (1057 live stubs, 0 without a .s): exactly ONE -O0-prologue stub
remains stranded in an -O2 TU fleet-wide — main:func_8002C410 in src/800_b.c, 299 ins. Nothing more
should be built for this class; the general tool already existed and what was missing was
correctness, not coverage.
BYTE-IDENTICAL on all four, with NOTHING banked (clean rm -rf asm/<bin> + extract +
build -j + check), which is the whole point: the structure lands first and proves neutral,
then drafts bank against it. split_indicator: 213 OK, 0 needing attention, of 213 —
the CARVE-BLOCKED class is now EMPTY fleet-wide.
One code object contributes exactly ONE contiguous .rodata run, so a subseg owning raw
jump tables in two non-adjacent spans could carve only one of them and every switch
function in the other span was unbankable at any effort (cookbook §426/§431).
ov_SC01_084 2 pieces cut 0x80182A00 (0x5A8A8)
ov_SC02_005 3 PIECES cuts 0x80185060 (0x5CF08) + 0x80185E80 (0x5DD28)
ov_SC02_011 3 PIECES cuts 0x80183178 (0x5B020) + 0x80188E3C (0x60CE4)
ov_SC03_105 2 pieces cut 0x8018624C (0x5E0F4)
TWO OF THE FOUR NEEDED A CUT THE BRIEF DID NOT NAME, and the address evidence found it:
each already had a carve run that could not merge with span 1, separated by rodata that
is not padding — ov_SC02_005 by `0000F040 00000000` (8 bytes, twice the widest .align 3
pad the JTBL_PADS spec can emit), ov_SC02_011 by `FEBEF6AE 000002DC 0 0` (a TU's trailing
const data). A gap detector keyed on zero words would have merged them and produced an
unbuildable carve: the load-bearing test is "is this word a valid code address in this
overlay's text range", not "is it zero". ov_SC01_084's divider is real data too
(`0 FFFF0000 00080000 0 0`), while ITS span-1 gap word IS a zero .align 3 pad and merges.
OVERLAY SPLITS ARE NEAR-FREE, AND THE REASON IS STRUCTURAL — the opposite of main.
The Phase-26 §8b carried decl layer re-emits each region's externs locally, so only
typedefs cross a cut: 1 name of 2,679 (ov_SC01_084, 0 typedefs) · 5 typedefs of 44
(ov_SC02_005) · 2 names of 3,254, 0 typedefs (ov_SC02_011) · 0 of 3,074 with zero
compiler errors (ov_SC03_105). main's split moved 57 of 1,247. Every crossing typedef was
MOVED to a <bin>_shared.h, never copied, and every list came from the compiler (R33).
Carve probes (jtbl_carve --func, then reverted — carve state is added when a function
banks, never speculatively): all four subsegs now accept a carve with no fail-loud, and
jtbl_carve derived the §8e per-table pad specs the zero-word rule predicts.
Unlocks 17 open switch functions: ov_SC01_084 func_80182A00 · ov_SC02_005 func_80185060,
func_80185E80 · ov_SC02_011 func_80183178, func_80183630, func_8018418C, func_80188E3C ·
ov_SC03_105 func_801806F8, func_80180ABC, func_80180EC0, func_801813BC, func_801818E8,
func_80181C84, func_8018624C, func_801867D0 (+2 more span-1 owners).
CORRECTION, measured not assumed: config/wave_exclude.txt listed ov_SC01_084:func_80182328
as CARVE-BLOCKED and it never was — its table ABUTS the existing carve, so it always
merged into one run. Proven by control on the PRISTINE unsplit config: --func func_80182A00
exits 1 "would host NON-CONTIGUOUS .rodata carves", --func func_80182328 succeeds.
StreamLoadStateMachine (459 ins) byte-identical. That completes wave S73m_1: 9 of 9
drafts banked, 2,413 instructions.
gate_main defect this exposed, twice in one gate: resolve_conflicts scanned every 'extern'
line with no notion of the preprocessor, so a declaration parked in the DEAD half of an
'#ifdef NON_MATCHING / #else / #endif' pair read as a live constraint. It is never
compiled and constrains nothing. func_80018714 and func_800377D8 each carry a stale
'(void)' declaration in a dead branch while the LIVE definition takes a pointer and a u8
respectively; the first mis-blamed a draft, the second got a byte-verified draft DROPPED.
live_text() now blanks those branches before the scan.
The detour is instructive: I 'fixed' the draft twice to satisfy a constraint that did not
exist, and each fix made it worse — the draft's original (u8) declaration was correct all
along, because it matched the LIVE definition. Read which branch a declaration lives in
before believing it.
Both needed the §376 recovery in the DRAFT — adopt the TU's spelling for a symbol the
draft also declares:
* CdReadSectorReadyCB dropped its own 'extern void func_800599B8(void *rect, ...)';
the TU declares it (SpadRect_800184F0 *) at src/800.c:5480, above the insertion point.
* func_80035C4C adopted 'extern void func_8003D650(int,int,int)' — no caller anywhere
uses the return value, so the s32-vs-void difference was free to give up.
Also corrects src/800.c's dead-branch 'extern void func_80018714(void);' to '(void *)'.
That declaration lives inside #ifdef NON_MATCHING and is never compiled, but gate_main's
DECL scan has no notion of preprocessor guards and read it as a live conflict. The live
K&R definition at :5576 takes void *, so the correction makes the dead copy agree with
reality as well as clearing the false conflict.
Prepares the S73 wave's 9 byte-verified drafts for gating. Five definitions have
promotion-safe params so the declaration becomes K&R no-prototype — which also keeps
func_8003388C's 'Ent388C *' typedef out of scope at the declaration site, where it is not
yet defined. CdReadSectorReadyCB's u8 is NARROW so no-proto is unsafe (§17-stop); it gets
the exact prototype, safe because that symbol is only ever passed BY ADDRESS.
Verified BYTE-IDENTICAL with no draft substituted, via a DIRECT extract+build with the
binary deleted first — NOT via gate_main --assert-baseline, whose first action is
'git checkout -- src/*.c'. I used that first and it silently reverted these very edits,
then reported GREEN for a tree that no longer contained them: a verification of the
wrong thing. Same hazard as the two banks lost this morning, from the other direction.
Six of the twelve were found by checking every draft systematically rather than trusting
the agents' notes; two were never reported.
The split created two new TUs and a shared header; four consumers still described main's
game code as one file:
* tools/reconcile_slate.py — HARDCODED open('src/800.c'), so after the split it saw a
THIRD of main's typedefs while reporting success (silently-narrowed scope, R32).
Measured: 133 visible before the fix, 187 after, 0 lost. Now globs
corpus.src_files('main') + src/800_shared.h, so a future split is already handled.
* docs/wave-playbook.md 1c — still said spans B/C/D were NOT drawable and that drawing
one is an R45 violation. That is now false and would have STOPPED a future session
from drawing the very targets this work unlocked.
* cookbook §426 — its 'the remaining spans need src/800.c split' paragraph now records
that it was done the same session, and points at §431 for the method.
* config/dedup.us.yaml + src/shared/clearTbl40.h — both said dedup group I0 is
instantiated 'at both sites in src/800.c'; both sites are above 0x80035270 and are now
in src/800_c.c.
Byte-neutral: dedup.us.yaml parses, gate_main --assert-baseline BYTE-IDENTICAL.
SETUP.md gains a row describing the layout and the rule it implies: never hardcode
src/800.c, glob corpus.src_files('main').
Both banked byte-identical earlier this session ('BANKED 2 of 3'), then sat UNCOMMITTED
while my very next action was another gate. gate_main.try_batch's first step is
`git checkout -- src/*.c`, which reverted them; the following commit captured only the
third function. I reported 14 banks; the source said 12.
Caught by counting banks from the SOURCE (the INCLUDE_ASM stub's absence) rather than
from my own account of what I had done — the oracle the project already mandates.
I had written this exact hazard into cookbook §431 an hour earlier, for DECLARATION
edits, and did not apply the same reasoning to BANKS. R42 is not 'commit at a good
stopping point', it is 'commit before the next command that can touch src/'.
Its blocker was an extern the DRAFT carried for a different symbol
(func_8004355C declared (s32, void*) against the TU's (s32, u8*)). Adopting the TU's
spelling verbatim — the documented §376 recovery — banked it byte-identical in 10s.
Only func_800316F8 of the eleven remains, and it is a TABLE REJECT: .text
byte-identical, 18 bytes wrong in its own jump table (§405-A).
Byte-identical. 13 main functions banked this session.
func_800316F8 rejected with a precise, new-class verdict: its .text is BYTE-IDENTICAL
and all 18 differing bytes are its own jump table at 0x800730C4 (attributed to the
preceding jtbl_800730AC because the table is now a cc1 $L label, not a data symbol).
That is §405-A in the flesh — match_one compares .text ONLY, so a draft can sit at
closeness 0 while emitting a wrong table. Not a body reject and not plumbing either.
Each of these five had a TU forward declaration that contradicted the real signature,
which is what made gate_main drop their byte-correct drafts:
func_800316F8 void f(void*) -> void f(s32) + cast at the one call
func_8003602C void f(void) -> void f(s32) (use is address-taken)
func_80036260 void f(void) -> int f(void) (use is address-taken)
func_80038FFC void f(u8**) -> s32 f(s32*) + cast at the one call
func_80039C70 void f(void*,s16,u8) -> void f(u8*,s16,s16)
Verified byte-identical with NO draft substituted, so any later gate failure is
attributable to the draft and not to this edit.
OPERATIONAL NOTE: gate_main's first action is , so an
UNCOMMITTED declaration edit is silently discarded and the gate then judges the drafts
against the old declarations. Commit alignment work before gating (R42's shape, seen
from the tool's side).
func_8002EED8 · func_8002F248 · func_80031988 — byte-identical, the first banks that
span B's carve made possible.
gate_main defect the split exposed: defs_above scans the destination .c ALONE, so a
typedef the TU gets through #include is invisible to strip_dup_typedefs and every draft
carrying its own copy dies with 'redefinition of X'. Latent until src/800.c's split moved
19 shared typedefs into src/800_shared.h, at which point func_80031988 — byte-correct,
and one of the eleven — failed to compile for that reason alone. header_defs() now walks
the destination file's quoted includes transitively and seeds defs_above with what they
provide, so an identical copy is stripped and a different shape is renamed, exactly as
for in-file definitions.
func_80031988 had TWO stacked blockers: this one, and the struct-tag false conflict in
typesig fixed earlier today. Neither was a property of the function.
BYTE-IDENTICAL with NO function banked (gate_main --assert-baseline, clean rebuild),
which is the whole point: the structure lands first and proves neutral, then drafts bank
against it.
One code object contributes exactly ONE contiguous .rodata run, and 800.o's is span A,
so spans B and C each needed their own object:
800 vram 0x800123F0-0x8002B0B4 -> .rodata span A (0x80072A38-0x80072C70)
800_b vram 0x8002B0B4-0x80035270 -> .rodata span B (0x80072E44-0x80073140)
800_c vram 0x80035270-0x8003A444 -> .rodata span C (0x800732A0-0x8007344C)
The span owners' address ranges are disjoint and ordered — tables pack tight WITHIN a
TU and are separated by other data ACROSS TUs — so these are (at least some of) the
original translation-unit boundaries. Splitting here is both the fix and the minimum;
any extra split would be speculation.
main's island is now a 7-piece data->rodata sandwich, so ld_interleave moves from
--front/--tail to --order.
THE SPLIT WAS CHEAP, AND MY FIRST ESTIMATE WAS WRONG. I costed it at '2,318 scattered
extern lines' — that is the TOTAL; what matters is how many CROSS a boundary, and that
is 57 of 1,247 declared names (4.6%), of which 19 are typedefs with exactly one
definition each and zero shape conflicts. Zero file-local statics. src/800_shared.h
carries exactly those, derived from the COMPILER's own errors rather than a regex model
of C (R33), and each typedef was MOVED, never copied.
Unlocks 17 functions / 4,471 instructions = 39% of what is left in main, incl.
SaveLoadRoutine (1139) and func_8003388C (663).
CdReadStateMachine (385 ins) · func_80024448 (362) · func_80026D64 (189) ·
func_8001B0D4 (86). One clean rebuild, 10.8 s, 4 of 4 accepted.
Wave shape: 5 targets, one agent per workflow, 5 concurrent. 4 MATCH / 1 NEAR.
Every agent verified its jump table and reloc stream past match_one's .text-only blind
spot (§405-A) because the packs carried the §426 carve note.
Three new laws banked from their notes: §428 (zero-byte cross-jump barrier), §429
(every held pointer needs its own local), §428a (two residuals moving in opposite
directions share one starved resource — which refuted my own prediction).
func_80024448 was recovered from disk after its Fable agent was killed by a rate limit
and the workflow reported NO-DRAFT; match_one on that file: closeness 0 (playbook §5b).
main's gate could only ever say "got X want Y". S71 read 7 such verdicts as body
rejects and recorded 11 functions as "PROVEN gate-rejects, §376 in its purest form".
They are not: all 11 are switch functions, and the blocker is that main has had
exactly ONE rodata carve since Phase 7 (LZSS's jtbl_80072A38). Every other main jump
table stayed raw in the tail data, so a drafted switch DOUBLE-EMITTED its table, the
image grew (+28/+52/+76/+84 measured), and all 238 symbols above 0x80072A4C shifted.
* tools/main_diff_locate.py (NEW) — turns a red image into a named list of divergent
symbols via the linker map; per-byte attribution, self-test flips a byte at a known
address and asserts the containing symbol (plus the identical-pair direction).
* gate_main.py — PRESERVES the red image + map before the R40 baseline control
rebuilds over it, and auto-localizes: BODY REJECT vs PLUMBING REJECT vs MIXED. Also
-j on the build (was single-threaded) and the §376 drop list written to
.run/gate_main_dropped.json with the reconciliation chain.
* splat.us.exe.yaml — the .rodata carve extends from the LZSS table alone to the whole
contiguous game-jtbl span 0x80072A38-0x80072C70 (12 tables, one 800.o run).
Byte-neutral with no drafts substituted (probed first).
* jtbl_rodata_pads.py — --derive now works for main: one file-0-vram expression makes
both address->bytes and yaml-piece->address correct for the EXE's 0x800 header and
leaves flat overlays unchanged. Makefile arms it for BINARY=main.
Banked byte-identical: func_8001A114, func_8001AAD0, func_8001AF34 — three of the
eleven. 25 of main's 59 frontier functions (6,215 of 12,912 instructions) are in this
class; the remaining spans need src/800.c split at the TU boundaries the spans reveal.
A §332 delay-slot wall the pinned triple cannot emit from C; banked the verbatim-asm way,
verified by one clean rebuild at 143dbb89f34491258bbc27810d0a12ec8b43a8dd. Bank confirmed
from the source (stub gone), not from the tool's own count.
The main lane, run the ONLY way main can be gated (§414): substitute the whole slate ->
make extract -> make build -> compare SHA1, with a bisect when the batch fails.
Baseline asserted green first (143dbb89...), batch of 6 failed, bisect isolated
func_8002C410 in 7 rebuilds, and the remaining 5 banked BYTE-IDENTICAL.
Four of the five are §265 verbatim-asm bodies for functions on the §332/§188 toolchain
wall list - the accepted route for a function the pinned triple cannot emit from C, the
same way func_800D0B1C banked overnight. Candidates came from scoring every stored draft
for main's 53 non-rejected open functions with match_one: 6 of 53 at closeness 0.
The R22 clean-fleet verify came back 212/213. The failure is main, and `git log -L` puts
all four conflicting declaration lines in commit:3586 — this session's own main re-gate:
src/800.c:24519 extern int func_8004355C(s32, void *); vs :24396 (s32, u8 *)
src/800.c:26395 extern void func_80038FFC(); vs the s32 definition below
Reconciling both declarations (byte-neutral, §376) made main COMPILE, and it was then
still not byte-identical — so the commit was wrong on both counts, not merely unbuildable.
The gate reported "11 banked" against a tree that cannot compile from clean.
src/800.c restored to commit:3586^; `make extract BINARY=main && make build BINARY=main`
now gives sha1 143dbb89f34491258bbc27810d0a12ec8b43a8dd BYTE-IDENTICAL. The 11 bodies are
kept at .run/S71_main_suspect/800.c.banked11 for a per-function re-gate — this revert is
about restoring a green fleet, not a verdict on every one of them.