Commit Graph

137 Commits

Author SHA1 Message Date
Drew T cabab00c0e wip(phase-33): B5 Ghidra regenerability — ExportAnnotations.java + ghidra_export_annotations.sh PROVEN (all 129 programs exported to byte-stable JSONL in 18.5 s), ghidra_annotations_delta.py (live − baseline), ImportAnnotations.java + ghidra_rebuild.sh WRITTEN but unproven (the resident rebuild hit an OSGi script-bundle load error — a compile error in the new importer breaks every script in the dir; the known bad LocalVariableImpl ctor is named in CURRENT_PHASE); Makefile print-% + GHIDRA_PROJ repo-relative; the six ghidra_*.sh repo-relative (BFM_GHIDRA_PROJ); DefineFunctions arg path; ImportPsyqGdt install-dir default; ExportSymbols R15 fix; scratch project under build/ (Ghidra refuses '.'-prefixed path components); SETUP P33 B5 section; checkpoint refreshed for the next session (S86 paused at 87% context) 2026-09-06 19:31:34 -06:00
Drew T 707ec491e4 feat(phase-33): B4 tools/fetch_psyq.sh — the OPTIONAL PsyQ SDK objects, user-supplied and sha256-verified (parser tarball + binary, RTL 4.2 archive -> lib421, the 20 PsyQ 4.0 LIBs off the DTL-S2002 redump disc via tools/psyq_libs_from_disc.py or --from), then the existing builders + make sdk-dual; tools/psyq/CHECKSUMS.sha256 -> tools/psyq_CHECKSUMS.sha256 with 22 new rows; controls: refuse without a 4.0 source, die on a corrupted LIB; the disc-path run rebuilt every SDK dir and proved sdk-dual in 37 s; SETUP P33 B4 (R21) 2026-09-06 19:19:06 -06:00
Drew T 89e087f52a feat(phase-33): B3 tools/bootstrap.sh + make bootstrap (apt presence printed, venv from requirements, submodules, cc1 tarballs sha256-checked + extracted, check-env) + check-env gains submodule/preset-header/payload-census lines; PROVEN on a fresh clone: bootstrap from nothing -> check-env OK -> disc-extract OK -> extract-all 217/217 -> check-all 218/218 in 4m18s with no SDK objects (the public user's path); SETUP P33 B3 (R21) 2026-09-06 19:14:11 -06:00
Drew T 9be6364a5b feat(phase-33): B2 portable includes — 34 absolute #include lines in 19 overlay sources (15 binaries) respelled to ../shared/ (30 engine_core.h + 4 engine_types.h); audit_text_sources.py gains the portable-include class (absolute / angle-bracket / unresolvable / outside-repo), controls 34 offenders before -> 0 after over 4,299 sources; the 15 binaries re-gated byte-identical (15/15 OK, 29 s wall); SETUP P33 B2 (R21) 2026-09-06 19:06:03 -06:00
Drew T 0265712916 feat(phase-33): B1 make disc-extract — the rom->decoder step in the build: extract.py --expect-manifest (compare against the committed oracle, never write it; mismatch -> .run/extract/ + diffs, exit 1) + --allow-missing-audio (explicit PARTIAL for Track-1-only dumps); disc-extract = probe (0.7 s no-op) -> disc presence -> redump SHA1/CRC32 -> extract+compare -> verify (15.7 s full); extract/extract-all call it; check-env WARN-on-absent EXE + oracle self-consistency; help rewritten; the 4 splat preset headers TRACKED (clean keeps them); .gitignore re-tightened to H1 (dumps/*.bin, ghidra/, tools/psyq/, session archive/, ghidra-ext zips, brave.exe; EXE re-include dropped); controls: no-disc exit 2, truncated disc FAILs with the oracle untouched, regenerated tree byte-identical to the previous (1,801 files) 2026-09-06 19:02:34 -06:00
Drew T 62854caca0 fix(phase-33): A4 family map carries its own coverage — family_hseq.json records the 217 scanned binaries + open_instances (0), audit_binaries CHECK 4 reads that denominator instead of inferring it from (now empty) families; regenerating the map had turned the P32-close 6-binary warn into 217 'missing' from a COMPLETE map (R35); controls: 0 [warn] now, the old format warns 'predates the coverage field'; docs/family-hseq.md regenerated (0 families); SETUP P33 A4 (R21) 2026-09-06 18:55:21 -06:00
Drew T 4bb29544a2 feat(phase-33): A3 the with/without-SDK dual — NO_SDK=1 knob (skips the eleven psyq_integrate rewrites AND the -T externals fragments), make sdk-dual (refuses without all 11 SDK object dirs; WITH → extract → rm build/psyq → NO_SDK=1 → extract → WITH restore; map assertions; both legs == config/check.us.sha), wired into tools-health with a [skip] when the SDK is absent; proven: main 143dbb89… byte-identical WITH (1,288 psyq objects) and WITHOUT (12 libcd1 stub tiles), 28 s wall; SETUP P33 A3 (R21) 2026-09-06 18:51:19 -06:00
Drew T b23300fc21 feat(phase-33): A2 reporting instruments — make sig-main rewritten (tools/main_seed_ends.py: main's game-code boundaries DERIVED from the link map + objects, tiling asserted; 809 fns / 45,150 ins), progress.py weighs main by the build-derived sig and EXITS with no sig (R32), the digest's oracle clause derived live (0 phantom / 0 truncated / 0 pad-tail), backlog.linked_closed retires the LINKED-range legacy row (0 open), dup_report on the derived sig; CORRECTED main denominator 45,150 (Ghidra's flow boundaries left 3,628 words of game code unowned) — fleet instr 13,492,113 / distinct 5,820,205, all 100%; make report BINARY=main EXIT=0, audit-digest OK; SETUP §6.8 + P33 A2 rows (R21) 2026-09-06 18:46:52 -06:00
Drew T 6e37e8ec62 docs(phase-32): T4c harvest + checkpoint — the frontier is EMPTY (census 0 stubs / 0 ins); cookbook §501-Q (the combine self-update ghost slot) + §501-R (a hoisted invariant read three times: inline temps merged by combine_movables, u16 as a cse firewall, the allocation order IS the callee-saved bank; every pin came off), accelerators (15)(16), decision-log P32 S85 T4c, tools/cc1_dumps_tu.sh + tools/alloc_table.py promoted (+ SETUP rows, cc1_dumps.sh -dL), wave_exclude 0 entries, backlog 1 legacy row, .run/P32/t4e (56 variants, 18+8 reproducers, NOTES, banks, slates); 🛑 checkpoint refreshed as the T5 seed (the fleet R22 + report chain runs in the background — its logs are recorded by the next session) 2026-09-06 16:13:19 -06:00
Drew T b6fd3fff28 docs(phase-32): T4b hand pass — func_80032A74 PROVED at 1 by producer census (§501-M); ghost_census.py + cc1_dumps.sh repair; backlog tie-break repair
- main:func_80032A74 (422 ins, closeness 1): the residual is ONE reload-time slot at sp+0x48 (u16 draft = 422/422 code, DIFF 22 frame
  rows; s16 draft = DIFF 1 at idx 244 lh vs lhu). Every post-parameter slot producer enumerated from gcc-2.7.2 (reload1.c:658 ghost
  alter_reg / caller-save.c:249 area / reload1.c:879 invalid-equiv / reload1.c:3499 spill_stack_slot) and refuted on the bytes: combine's
  newi2pat ghosts re-derive a narrow load and the site is lhu (no lb, no double load); a save area without sw/lw needs sched.c:4962
  staleness and no register-only insn shares a block with a call; $t0 holds no pseudo; LO mult results retry into GR_REGS. Verdict
  PROVED at 1 (pin kept with the verdict; ledger WALL-PROVED).
- NEW mechanism measured: local-alloc.c optimize_reg_copy_2 (tmp = x; tmp op= c; x = tmp) mints a ghost with stale refs, but after
  regclass -> GR_REGS, allocated, no slot (P13/P14). 18 isolated reproducers, 0 draft variants; cookbook §501-M; accelerators (12).
- tools/ghost_census.py (new): ghosts in a .lreg dump with their class (ST_REGS => slot). tools/cc1_dumps.sh: prints the .frame line,
  ins count, spill lines and the census; the under-counting standalone-(use) grep is gone. SETUP rows (R21).
- tools/backlog.py: load_best kept the EARLIEST record at equal closeness (docstring said latest) — the S84 row never rendered; fixed.
- CURRENT_PHASE.md: S84 log + refreshed 🛑 checkpoint (rows (b)(c)(d) next, then T5). No src/config/carve change; fleet check-all
  218/218 rc 0 at the S84 preflight.
2026-09-06 11:44:40 -06:00
Drew T 11f79c1b0a docs(phase-32): T3 checkpoint — the 31 agents' FULL final reports saved to .run/P32/t3/reports/ (tools/agent_reports.py, NEW) + tools/transcript_dump.py (NEW, the dead-session reader); SETUP rows, playbook addendum-2, 🛑 block §3/§6 point at them 2026-09-05 10:11:43 -06:00
Drew T 42ff6116ea docs(phase-32): session-start protocol → phase-ends/DIGEST.md + last 3 PhaseEnds (~100k tokens), checkpoint replayed VERBATIM (R64 candidate); the T3 checkpoint refreshed to the verbatim-replay standard
- phase-ends/DIGEST.md (NEW, Drew-directed 2026-09-05): every phase's synopsis (P1–P32), every rule R1–R64 in full, the
  corrections that supersede parts of PROJECT_CONTEXT.md (P8→R19, commit cadence→R42, H1→R1, headless Ghidra, roadmap v2,
  effort doctrine, the pinned triple), and the doc map. Maintained at every PhaseEnd (CLAUDE.md Phase Boundary step 3b, P7).
- CLAUDE.md Session Start Protocol rewritten: PROJECT_CONTEXT → DIGEST → the THREE most recent PhaseEnds → CURRENT_PHASE
  (+ cookbook head/newest § + SETUP §5.4 for matching phases); rules transcribed in full from the digest; the 🛑 SESSION
  CHECKPOINT block reproduced VERBATIM in chat as the session's only in-phase seed. Measured load order ≈55k tokens
  (was ~150k reading all 32 PhaseEnds). phase-ends/README.md + SETUP §7 pointer updated (R21).
- CURRENT_PHASE.md: the T3 🛑 block REFRESHED and SUPERSEDING the 09:30 one — written to be replayed: what happened in the
  dead session (times, hashes, the overflow, the swept dir), what the successor did, the 44-stub census with every row's
  state/draft path, the 10 banked with hashes+shas, the 9-step resume order with exact invocations, the file/tool
  inventory and gotchas, carried context for T4/T5, environment, the plain-English recap; Log entry for the protocol change.
- .run/P32/t3/PROMPT_TEMPLATE.md (tracked): the verbatim agent prompts (Haiku / Haiku+twin / Sonnet escalation / Opus /
  Sonnet) for launching the 17 queued rows under the amended output contract.
- memories updated outside the repo: checkpoint-current-phase-before-pause (the verbatim-replay contract; a dead session's
  checkpoint is written by the successor from the transcripts) and session-start-list-rules-in-full (the ~100k protocol).
2026-09-05 10:02:04 -06:00
Drew T 9480c979a0 docs(phase-32): T3 CHECKPOINT — the launching coordinator overflowed after bank 9; 22 agent verdicts recovered (20 MATCH / 9 NEAR / 2 FAIL of 31), 11 swept Opus deliverables restored + re-verified, cookbook §500 harvest, playbook §S80 addendum-2, agent_drafts_restore.py
- verdict ledger .run/P32/t3/verdicts.jsonl rebuilt from the 31 T3 transcripts (agent_verdicts.py); every unbanked draft
  re-verified with rtu_match in its real TU: 10 MATCH awaiting the gate (main func_80015B6C 120 + func_8002FDE8 73;
  md_SC03_054 func_801EF6D8 604 + six jtbls; md_SC03_053 func_801EF734 44 + func_801EF7E4 72; md_MAIN_007
  func_800CF148/2BC/EEFC/EF94/068) + func_800CF3B0 leaf-exact behind the TU's void/3-arg decl; 9 NEAR at exact length
  (2/6/15/17/27/35/46/49/137), each with its class and inert-lever list
- R48 incident: one agent's `find .run/P32/t3/opus -maxdepth 1 -type f ! -name <mine> -exec mv {} _scratch/` swept 11
  sibling deliverables (two MATCHes among them); found in _scratch/, restored to the contract paths, byte-verified;
  tools/agent_drafts_restore.py (NEW: transcript replay) as the fallback; .gitignore allowlist for .run/P32/** so the
  drafts, ledger and census files are committed (R20)
- harvest (R16/R30): cookbook §500 (10 banked closers, 10 MATCH closers, 9 NEAR classes, two NEW mechanisms — the
  pinned-base-vs-pseudo-address alias basin and #line-equalised ASM_OPERANDS for cross_jump — and the wave-process
  defects); wave-playbook §S80 addendum-2 (per-function work dirs, JSON-only final message, the 20-agent cap, the
  recovery tools); accelerators P32 T3; decision-log P32 S82 (R31); SETUP tooling row (R21); cookbook-index
  regenerated; .run/P32/t3/BRIEF.md output contract amended for the 17 queued launches
- CURRENT_PHASE: T3 row IN PROGRESS, Log entry, 🛑 SESSION CHECKPOINT (census 44 stubs / 5,313 ins with every row's
  state and draft path, the 9-step resume order, the dead session's read-only T4 pre-read); harness task list rebuilt
- no src/ or config/ change in this commit; no fleet R22 has run since the 10 T3 banks — the resume order starts with one
2026-09-05 09:36:43 -06:00
Drew T 8b2bbff831 fix(phase-32): T2c (1) — split_indicator's population is derived from the yamls (was a stale stored 213-name list; tools-health said "213 OK of 213" over 218 binaries); R32 denominator assertion 2026-09-05 00:08:44 -06:00
Drew T 1e843c607a feat(phase-32): T2b (4) — SC03/56 ONBOARDED as md_SC03_056 @0x801CBB50 (ov_SC03_002's DESTPTR), byte-identical bc768a6b; ALL FIVE parked payloads are now binaries (fleet 213 -> 218); evidence tool v2
- md_SC03_056 (TEXT_LO 0x4, 4 stubs / 61 ins): 15/17 pointers cluster inside at 0x801CBB50; one outward call
  (0x8018151C) hits a function only 3 overlays have, ov_SC03_002 among them; req_fit 9/9 for ov_SC03_002
- payload_base_evidence.py v2 (controls 7/7 throughout): (a) STRONG = internal jals + fn-ptr-table entries on the
  module's own starts >= 2 (SC03/53 STRONG); (b) OUTWARD-EXPLAINED — a pure jal-vote base whose "internal" targets
  are function starts of the fleet's overlays is downgraded: SC03/56's 0x80178C8C was two SHARED-engine functions
  spaced like two of its five starts (and nobody's DESTPTR), a false STRONG; (c) the requester cross-check is
  informational only — shared engine code makes every requester fit (an R39 control caught it scoring: 6/7)
- memory-map §S45 p7 amended: all five rows ONBOARDED + the two instrument findings (the first build is a NULL
  oracle for FINE base errors — +8 builds byte-identical, +0x1000 fails the link; outward-explained vote bases);
  SETUP row amended. The parked-for-L3 ledger is EMPTY pending `make audit-disc` (T2c).
2026-09-04 23:52:58 -06:00
Drew T 848c7c50ab feat(phase-32): T2a — tools/payload_base_evidence.py (controls-gated static base evidence) + memory-map §S45 p7: the parked five get candidate bases
- the instrument: module-id word, TEXT_LO estimate, absolute-pointer set, lui hi-half histogram, and a
  jal->function-start VOTE (starts = prologues ∪ the word after every `jr $ra`+delay — leaf functions have no
  prologue, the recall killer of S45's vote_base 4/12); scores a BOUNDED candidate list (5 §S44 slots ∪ 134
  IDXTAB DESTPTRs ∪ vote bases): STRONG / CONSISTENT / INCONSISTENT / NO-EVIDENCE; AMBIGUOUS tie sets are
  printed, never picked; a payload with no self-reference is REFUSED as base-independent (R43)
- R39 controls run before any emission: md_MAIN_008/011/013/042, md_SC03_073, md_SC02_009, md_SC07_004
  re-derive their byte-proven bases top-ranked from their payloads alone (7/7); TEXT_LO estimates == yaml
  (incl. the header-table modules 0x7C/0x14/0x158). The first draft of the scorer FAILED 5/7 (prologue-only
  starts; a top-rank assertion on modules the bytes cannot discriminate) — fixed by the controls, not shipped
- the five (G5 static-derived, US): MAIN/7 STRONG 0x800CEDF8 (9/9 jals, 14/16 ptrs on starts); MAIN/9 STRONG
  0x800CD348 (6/6, 9/9); SC03/53 + SC03/54 CONSISTENT with 0x801EF468 top of a 12-way tie; SC03/56 SPLIT
  (jal vote 0x80178C8C vs pointers/lui ~0x801CBB50). T2b probes each with new_binary.sh — the byte gate decides
- SETUP row (R21); evidence rows .run/P32/t2a/evidence.json
2026-09-04 23:42:53 -06:00
Drew T c513e1fbbd feat(phase-32): T1a (2) — resident: func_800D128C (243 ins) BANKED byte-identical 8e17e02f via the raw splice + a 5-piece carve; three instrument fixes (§498)
- BANK: the stored S71 closeness-0 draft spliced into src/resident/resident_jr_800D128C.c; jtbl_carve --func
  carved jtbl_80113FB8 (119 entries, 1 pad word trimmed) + jtbl_80114198 into [0x451c0, .rodata,
  resident_jr_800D128C] + [0x453c4, data, tail3]; JTBL_PADS 0,4; make extract + make build BINARY=resident -j8
  rc 0, sha 8e17e02ff8954d07c979449198f7e1645046b353 == check (R53). pads_audit ok/ok; interleave_check
  ALIGNED n=5; verbatim_check --strict 5==5. Resident stubs 2 -> 1 (func_800D06E8 remains).
- WHY THE GATE SAID DIFF (parallel_gate banked 0/DIFF on an rtu_match MATCH): jtbl_carve.set_overlays_var
  regenerated resident_JTBL_INTERLEAVE from the carve set and DROPPED the resident's `--pre hdr.rodata.o`
  (§8f leading-rodata sandwich); make extract refused (ld_interleave: hdr.rodata.o would be parked with
  .text), the build linked the STALE script (249,252 differing bytes from file offset 0x4), and
  harvest_verify._jtbl_prep_one never read the post-carve extract's exit code (R49/R61).
- FIXES (R35/R40/R57): jtbl_carve._merge_pre carries an existing --pre forward (idempotent; overlays
  unchanged, 4-shape unit control); harvest_verify refuses loudly on a failed post-carve extract and
  restores the snapshot (CARVE refusal, NOT a draft verdict); interleave_check's anchor accepts a leading
  --pre (was a false DRIFT n=0 on the resident; control ov_SC02_017 ALIGNED n=44 unchanged).
- cookbook §498 (+ the stale-asm-after-a-failed-extract sequencing law); SETUP rows for all three
2026-09-04 23:28:59 -06:00
Drew T c52190ca86 fix(phase-32): T1b (1) — jr_isolate_all keys a bodiless typedef struct Tag Alias; by the ALIAS (§497); ov_SC02_017 dry-run REFUSED -> CLEAN, no source rename
- _type_names returned the TAG for `typedef struct Rec801806C8_s Rec801806C8;`, so the typedef block and the
  tag's own packed struct definition collided under one key with different bodies and the R43 "CONFLICTING
  bodies — a rename is needed" refusal fired on legal C. Now keyed by the alias (_TYPEDEF_TAG_ALIAS); the
  `carried` set learns the alias; `typedef struct X X;` (alias == tag) keeps the old key so a second one
  still dedupes/refuses. Unit control on 7 block shapes PASS; ov_SC02_017 --only func_80186C64 --dry-run:
  2 region files, no carve repoints. cookbook §497; SETUP row.
2026-09-04 23:24:02 -06:00
Drew T 380ccdc843 feat(phase-32): T1a (1) — resident code subseg split (3 regions, byte-identical 8e17e02f) + jr_isolate_all include-derived provided types (§496)
- jr_isolate_all resident --only func_800D128C: [0x4 c resident] [0x12ec c resident_jr_800D00E4]
  [0x2494 c resident_jr_800D128C]; the banked jr func_800D00E4's .rodata carve + JTBL_PADS + --order
  repointed to resident_jr_800D00E4.o (config/overlays.mk resident block only, R60); make extract +
  make build BINARY=resident -j8 rc 0, sha 8e17e02ff8954d07c979449198f7e1645046b353 == check (R53)
- TOOL FIX (R43/R33): the carried-type test consulted _engine_types() (engine_types.h + common.h) for
  every TU, assuming each region includes engine_core.h; the resident includes only common.h, so its
  file-local `typedef struct {...} CdFileLoc;` (a name engine_types.h also defines) was silently NOT
  carried -> `parse error before cdFileLocTable` in both region TUs, build rc 2 while the stale binary
  on disk read green. Now _provided_types(header) derives the set from the TU's own #include lines
  (engine_core.h => engine_types.h + common.h, never engine_core's macro-internal typedefs; common.h
  => common.h) and _file_scope_decls(items, provided) uses it at both decision points. R39 controls:
  overlay header == legacy set (1,197 names); resident set lacks CdFileLoc. cookbook §496; SETUP row
- rtu_match func_800D128C --split resident_jr_800D128C: MATCH (243 ins) on the stored S71 draft;
  the gate is the next commit
2026-09-04 23:20:04 -06:00
Drew T 452975e852 docs(phase-31): S80 #10 CLOSE — the verbatim end-state: manifest 6 → 5 rows (the five PERMANENT rows RATIFIED in _README; the GAME-C row decompiled), cookbook §495 (two def-side declaration walls, the S79 assembly "bank" P9 correction, the gate that dropped a bank on exit 0), decision-log S80 addendum (R31), SETUP rows; tools/parallel_gate.py: banked-but-not-merged now exits 2 with the worker's raw git status kept + per-run .run/pgate_runs/<ts>.json; CURRENT_PHASE #10 bullet + the S80 #10 CLOSE checkpoint (R22 213/213, tools-health OK, census 21 stubs / 4,554 ins, NEXT #11 = PhaseEnd, gate 2); regenerated digests 2026-09-04 22:07:35 -06:00
Drew T 7fbdb8fd63 fix(phase-31): S80 #9c — the permuter could not permute a PINNED seed, and it was our instrument: hide_asm carried only the __asm__ spelling (3 S79 seeds use asm("$7")), permuter_ils warm-restarted from the DECODED waypoint (raw pins back in base.c → cycles 2..N were silent parser refusals reported "(unchanged)"), and defines_fn refused K&R-style definitions (436 stored backlog drafts kept out of the lane for four phases). Fixed + R39-controlled over 5,311 drafts (the bare word asm in INCLUDE_ASM path strings was a caught false positive): re-hide every waypoint, assert the definition survived, abort exit-2 on a refusal (R61a), flushed logs (R55). Every S79 pinned seed now iterates; ov_SC06_022:func_8017DF28 (pinned WALL, closeness 2) reached 1 in its first cycle. cookbook §493 S80 correction + §494 v1 (S79 idioms); SETUP rows (p16_permute/permuter_ils, agent_verdicts.py) 2026-09-04 20:59:35 -06:00
Drew T 2709321082 docs(phase-31): S79 HANDOFF checkpoint mid-task #9 — 7 banks this task (open stubs 51 -> 25 this session), plateaus with residuals named, 11 drafting agents still running; tools/agent_verdicts.py extracts their final JSON verdicts from the subagent transcripts for the fresh session to aggregate (procedure + paths in the 🛑 block) 2026-09-04 19:50:01 -06:00
Drew T a0139c31c8 docs(phase-31): S79 #8 close — cookbook §493 (the permuter route end-to-end; the D-NEAR ledger), p16_permute surfaces the permuter's parser refusals, SETUP row, census 31, report, checkpoint (task #9 brief)
Stubs 32 -> 31 after the func_80015760 bank (commit:3877); R22 fleet 213/213 (.run/S79_check_all_8.log);
main game-code 93.5% (38,854 / 41,534). Permuter ILS plateaus recorded with their residual named:
func_80015608 best 1, func_80039B20 best 7, func_80038698 pinned seed refused (11). The ILS runner
had reported "no waypoint" for 8 cycles in 20 s on a seed the permuter's C parser rejects; it now
prints [permuter] REFUSED and leaves PERMUTER_REFUSED.txt (positive-controlled on func_80038698).
2026-09-04 18:50:20 -06:00
Drew T 595fc9fa49 docs(phase-31): S79 #6 close — cookbook §491 (the mechanical class: a phantom stub, two jtbl twins, one clone; three tool gaps), jtbl_pads_fix regex fix (+positive control), SETUP rows, census 35, checkpoint refreshed (task #7 brief)
Stubs 38 -> 35 after the #6 banks (commit:3868 commit:3869 commit:3870 commit:3871); R22 fleet 213/213
(.run/S79_check_all_6.log); frontier_classify 35 rows (main 16, md_MAIN_003 5, resident 2, ov 12).
jtbl_pads_fix's PAD_ERR_MORE regex carried jtbl_rodata_pads' old wording and reported "no
pad-count drift" over a red build; it now accepts both spellings and, positive-controlled with a
deliberately short spec, reports "emits >4 table(s), spec declares 4". The deferred carves and
their blockers are itemised in §491 and in the checkpoint's task #7 brief.
2026-09-04 18:19:57 -06:00
Drew T 02f060f607 feat(phase-31): S79 #5 — the libpad 4.2.1 + libapi 4.2 band and the apicard region LINKED from real objects: 13 stubs + 4 TUs + the reorder island gone; main 16 stubs, fleet 38
800c3 (0x8005CE18-0x8005FC68, one contiguous run of 33 interleaved Sony objects) is now four
stub rows — libapi1 (21 BIOS trampolines + COUNTER), libpad1 (PADENTRY + PADMAIN 760), libapi2
(L02/L03), libpad2 (PADCMD PADIF PADPORTD PADSEQD WAITRC2) — fed by two WINDOWED psyq_integrate
calls from the raw .run/obj42/{libapi42,libpad421} dirs (integrate tiles each stub with one
library; every boundary checked against .text SECTION sizes). The apicard region's three
"game code" rows were libapi 4.2's C objects to the byte: 800c2 = FIRST.o (firstfile + the
"no jump table wall" stub func_80062144), 800c2_2 = PAD.o, 800c2_3 = PATCH.o + CHCLRPAD.o ->
apicard5/6/7; make_apicard_used.py sources libapi from 4.2 (the EXE's real libapi; libcard
stays 4.0) into .run/obj42/apicard_used, 26 objects / 7 blocks, no game code left in
0x80061F38-0x80062888. src/800c3.c (129 hand-matched "C", 62 verbatim bodies, 19 stubs incl.
the four §332 %lo-in-a-delay-slot "walls"), src/800c2.c, src/800c2_2.c, src/800c2_3.c removed;
REORDER_TUS is empty (mechanism kept). Cookbook §490.

Two stale instruments fixed: exclude_audit let a pinned WALL outrank LINKED (PopMatrix/
PushMatrix had sat as walls since S68 while living in libgte3, linked since Phase 8) — LINKED
dominates now, config/wave_exclude.txt 13 -> 3; frontier_classify carried a hard-coded 49-name
LINKED set (R51) and reported 337 "stubs" — derived from the Makefile now.

Verified: main 143dbb89f34491258bbc27810d0a12ec8b43a8dd WITH all SDK dirs and WITHOUT them from
a fresh extract; make tools-health OK; R22 fleet extract-all 212/212 + check-all 213/213.
Metrics: main REAL 839->773, LINKED 1,150->1,256, VERBATIM 29->3, stubs 29->16, byte-identical
2,075/2,091 = 99.2%; game-code weighted 93.3% (38,748/41,534), remainder 2,786 = the open-stub
sum; fleet stubs 51->38 (frontier_classify: 39 rows incl. the data word). Verbatim manifest
33 -> 6. Docs: worklist rows + "S79 task #5", SETUP (fresh-clone obj42 commands, Makefile
blocks, exclude_audit), decision-log "S79 addendum 2", accelerators "S79 (2)", CURRENT_PHASE
S79 FINAL refreshed (census, metrics, the task #6 brief).
2026-09-04 17:56:31 -06:00
Drew T 58996ca4f7 feat(phase-31): S79 #13 — FOUND the EXE's libpad 4.2.1 + libapi 4.2 (PsyQ RTL 4.2 archive + the J421PD patch): 46/46 band objects link byte-identical
The bounded hunt succeeded on its first lead. archive.org item
`play-station-programmer-tool-runtime-library-version-4.2.7z` (383 KB) is the PsyQ Runtime
Library 4.2 (LIB/*.LIB + INCLUDE, 1998-01-21) plus LIB/42PATCH/J421PD.ZIP — SCE R&D's
1998-02-26 "Libpad.lib version 4.2.1 for the Analog Controller (DUAL SHOCK)" patch, shipping
LIBPAD.LIB 4.2.1 with LIBAPI.LIB 4.2 and LIBPAD.H/LIBAPI.H/KERNEL.H.

Placed and byte-verified against the EXE (psyq_identify 0x8005CE18-0x800629DC, then
psyq_link.py per object): libpad 4.2.1 7/11 — PADENTRY, PADMAIN (760 ins, the 4.2.1 build,
exact), PADCMD, PADIF, PADPORTD, PADSEQD, WAITRC2 — and libapi 4.2 39/88 — the 21 band
trampolines, COUNTER, L02/L03, and the apicard-region C112/A50/A51/A54/A65/A67/A69/FIRST/A66/
PAD/A18-21/PATCH/CHCLRPAD. All 46 PASS. Neighbours for the record: plain libpad 4.2 and the
4.3 disc (DTL-S2340, 1998-05-18; PADMAIN 832 / PADIF 380 / PADSEQD 292) each place only 4;
4.2.1 is the unique exact match, so the game was built between Feb and May 1998.

Banked (R20): the 7z tracked under tools/psyq/ with sha256 + provenance in CHECKSUMS.sha256;
extracted to gitignored tools/psyq/lib42/ and lib421/ (the 4.2.1 headers are the band's
prototype oracle from now on); ELF in .run/obj42/{libpad421,libapi42}. Docs: psyq-worklist
"S79 task #13", SETUP archive table + §5.1 + S79 tool table, CURRENT_PHASE (#13 log; the S79
FINAL block's §5 records the archive and §6 is the re-scoped task #5 brief: link the whole
0x8005CE18-0x8005FC68 band and re-source the apicard region's libapi from 4.2).
2026-09-04 17:36:03 -06:00
Drew T 757bd82a0f feat(phase-31): S79 #4 — scattered-.bss split at link-prepare (psyq_bss_split): SYS.o→libgpu2, VM_F.o→snd12, GS_001.o→libgs8 LINKED; libgpu_used retired
The §9.1 "scattered .bss commons" exclusion class (Phase 8 → P31) is closed 3/3. New
tools/psyq_bss_split.py (own ELF32 REL reader/writer) cuts an object's packed .bss into
per-base NOBITS pieces: bases derived from the game bytes per HI16/LO16 pair, references
walked in offset order into single-base runs, cuts snapped to symbol starts (the linker
scattered SYMBOLS), symbols moved, a LOCAL section symbol per piece inserted, relocs
retargeted with the addend rewritten in the immediates, self-diffed. It runs inside the one
prepare step shared by psyq_link.link_object / psyq_link_region.build_region /
psyq_integrate.integrate (prepare_object before classify), re-derived every build.

GS_001.o was certified "5 interleaved bases, NOT splittable" by the S77 probe, which grouped
by BASE; by RUN it is six symbol-aligned pieces. All seven cuts across the three objects are
confirmed by the other objects' by-name recoveries (_que 0x800C5510, _svm_sreg_buf
0x800B9B58, PSDBASEX/CLIP2/PSDBASEY/POSITION/GsDRAWENV). R39 negative control: 235 placed
objects across 9 curated dirs, 0 refusals, exactly 3 splits (a libcd .bss+size end pointer
refused the first build → reference problems are fatal only when a split is needed).

Wiring: yaml 800c→libgpu2, sgap_6→sgap_6+snd12, gsgap3→libgs8 (comments rewritten);
LIBGPU_ELF := .run/obj40/libgpu (curated libgpu_used retired); libgs 34 objs/8 blocks
(make_libgs.sh +GS_001); snd 63/12 (make_snd_used.py exclusions 4→3). src/800c.c and
src/gsgap3.c removed (Sony code hand-matched as REAL/verbatim), sgap_6.c keeps only
func_8003FA54; splat-emitted libgpu2.c/libgs8.c/snd12.c stubs for the no-SDK fallback.

Verified: main 143dbb89f34491258bbc27810d0a12ec8b43a8dd WITH the SDK objects and WITHOUT
them from a fresh extract; make tools-health OK; R22 fleet clean extract-all 212/212 +
check-all 213/213. Metrics: main REAL 886→839, LINKED 1,040→1,150, VERBATIM 85→29, stubs 29
(unchanged); game-code weighted 91.1% (40,895/44,870) — both terms lost the 3,667 SDK ins;
the remainder is still exactly the 3,975-ins open-stub sum. Verbatim manifest --update
200→33 rows (subtractive). Docs: cookbook §489 (+index), psyq-worklist rows + "S78 task #4",
SETUP S79 R21 table, decision-log S79 addendum, accelerators S79, CURRENT_PHASE S79 FINAL 🛑.
2026-09-04 17:19:29 -06:00
Drew T a85733a487 feat(phase-31): S78 #3 — 13 "game code" subsegs were PsyQ objects: wired LINKED (libgte 70/30, libgs 33/7, snd 62/11); main's game-code metric corrected to 91.8%
- exact tiles, 0 tokens: libgte23-26 (MSC01/02/05/09, SMP_00, FGO_01-06, PATCHGTE), libgte9 re-derived
  as SMP_05 NormalClip (SMP_06 NormalClipS = nested sub-pattern; psyq_integrate now drops nested
  placements), libgte27-30 (the libgs-gap MTX_05/07/11, REG03+REG11), libgs7 (2D_BG0+2D_BG1), snd10
  (VM_NO1), snd11 (VM_NOWON carved off sgap_8). LINKED 959->1040, REAL 912->886 (SDK inline-asm wrappers
  re-provenanced), VERBATIM 146->85, 13 TUs deleted; splat re-emits the stub records.
- main 143dbb89 WITH and WITHOUT the SDK objects. The no-SDK fallback had been red since S7x
  (CdReadyCallback called by its SDK name while the libcd stub carried func_800435B4) — curated
  CdReadyCallback = 0x800435B4, refs unified. R22 clean fleet 213/213; tools-health OK.
- METRIC CORRECTION (R35): progress.py's "MAIN game-code weighted" sig never excluded the LINKED
  objects (its comment said it did) — ~31k linked-SDK ins sat in the denominator as unmatched game
  code. Exclusion now derived LIVE from the Makefile stub lists + yaml ranges: 91.8% (44,562/48,537),
  not 59.8%; the 3,975-ins remainder equals the open-stub sum exactly.
- VM_F.o probed SPLITTABLE at .bss 0x50c (SYS.o's class -> task #4). cookbook §488; worklist S78 #3;
  decision-log + accelerators; SETUP rows.
2026-09-04 16:26:12 -06:00
Drew T a7394f44dc feat(phase-31): S78 #12 — the 800c3 "wall" band is LIBPAD 4.2.1 + LIBAPI 4.2: 46 names applied; integrate wired by subseg range; renames via ApplySymbols
- provenance: the psx loader's per-version PsyQ signature sets place PADENTRY/PADCMD/PADPORTD/
  PADSEQD (4.2), WAITRC2 (4.3), COUNTER/C114/FIRST/PAD/PATCH/CHCLRPAD (libapi 4.2) byte-exact in
  0x8005CE48-0x8005FC68 / 800c2 -> 12 of main's 29 stubs incl. all four §332 walls are Sony's
  DualShock library in reorder mode. 46 names -> symbols.us.txt (count 1081), band TUs, verbatim
  manifest, wave_exclude; firstfile/firstfile2 (4.2 naming); CdGetToc @0x800430B8 (was the Phase-21
  xdedup mislabel DecDCToutCallback). SETUP §5.1 corrected; psyq-worklist S78; cookbook §487;
  decision-log + accelerators S78; CHECKSUMS +Psy-Q_46.zip +PSYQ_SDevTC_v4.5.zip.
- psyq_integrate: --yaml maps stub<->objects by SUBSEG RANGE with an exact-tiling check and PRINTS
  the located-but-unwired residue (libgte: 13 objs / 1,264 ins) — main's LINKED build had been RED
  at HEAD since the S77 psyq_identify fix (22 libgte blocks merged to 3; gate worktrees take the
  stub fallback so it never showed); a library object's exported symbol whose recovered address the
  curated file names differently is --redefine-sym'd (R15; A66 firstfile->firstfile2).
- Ghidra: 47 MCP renames did NOT persist through the sentinel stop (R9 caught it) -> NEW
  tools/ghidra_scripts/ApplySymbols.java + tools/ghidra_apply_symbols.sh mirror the curated file
  headless with a real save: 73 renamed, R9-verified x4. SETUP inventory rows (R21).
- lint_symbol_refs: scans verbatim __asm__ bodies (`.ent\tfunc_X` is invisible to \b and to the
  string-masked scan); negative-controlled (red on the pre-fix TUs, green on the passing tree).
- R22: clean extract-all 212/212 + check-all green on the final config; main rebuilt byte-identical
  143dbb89 after the last src-only fix -> 213/213; tools-health OK.
2026-09-04 15:57:06 -06:00
Drew T ddbe7f455c docs(R31): S77 decision-log — the frontier is wall-proof work now, and R61's twelve defects
Also: cookbook §486 (the main -O0 island carve, five coupled pieces), SETUP rows
for psyq_bss_probe and make sig-main-oracle (R21).
2026-09-03 22:48:58 -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 e0229af908 docs: restore §462/§463, record S76 tooling in SETUP, add the gate-triage step to the playbook
Three gaps found by auditing instead of asserting.

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

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

The playbook had nothing on what to do when a gate banks far less than it
staged — which is exactly what happened this session. Added the triage step:
probe first (CC1-FAIL 16 / DIFF 18 / MATCH 6 on main's 40), sync declarations
for the plumbing class, hand self_decl_tu to cast_self_callers, and expect a
cascade because every bank changes the declaration environment for the drafts
that follow it.
2026-09-03 17:41:43 -06:00
Drew T c05ea15cbe feat(tools): asm_in_c.py — 154 game functions are assembly wearing a .c extension
A .c file in src/ looks decompiled. 199 functions are not: they are the target
assembly pasted into a C string literal (§265), byte-identical BY CONSTRUCTION
and completely unexplained. 45 are PsyQ/CRT routines where that is defensible;
154 are GAME CODE, 171 of the 199 in main, the largest being SaveLoadRoutine at
1,165 instructions.

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

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

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

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

Cookbook §448, SETUP row. Law: when a count comes from a text pattern, the
pattern has a denominator too -- validate it against one known-true case of
every FORM the corpus contains before quoting the number.
2026-09-03 00:01:11 -06:00
Drew T cf7f837231 fix(gate): main's TABLE REJECT verdict was unreachable — SaveLoadRoutine is a CARVE, not plumbing
SaveLoadRoutine (1,165 ins) is the largest open function in the project, 9.2%
of all remaining work, and has been carried as the §434 WALL. Gated alone
through gate_main, with the §376/§378 chain already applied, the verdict layer
says: "SaveLoadRoutine is BYTE-IDENTICAL; all 3989 differing bytes are
ELSEWHERE". The body has been correct the whole time.

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

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

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

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

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

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

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

Cookbook §447. The law: a class that cannot fire is worse than a class that does
not exist -- it converts "I don't know" into confident, specific, wrong advice.
When a verdict names a subsystem, check that subsystem owns the MAJORITY OF THE
BYTES before acting on it.
2026-09-02 23:05:27 -06:00
Drew T cb948a6bbc feat(decomp): the ov_SC01 reloc-only cluster + its 5th latent victim — 5 fns, 1,301 ins
S74 handed this forward as "1,116 instructions behind one question": family_remap
on ov_SC01_004/005/006/008 gated DIFF 4/4 against the banked exemplar
ov_SC01_009:func_8017EB08, and the class had been carried as a codegen wall since
S70. The four bodies were byte-identical to the exemplar the entire time.

Word-level classification vs the exemplar, computed independently twice (a Fable
agent's script, then mine from scratch against the retail images), identical:

    nins=279   EQ 213 · RELOC-HI16 23 · RELOC-LO16 24 · INTERNAL-J 19 · CODEGEN 0

Zero register-allocation, instruction-selection or scheduling differences.

ROOT CAUSE — tools/jtbl_carve.py reserved ONE WORD TOO MANY per table:
  * spimdisasm runs an island's LAST `jtbl_` dlabel one word into the following
    NON-ZERO data (string bytes 0x696F760A / 0x000013FF / 0x62647020), so the
    zero-word trim cannot see it; and
  * the over-span clamp that would have caught it was guarded by
    `len(sltiu_bounds) == 1` -- but `sltiu` is ALSO how gcc emits an unsigned
    range check ((u32)(x-lo) < n, I1). These four carry five distinct sltiu
    immediates, so the guard silently disabled itself on precisely the functions
    that needed it.
  0x2C reserved for a 0x28 table => image 4 bytes short => ~850 %lo immediates
  shift => whole-binary DIFF about a function whose own bytes are perfect.
Fixed with a PER-TABLE bound: gcc-2.7.2's dispatch is a fixed idiom, so the
`sltiu` nearest ABOVE that table's own %hi(jtbl_X) is unambiguous whatever else
the function tests. Second defect stacked behind it: a carve span whose
JTBL_PADS line lacks a `tables=` comment lost its existing table's start on
merge and refused "table starts do not fit the span" -- which harvest_verify
then "repaired" with a needless jr_isolate_all that walked back into the first.

THE NEGATIVE CONTROL IS THE STORY. Run over every other open table-bearing stub
fleet-wide, the fixed bound changed exactly one more table: ov_SC06_022/
func_80185B80 (185 ins), a FIFTH victim nobody had drafted against. A guard that
disables itself on a common idiom does not fail once -- it fails quietly across
the whole corpus.

Banked, each with its own byte-gate verdict (--no-propagate, clean re-gate):
  func_8017EB30  ov_SC01_004  279
  func_8017F2D4  ov_SC01_005  279
  func_8017F2D4  ov_SC01_006  279
  func_8017EC68  ov_SC01_008  279
  func_80185B80  ov_SC06_022  185

Also here:
  * dedup_propagate: memoize find_site's mask (lru_cache) -- 54 ms of masking
    per call over the whole source, recomputed though it depends only on the
    text. 2x on that loop (58.3 -> 33.0 ms/call), NC identical on 120 addrs.
    Scoped honestly: that loop is ~2.4 min of a 30-min run; the profiler puts
    43% in family_remap._alias_decl_for, which is NOT fixed here.
  * Makefile: `clean` says out loud that BINARY= is ignored and it is fleet-wide
    (cookbook §445) -- it silently deleted asm/ for all 213 binaries this session.
  * Cookbook §446 (the carve law: when a standalone-MATCH jtbl draft gates DIFF,
    diff the carve extent against 4 x sltiu before touching the body), §445, and
    SETUP rows for both tools (R21).
  * CURRENT_PHASE: the S75 log, incl. the measured fleet dedup-hygiene census
    (~2,073 fns / ~12,116 items, all ALREADY MATCHED -- cleanup, not work) and
    Drew's decision to leave it and gate --no-propagate from here.
2026-09-02 22:15:20 -06:00
Drew T 0c2b37a287 fix(gate): overlays.mk carve-state snapshot was singular, so the revert half-restored
Found by running one reject to ground. After a gate that REJECTED
resident:func_800D06E8, config/overlays.mk had a 4th JTBL_PADS entry and had
LOST `--pre hdr.rodata.o` (the §440 resident leading-rodata sandwich). The
binary then would not build at all -- "consumed 3 rodata jump table(s) but 4 pad
spec(s) given -- table-count drift vs the carve" -- while src/ was perfectly
clean, which is the only place anyone looks before building.

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

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

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

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

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

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

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

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

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

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

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

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

Playbook: new step 2a-0 — the same-address lead is size-filtered now; read the `⚠ IGNORE` line, and
regenerate any pack built before S74 rather than trusting a bare address lead.
2026-09-02 19:53:56 -06:00
Drew T 3a886b9652 fix(tu-split): a block comment a construct OPENS MID-LINE and WRAPS defeated every peeler
FIVE independently-MATCHed ov_SC06_029 bodies were rejected by a `parse error before '#'` in a file
the GATE ITSELF generates, at a line no draft contains. The isolation emitted, into the §8b carried
decl layer:

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

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

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

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

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

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

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

The banks themselves are NOT in this commit: the agent's worktree predated func_8017F9C0's bank, so
adopting its TU verbatim would have destroyed one. They get re-gated against HEAD with these tools.
2026-09-02 19:22:16 -06:00
Drew T 95c7b7fe0f fix(tools): two tools read a source of truth describing a different world (+ hard-gate the third)
Three independent split agents hit both defects in one session, on the tools that CERTIFY and UNDO
the work they were doing. Each is fixed, negative-controlled against the exact failing case, wired
into its siblings, and documented in the same change (cookbook §436).

1. split_indicator attributed a jump table by the STUB'S DIRECTORY PATH. `make extract` does not
   prune a re-homed subseg's `nonmatchings/<old>/` dir, so after a correct, byte-green §431 split
   both the old and new dirs hold the moved stub — and the tool printed NEEDS SPLIT for a split that
   was already correct. owners() now derives the owner from the CONFIG by address (R33), exactly as
   jtbl_carve.func_subseg already does for the identical §8b hazard, and NAMES any leftover stub in
   a `note:` line. Notes now print on an OK verdict too: hiding one behind `st != OK` is the same
   defect in the other direction — a true verdict about a narrower world than the reader believes.
   PROVEN by planting a stale stub for func_80182A00 under its old subseg: OK + the note, where the
   old code would have seen one subseg owning two spans. --self-test still PASSes both directions.

2. jtbl_carve --revert did `git checkout --` on the WHOLE splat yaml. The carve owns only the
   trailing data/.rodata region; the `c` pieces are source configuration it never writes. The blunt
   form cannot tell "carve state I just added" from "the §431 split someone added to the same
   uncommitted file", so --revert after a carve PROBE silently un-split the overlay — each agent
   recovered only because they had backed the yaml up by hand. It now splices back only its own
   region (parse_config gained an optional `lines=` so the SAME region derivation runs over the
   committed text — one derivation, two callers), refuses loudly if the committed region carves onto
   a subseg the current config no longer defines, and reports how many uncommitted `c` pieces it
   preserved. PROVEN in the ov_SC01_084 worktree: carve → revert → the uncommitted split survived
   ("PRESERVED 30 uncommitted `c` piece(s)"), carve lines gone, diff back to the 6 split lines.

   SIBLING: jtbl_family_bank.revert carried the same blunt checkout for the isolation's code pieces.
   It now keeps whatever pre-dated the attempt (the `keep_regions` signal it already trusts for
   src/) and NAMES anything it drops — an isolation region and a §431 split piece are both
   `<ov>_jr_<addr>`, so no name test can tell them apart and only that signal can.

3. NOT A DEFECT, and recorded as such: a speculative carve fails the build with `jtbl_rodata_pads:
   consumed 3 rodata jump table(s) but 9 pad spec(s) given`. That is R43 working — the pad spec is a
   CONSEQUENCE of banking, not a prediction of it — and it reproduces identically on the pristine
   unsplit config, so it is never evidence about a split.

make tools-health: split_indicator is a HARD GATE now, as its own comment promised it would become
once the last violation was split. 213 OK of 213; a new one fails the build instead of being echoed
past.

Cookbook §435 (an overlay TU split is near-free — 0/3,074, 1/2,679, 2/3,254 names crossed, because
the §8b carried decl layer re-emits externs per region so only typedefs can cross; and the gap test
between two rodata runs is "is this word a valid code address", not "is it zero") + §436 (the two
defects and the shape they share). Playbook + SETUP.md carry the emptied CARVE-BLOCKED class.
2026-09-02 18:11:48 -06:00
Drew T 0840c0991c docs(SETUP): fix four stale spots the tool-inventory update missed
I updated SETUP's tooling INVENTORY when each tool changed, but not the HUMAN-facing
procedure, and §6.6 is where a person learns the matching loop:

* :493 still said 'In src/800.c, replace the INCLUDE_ASM line with the C function body.'
  main's game code is THREE TUs since S72, and WHICH one is load-bearing for any switch
  function — one code object contributes exactly ONE contiguous .rodata run, so the TU
  decides which jump-table span the body's table lands in. Following that line for a
  span-B/C function re-creates the exact §426 double-emit this session existed to remove.
  Replaced with the vram -> TU -> asm-path -> span table.
* :759 listed main_diff_locate's verdicts as an exhaustive three — BODY / PLUMBING /
  MIXED. There are FOUR, and the missing TABLE REJECT is checked FIRST and covers
  precisely the case the PLUMBING clause claimed ('byte-identical, everything differs
  elsewhere'), routing the reader into the one chain the tool forbids for that class.
* config/wave_exclude.txt was named nowhere in SETUP despite being tracked config that
  draw_waves now requires. Added, with both entry classes and the WALL pin.
* :537 described ld_interleave as --front/--tail only; main uses --order since S72.
2026-09-02 17:00:16 -06:00
Drew T 983df054f2 feat(draw): audit the exclude list as a PREREQUISITE — a stale one is refused
An exclude list records what the TOOLING could not do, then gets treated as a property of
the FUNCTIONS. Nothing re-examined it, so every tool fix left behind a population that is
now tractable and still marked impossible — invisible, because the draw filters it out
before anything measures it.

MEASURED one day after .run/S71_exclude.txt was written: 88 of its 107 entries were
stale — 28 already banked, 14 linked PsyQ symbols that were never targets, and 46 whose
blocker had since been fixed. Those 46 are 12,750 instructions of open, drawable work
including main:SaveLoadRoutine (1,165), the largest function left in main.

* tools/exclude_audit.py (NEW) — classifies each entry by its CURRENT blocker
  (BANKED / LINKED / RE-PROBE / CARVE-BLOCKED / WALL), regenerates keeping only the
  still-valid classes, and --assert-fresh exits 3 on staleness.
* draw_waves --exclude-file — runs that audit and REFUSES to draw on a stale list, naming
  the counts and the regenerate command. --exclude-stale-ok still draws but prints what it
  ignores: skipping is possible, never silent. Also fixes the old --exclude parsing, which
  could not survive a '#' comment.
* .run/S72_exclude.txt — the regenerated list: 19 entries (16 CARVE-BLOCKED + 3 WALL),
  each carrying its reason, down from 107.

Verified in all three directions: stale refuses rc=1, fresh proceeds rc=0, override
proceeds and announces. The parser's own report-don't-drop design caught a bug I
introduced in it (comma-splitting before comment-stripping).
2026-09-02 14:38:39 -06:00
Drew T 8e8521da22 fix+docs: make every consumer aware of main's new TU layout (R36)
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').
2026-09-02 13:48:00 -06:00
Drew T e94bb20833 docs: bank the S72 main carve finding — cookbook §426/§427, playbook 1c, SETUP, decision log
§426 — main's switch functions were never a codegen wall: one .rodata carve had been
missing since Phase 7, so a drafted switch double-emitted its jump table (+28/+52/+76/+84
image growth, 238 symbols shifted, first mover jtbl_80072A4C every time). Includes the
derived name-address overlay that names a layout shift with no reference build, the span
table, and why spans B-D need src/800.c split at the original TU boundaries the spans reveal.

§427 — a hash is a correctness oracle with zero diagnostic content; preserve the red
artifact before anything rebuilds over it, attribute per byte, and negative-control the
localizer in both directions.

playbook 1c — which main jtbl functions are drawable (R45), and the gate note: main is not
in the parallel lane, read the BODY/PLUMBING/MIXED verdict before recording a main reject.
2026-09-02 12:05:01 -06:00
Drew T 6c904ebd0c docs: close the S71 documentation gaps - SETUP tools table, playbook steps, decision log, gate ledger
Audit found real gaps rather than assumed coverage:
* SETUP.md (R21) had NONE of the five tools written this session. Added a table for
  journal_notes / launch_check / gate_triage / restage_matching / weave_sweep, each with
  when you need it, plus the two gating rules now enforced in code (parallel_gate refuses
  main; gate_main refuses a no-op draft and counts banks from the source).
* wave-playbook: launch_check as step 4c (payloads go stale while gates run - 3 of 27
  wave-2 targets were already banked) and gate_triage as step 6b with the measured
  blocker census.
* decision-log (R31) held only the §406 pivot. Added the two strategic entries this
  session actually turned on: gating main with a tool documented as unable to gate it
  (false PASS, caught only by R22), and the drafting pool running dry while the lever
  was an exclude list nobody re-probed after a tool fix.
* CURRENT_PHASE: the per-gate ledger for all 14 cycles plus the carve/rebase/main gates.
* Two memories: gate-main-only-with-gate-main, reprobe-exclude-lists-after-tool-fixes.
2026-09-02 11:19:49 -06:00
Drew T dff33f66d9 docs(setup): WSL memory cap lowered 48GB -> 32GB (Drew, 2026-09-02); applies on next wsl --shutdown 2026-09-02 01:49:46 -06:00
Drew T 8f87334399 docs(setup): WSL2 memory reclaim — autoMemoryReclaim=gradual + the interim cache dropper
Task Manager showed WSL holding 30 GB while Linux was using 4 GB and ~22 GB was
reclaimable page cache (a gate wave reads the 450 MB asm/ tree plus every build
object). .wslconfig had a memory cap but no reclaim policy, so WSL2 never handed
freed pages back to Windows. Added autoMemoryReclaim=gradual (needs wsl --shutdown),
recorded the no-restart manual reclaim, and .run/memkeeper.sh as the interim
automatic form. R21.
2026-09-02 01:17:25 -06:00
Drew T 20f80fd933 docs: §400 + SETUP + carve-state memory for the new-file adoption fix; correct a stale docstring
§400 — a baseline check that conflates "absent everywhere" with "changed under
us" silently drops new files. The general law: when a comparison uses two
different sentinels for "nothing" ("" from a failed command, None from a missing
file), it reports a difference that does not exist — and in a GUARD, a phantom
difference becomes a refusal, which looks exactly like the guard working.

Corollary recorded in both §400 and the carve-state memory: "never blanket-add"
covers SHARED carve state (overlays.mk, splat yamls). It does NOT cover a carve's
own new per-binary source file, which is named by a committed yaml and whose 31
siblings are tracked — that one must be adopted with the bank that created it.

Docstring correction: parallel_gate does NOT use `git add -u src/` (that is
gate_stage's form); it adds exactly the adopted paths. My first diagnosis of this
bug blamed `-u` on the strength of that stale line and was WRONG — the cause was
the baseline comparison. Noted in the docstring so the next reader is not
misdirected the same way.
2026-09-01 15:33:04 -06:00
Drew T 8f171c6ce6 feat(tools): verify_binary + twin_rescan — put S69's two habits in the tooling, not in prose
Both rules were already written down (§384, §397) and both were violated anyway,
which is the argument for a tool: a habit you must remember at the moment you are
impatient is not a control.

tools/verify_binary.py — ALWAYS re-extracts before building, because a carve
rewrites splat inputs and a build over stale extract state produces a meaningless
SHA. S69 read three binaries as red on build-only checks; all three were
BYTE-IDENTICAL after extract+build, and two false reds cost legitimate work that
had to be restored (a 96-line match, and 23 declaration edits). --all-touched
sweeps everything with uncommitted src/ or config/ changes.

tools/twin_rescan.py — the twin oracle answers "is there a BANKED body like
this?", so an OPEN-OPEN cluster correctly reports "no banked twin" for every
member and that verdict is stale the instant one banks. Diffs the scan against
the previous snapshot so it reports what JUST became free, not the whole board,
with the ready-to-run family_remap command per row. Baseline: 318 open stubs, 37
already carry a banked twin at d<=5.

Memories added: rescan-twins-after-every-bank, check-against-a-known-true-case.
2026-09-01 14:50:26 -06:00