SaveLoadRoutine is `case 0:` inside func_8002B0B4, not a function of its
own (S75). The lingering `= 0x8002B154; // func` declaration kept splat
emitting asm/nonmatchings/800_b/SaveLoadRoutine.s, which progress.py
reported as the single UNPLACED parse hole. The two config/wave_exclude.txt
WALL entries described the same misconception.
UNPLACED 1 -> 0. Build stays byte-identical at 143dbb89.
R33, "the best outcome is a DELETED SCANNER, not a fixed regex". asm_in_c.py
existed to DISCOVER the §265 verbatim class by parsing __asm__ blocks. That job
is done, and regex was the wrong instrument: five successive censuses returned
116 -> 112 -> 108 -> 178 -> 199, and the classification was worse than the count
-- it called 154 rows "game code" where the authoritative answer is 24.
The real answers came from evidence a regex cannot see:
* the <OBJ>_OBJ_<hex> naming key -- every one is placed_object.text_start +
hex, so those symbols are OFFSETS INTO LIBRARY OBJECTS, not functions;
* the PsyQ archive symbol tables in .run/obj40/, which keep statics as W
symbols, so for a byte-identical object the archive IS the function map
(checkRECT = SYS.o+0x52C = func_80059760, and NONE of the 44 SYS_OBJ_*
symbols in SYS.o is a function).
So:
config/verbatim_manifest.json (NEW, committed) -- the authoritative census.
200 rows, derived once from the ROM image + archives + naming key, each with a
class and a DISPOSITION:
PERMANENT-VERBATIM 69 rows / 57 units hand asm; never decompilable
DECOMPILE-AS-PARENT 57 rows / 23 units a FRAGMENT; decompile unit_entry,
never the fragment itself
DECOMPILE-NOW 41 rows / 41 units
DECOMPILE-LOW-VALUE 20 rows / 4 units
UNCERTAIN 5 / NOT-VERBATIM 7 / NOT-CODE 1
tools/verbatim_check.py (NEW) -- a GUARD, not a census. Detects verbatim bodies
(the cheap part, and the only part regex is good at), diffs the NAMES against the
manifest, and reports NEW / GONE / MOVED. A NEW row means someone banked assembly
and it is about to become invisible work; it is never allowed to inherit a
disposition by default. It deliberately does not classify or count units.
Compares case-insensitively on the hex, because an address is a NUMBER (R48).
tools/verbatim_target_s.py -- put on the MANIFEST LEASH. It used to enumerate
every verbatim SYMBOL, and 62 of those are not functions (fragments, bare
epilogue tails, padding, trampolines). Emitting per-symbol targets for them is
what sent two drafting bursts at things no C function can express. It now takes
only DRAFTABLE dispositions: 66 targets emitted, 134 skipped and SAID SO.
tools/verbatim_to_stub.py -- repointed to verbatim_check for detection, so there
is ONE detector in the tree rather than three copies.
tools/asm_in_c.py -- REMOVED.
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).
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.
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.
16 of 27 entries dropped as STALE, all of them the CARVE-BLOCKED set the four §431 splits
in commit:3709 just unblocked. 11 WALL entries kept (curated, pinned by their `# WALL:`
annotation, which exclude_audit re-reads as a pin so a wall survives regeneration).
An exclude list records what the TOOLING could not do, so it becomes a list of work you
decided not to do the moment the tooling improves — regenerating it is part of the fix,
not follow-up hygiene. draw_waves --exclude-file audits and REFUSES a stale list.
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.
TWO REAL DEFECTS I INTRODUCED, both found by the audit:
1. §429 WAS SILENTLY DELETED. My §428a rewrite (commit:3659) wrote t[:start]+new instead of
t[:start]+new+t[end:], truncating everything below §428a. §429 ('every held pointer
needs its own local') was the casualty and had been gone for the rest of the session.
Restored verbatim from commit:3658, between §428a and §430. All of 426-434 now present;
index 1103 sections.
2. §434 ACCUSED AN AGENT OF INVENTING ITS CITATION OF §265. §265 exists and says exactly
what the agent said — 'THE VERBATIM-ASM BANK LANE: A FUNCTION NO -O2 C CAN EVER MATCH
BANKS AS A RAW __asm__ BODY' — with four named byte-banked precedents. I ran
cookbook_index --resolve 265, which resolves a LINE number not a section, and believed
it without opening §265. Retracted in the section itself.
The verdict also needed narrowing: gated, the §265 transcription of SaveLoadRoutine is
BYTE-IDENTICAL for the function itself and fails only because substituting one half of
the shared frame moves 3,989 bytes across 262 symbols. True statement: neither can bank
SEPARATELY; the route is to transcribe/resegment the PAIR together via §265. The
exclude entries now say 'excluded from DRAWS only' and name that route, instead of
reading as 'unmatchable'.
I also mis-read the draft as containing INCLUDE_ASM by grepping raw text — all three hits
were in comments. Sixth instance this session of reading prose as code.
The previous commit's cookbook change landed but the exclude entry did not: the guard was
`assert 'SaveLoadRoutine' not in t` over the whole file, and that string already appeared
inside func_8002B0B4's WALL note. Fourth instance today of matching PROSE as if it were
structure. Now compares against parsed ENTRIES, not a substring.
My sweep for '§179-C documented walls' grepped raw text, so it matched an INCLUDE_ASM
line quoted inside a 'BANKING: this block REPLACES the line ...' comment. corpus.stubs
said banked, my grep said stubbed, and corpus was right.
Third instance today of one bug class — reading PROSE as CODE. The other two were
split_src_region.item_name matching a parenthesised token inside a comment, and matching
a leading extern declaration as the definition. The exclude_audit caught this one
immediately by flagging my own addition as STALE.
R45 — never draw a card the pipeline cannot bank. src/800_b.c carries the explanation
directly above func_8002B0B4's stub (no epilogue; every exit is a raw j/jr into labels
inside SaveLoadRoutine's body, so gcc-2.7.2 always synthesizes an epilogue the target
lacks), and the wave drew it anyway: 70k tokens and 100s for an agent to re-derive that
paragraph and hand back the stub verbatim, reported as MATCH closeness 0.
A draft that IS its own INCLUDE_ASM is the silent-no-op class gate_main already refuses,
so nothing would have banked — but the agent slot was spent. Swept main for the class:
exactly 2 such stubs, both now excluded.
Found by checking readiness rather than asserting it: S71's two PROVEN walls
(ov_SC03_105:func_801834A4, ov_SC06_022:func_8017DF28) were NOT in the canonical list —
they lived in a separate .run/S71_walls_found.txt the regeneration never saw. Drawing
would have spent agents re-proving them (playbook §1b: a full agent run each time).
Merging them in exposed a second defect: a WALL has no jump table, so the DERIVED logic
would classify it RE-PROBE and drop it. in the input is now read as a PIN that
survives regeneration, and the entry's ORIGINAL note is carried through — a wall's value
is its refutation list, and replacing that with boilerplate turns evidence into a bare
'do not try'. Round-trip verified idempotent: 9 walls survive a second pass unchanged.
Merged 7 walls ledgers (S67/S68/S68_332/S69/S70/S71/S71_found) into
config/wave_exclude.txt: 25 entries = 16 CARVE-BLOCKED (derived) + 9 WALL (curated).
The audit found 8 of the merged walls already BANKED — a wall that got matched is no
longer a wall.
DELIBERATELY NOT merged: .run/t3wall_list.txt, 99 BARE function names with no binary.
R48 — the same name is a different function in another overlay, so a bare-name exclude
over-excludes silently fleet-wide.
There were NINE session-snapshot copies under .run/ and no way to tell which was
current — the accumulation smell behind the whole staleness problem. This is the one,
it is tracked, and it is regenerated rather than hand-edited.
.run/ is gitignored scratch, which is the wrong home for it: CARVE-BLOCKED entries are
derived and vanish when the subseg is split, but WALL entries are CURATED and cannot be
re-derived — that is precisely why the file needs to be tracked.
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').
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).
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.
The type-name scan matched `}\s*(\w+)\s*;`, which reads `__attribute__` as the name
and fails on the following `((` — so a packed file-local typedef never entered the
carried set, every decl naming it read as an unknown type, and the isolate refused the
whole overlay. Stripping attributes before the scan is the entire fix.
The type-name scan matched `}\s*(\w+)\s*;`, which reads `__attribute__` as the name
and fails on the following `((` — so a packed file-local typedef never entered the
carried set, every decl naming it read as an unknown type, and the isolate refused the
whole overlay. Stripping attributes before the scan is the entire fix.
The CARVE-REFUSED class (10 of the frontier's gate failures) has one dominant cause:
"subseg <ov>_jr_<addr> would host NON-CONTIGUOUS .rodata carves", whose named remedy
is jr_isolate_all. The isolate itself then refused 4 of the 6 affected overlays over a
file-local `static inline` helper (bandsetup, setup_80188D90) that has no address BY
CONSTRUCTION — §82.1 helpers exist to shape their caller's code and emit no symbol.
* jr_isolate_all now places such a definition with the ONE region that uses it, and
refuses loudly if two regions do (two copies of a used static is a byte change, R43).
* overlay_src_split._proto_from_lines no longer prefixes `extern` to a declaration that
already has a storage class — `extern static inline void f(...)` is "multiple storage
classes" to cc1. The two changes are inseparable: placing statics is what first made
the tool emit a prototype for one.
Byte-gated on ov_SC03_010: extract + build rc=0,
sha1 cacaf7c2c08037e6934f9d02c0ae5d7c78cf2463 BYTE-IDENTICAL. jtbl_carve --probe then
moves from `plan-refused` to `tail — standard §8a carve at gate time`.
Reverts commit:3475. The bank is byte-identical; MY VERIFICATION WAS BROKEN.
A jtbl bank changes CARVE CONFIG (JTBL_PADS in config/overlays.mk + the splat
yaml). Those are splat INPUTS: asm/ and the linker script are regenerated FROM
them. I checked the binary with `make build` alone, so the build linked
newly-carved C against STALE extracted state and produced a mismatched SHA. That
is the R22 corollary ("a reverted config needs a make extract, not just a make
check") pointed the other way — a LANDED config change needs one too.
Proof, run on both binaries:
make extract BINARY=ov_SC06_025 && make build -> BYTE-IDENTICAL
make extract BINARY=ov_SC04_011 && make build -> BYTE-IDENTICAL
So: R40 against myself. I attributed the failure to the subject (the bank) when
the instrument (a build over stale extract state) was at fault — after writing
"it may not even be false" into the checkpoint and reverting without testing it.
The first revert also cost real work: it discarded a legitimate 96-line match.
STANDING FIX: a per-binary verify after any gate that touched config/ MUST be
`make extract BINARY=<b> && make build BINARY=<b>`. Build-only is a valid check
ONLY when the gate changed nothing under config/.
Reverts commit:3472. The binary was RED at HEAD: sha1 9c94d36a vs expected
8bc09c42. The gate that banked it ran with --r22 disabled because 24 drafting
agents were live (R22 does make clean, which deletes asm/ under them), so the
one check that would have caught it was the one I had turned off.
The revert must carry the CARVE STATE, not just the C: the bank moved
JTBL_PADS 0,0,4,4 -> 0,0,4,4,4 plus the splat yaml, and a src-only revert left
4 tables against 5 pad specs ('table-count drift vs the carve'). Reverting the
whole commit restores BYTE-IDENTICAL.
Found only because two drafting agents independently reported their target's
binary as BASELINE-RED and I checked their claim against the bytes.
MY HYPOTHESIS WAS WRONG AND THE AGENT SAID SO. I predicted the ownership oracle
was blind to verbatim-asm owners. It is not. 0x800cedf8 is the §154-A LEADING
RODATA ISLAND (the module-id header + jtbl/ptr table at segment offset 0), which
rodata_carves already exempts via 'off == 0 and sub == ov'. The S68 first carve
legitimately renamed that subseg to md_MAIN_003_jr_800D12D0 (§371: spimdisasm
rodata migration is same-subseg-only), so the 'sub == ov' conjunct stopped firing
and offset 0 leaked in as a 'carve'. The island has NO single owner BY DESIGN --
which is why the exemption exists -- so widening owner kinds could never have
restored 1:1.
The fix drops one conjunct: offset 0 alone is the honest structural key, because a
carve is a table LIFTED OUT OF THE DATA TAIL and can never sit at the segment's own
offset 0. Verified across all 213 configs: every offset-0 .rodata piece is an md_*
leading island; ov_*/main have none. The R32 hard abort is UNTOUCHED -- this widens
the recognised-island set, it does not soften the refusal.
NEGATIVE CONTROL (R39) over all 184 binaries with .rodata pieces: OK 182 -> 183,
ABORT 2 -> 1, and exactly ONE verdict moved (md_MAIN_003). The remaining us.exe
abort (UNOWNED 0x80073238, the LZSS jtbl carve whose owner LzssDecodeSector does
not live under src/us.exe/*.c) is byte-identical before and after -- PRE-EXISTING,
not newly hidden, and logged rather than silently absorbed.
Carve byte-neutral and bank byte-identical, both re-verified by my own rebuild:
sha1 dd1b32ecf1103c6f7cf1943d25546a3046e17b14 == config/check.md_MAIN_003.sha.
md_MAIN_003 12 -> 11 stubs.
THREE o0_subsplit GAPS surfaced and hand-finished, and they must be fixed before
the remaining 7 -O0 stubs here are carved: build_new_config drops a cut at the
object start so region 0 kept the -O2 name while the tool PRINTED the _o0 name;
parse_overlay_c folds pre-anchor text into the FOLLOWING anchor, so a verbatim body
inside region 0 attached to region 1; and the island .rodata piece needs repointing
to whichever TU ends up holding its emitters.