25 KiB
Phase 3.38 — Struct unification (implements GENERATION_PLAN.md phase 3.38)
Milestone: make tools-health OK and make kit-corpus clean at phase start (T1); PY tools/type_census.py --check-structs → 0 duplicate-layout classes, 0 definitions outside the canonical type files, 0 dead names, 0 variant names, 0 legacy pad<SIZE> names, 0 parse-error (type-not-visible) declarations, controls 4/4 (from 7,255 defs / 206 dup classes / 40 variant camps / 26 pad<SIZE>); R22 check-all: 218 passed, 0 failed of 218 — verified by: .venv/bin/python tools/type_census.py --check-structs exit 0 and make clean && make extract-all JOBS=16 && make check-all JOBS=16 exit 0 at the closing commit
Approved: 2026-09-29 Planner: claude-opus-5-5/medium Plan-hash: 81ea169f9e3057048c934c68dd6547dfa092140d241f125f7a0d1baa3bd7bcd7
Context
- Project: BFM (PS1) matching decomp, 100 % byte-identical since P32. Gen 3 = readability. P35 dedup, P36 levers off, P37 (legacy) = the structs phase part one: census, struct map,
restructengine, the declaration layer to its floor. This phase = legacy P38 / P37-carry T5 ("rung L"): one canonical definition per TYPE. - Seed sources (not to be re-read by experts):
phase-ends/PhaseEnd_Phase37.md:75-99(carry list),phase-ends/logs/Phase37.mdS108 checkpoint (~L820-965), cookbook C0507/C0508. Digest below is what they say. - Oracle = the byte gate. Every edit batch is judged by
tools/restruct.py(whole-object byte compare; LINKED mode for global-block edits whose relocation spelling moves but linked bytes do not) and committed only after a green R22 (make clean && make extract-all JOBS=16 && make check-all JOBS=16, ~95 s, read the exit code; green =extract-all: 217 extracted, 0 failed of 217+check-all: 218 passed, 0 failed of 218). - Interpreter: BFM tools run under
.venv/bin/python(MakefileVENV_PY),-j 16/JOBS=16, repo on ext4. PA tools (plan_edit,task_log,research_add) run under/usr/bin/python3.tools/type_census.py,tools/restruct.py,tools/restruct_cycle.sh,tools/lift_types.py,tools/delever_oracle.py,tools/lever_progress.pyare BFM's own product tooling (not Project Architect's) and are in scope to edit. - Census state:
.run/P37/census/type_census.txtis STALE (2026-09-13 atbd7d32ec2, before T4). Its DEFINITIONS line: 7,255 bodies = canonical header 1,179 ·.cfile-scope 4,085 ·.cblock-scope 1,904 · per-function shared headers 63 · other headers 24 · include/ 0; 3,143 names, 527 layouts, 206 duplicate classes over 2,823 names (tier 1 122 same-named, tier 2 84 opaque; 46 layout-twin classes / 1,867 names kept apart); 40 variant camps. T4-close figures: 1,796 lying declarations / 76 callees (K&R-empty1,373,narrow423), 5,262 K&R sites counted apart (the original's calling convention, not debt), 418 cross-binary apart. T1 re-baselines. - Canonical type files:
CANON_HEADERS = ("src/shared/engine_types.h",)attools/type_census.py:55; comment there says T3/T5 addsrc/shared/main/types.hand the slots'. Existing trees:src/shared/{main,ov,slot_800CAE08,slot_801E25E8,slot_801E7B28,slot_801ED988,slot_801EF468}/,src/shared/engine_prelude.h,src/800_shared.h(main). Onlyinclude/{common.h,gte_inline.h,include_asm.h}ininclude/. - Head types: the three names
Unkstruct_80126B58(entity/actor, 0x24C, 146 fields, preview skipped {overlap 28, misaligned 1}),Unkstruct_800B5CB8(0x10C, 90 fields, skipped {overlap 44}),Unkstruct_80078E00(the player block, instance at 0x80078E00;docs/actor-struct.md:3"~154 fields over 0x24C") exist ONLY in the generated preview.run/P37/restruct/types_preview/ov.h(fromrestruct.py --write-types --top 6 --space ov); 0 occurrences insrc/,include/,docs/. The overlaps are the struct map's width conflicts: the union-vs-split call is per offset. Source map:.run/P37/census/struct_map.json(18,760 types, explains 498,896/503,016 sites, 99.2 %), rendereddocs/struct-map.md(generated, never hand-edited). Parked P36 work:.run/P37/census/parked.tsv(24 classes, 453 bodies; struct 7). - Naming law (G62):
Unkstruct_<addr>placeholders,unkHEXfields with/* 0xNN */comments and// size = 0x..; a field gets a name only with cited evidence (docs/memory-map.md§3.4,docs/actor-struct.md); PsyQ layouts take Sony's SDK names. Layout twins without evidence stay apart (S106 "per type", not "per layout"). - Byte facts (C0507 §458): struct spelling is a per-access dial —
*(T*)(p+k)carries noMEM_IN_STRUCT_P, a member access does, so a cast→member rewrite can move scheduling. An opaque-tier fold renames members (T2 probe: 8 of 20 TUs), so rung L rewrites accesses too, and every fold is judged, never assumed neutral. (C0508 §459): a K&R site is per-TU, but a definition in a shared header makes it per-definition; a return type is observable in a delay slot; what a prototype cannot hide: types the TU cannot see (1,120parse error→DECL-KEPT"type not visible",tools/restruct.py:1741-1745),conflicting types(417,:1737-1739), jr-merged callers (10, → 3.44), void defs whose callers use$v0(5, → 3.40), zero-arg callers passing their own$a0(→ 3.40). - Owed from P37:
make tools-health(last green at P36 close; >15 min,audit-cdecl~787 s) andmake kit-corpus(the kit'stool_census --checkmay read red until then);docs/actor-struct.md: +0x108/+0x10C are 4-byte andD_80078F08/0Care code-width 4, not 2 (the census control prints them asdoc-disputed); the 45 TU-CONFLICT rows; the 13syntax errorunits insrc/800.c(600 KB; the marker lives in the restruct ledger / census logs, not the source); the cookbook §396(a) correction. - Tree at plan time:
config/kit_coverage_map.tsvmodified (uncommitted),phase-ends/PhaseEnd_Phase37.mduntracked (the router's migration ceremony owns it; tasks never stage it). Disk: ~22 GB free;.run/P32/(6.6 GB) and parts of.run/P36/are prunable scratch, but.run/P36/delever/baseline/and.run/P36/delever/ledger.jsonlare live inputs ofdelever_oracle/lever_progress. - Gotchas carried (S108, binding for every task):
- Stop a judge with
kill -INT <pid>, WAIT for exit (~2 min, finishes in-flight units), only then--restore; an early restore consumesinflight.json(S108: 6 files hand-reverted, 197 orphan rows). - Never put a literal of your own command line in
pgrep -f/pkill -f; bracket a char (restruct_cycl[e]). - Never edit tracked files the cycle rewrites between a cycle's R22 and its commit; check
psformake. delever_oracle --snapshot-baselineonly after a GREEN clean fleet run;--applyrefuses a stale snapshot;restruct_cycle.shrefreshes snapshot + calibration itself when HEAD moved.DECL-NONEis a DONE verdict: redraw with--redraw VERDICT:cause, never bareDECL-NONE.- A flat yield is not "nothing drawable": bucket the remainder by ledger verdict before stopping (
md_*hid 3,700 never-judged pairs). - A pack's "one caller" is a claim:
grep -rnE '\bfunc_X\s*\(' srcbefore a signature unit. - Census blind spots that moved counts by thousands: include graph not followed, name-keyed maps (6,415 names carry >1 body), column-0 definition regex, one-sided redraw key, snapshot stamped fresh over empty
build/. Key every map by (binary, address, body). - Shared headers are judged serially (~35 s per judgement over 141 includers): budget header batches accordingly.
- Cycle launch form (detached, outlives the agent):
setsid nohup bash -c 'LABEL_PREFIX=<p> TASK=<T> CENSUS=0 tools/restruct_cycle.sh START END 400 <RUNG>; echo "cycle exit=$?"' > .run/P38/restruct/cycle_<x>.log 2>&1 &;HEADERS=1for header batches;REDRAW="<VERDICT>:<cause>"to redraw. Scratch for this phase:.run/P38/. type_census --selftest(21 tests) andrestruct --selftest(54;--realadds calibration + known-true + linked controls) re-run after any engine change; known-true controls:restruct --try <TU> func_801814AC --base global:D_801C7E30 --recipes→ DIFFERS, S2 keeps 1, R1 IDENTICAL.
- Stop a judge with
- Long commands: one tool call stays under 285 s. Anything longer:
bash tools/run.sh --bg <name> -- <cmd>thenbash tools/run.sh --wait <name> --max 280(repeat). R22 (~95 s) fits one foreground call.
Rationale
- Instrument first (T2).
type_census --check(tools/type_census.py:1621-1634) also fails on raw cast sites (~503k, phase 3.39) and lying declarations (K&R-empty/narrow residue, phase 3.40), so it cannot go green this phase; it does not gate dead names at all, and no mode counts parse-error declarations. The generation milestone's content is kept exactly; T2 adds the phase-scoped--check-structsthat asserts it, and the closing expert runs that.--checkitself stays unchanged for 3.39/3.41. Rejected: reading numbers off the census text by hand (R85: a check compares derived figures; G58: generated, never typed). - The parse-error count comes from the restruct ledger's latest verdict per unit, read through restruct's own latest-verdict function (imported), never a second filter (P37 proposed rule (c): one "done" filter used by every side).
- Head types before the long tail (gen-plan order): the two big types own most of the 206 classes' members and the 1,120 type-not-visible declarations; folding them first makes the long tail mostly mechanical
lift_types.py/ rung L work, and T6's redraw depends on the types being visible. effort: highon T3 only (1 of 7): the union-vs-split decision per overlapping offset (28+1 on the entity, 44 onUnkstruct_800B5CB8) and the canonical-file layout are judgments the byte gate cannot arbitrate — both spellings can be byte-identical; the choice rests on reading the access evidence instruct_map.json(which functions read which width at which offset) and on what a reader of the C should see. T4 then applies that design mechanically at medium.- T3 designs + proves on exemplars; T4 applies fleet-wide. Splitting keeps the Fable context on the judgment and the rollout on cycles at medium.
- T5 (long tail) separate from T4: different instrument (
lift_types.py+ rung L cycles vs a designed layout), different evidence, and a natural checkpoint where--check-structsshows the head types gone from the dup classes. - T6 (declarations) after T5: the gen plan's "most of the 417
conflicting typesfall to the canonical files" and the 1,120parse errorredraw only pay once every type is in a canonical file every TU includes. The 13src/800.csyntax-error units and 45 TU-CONFLICT rows are the same class (declaration environment) and ride along. docs/actor-struct.mdfix goes in T2: the census controlplayer_block_widthsreads it and currently prints twodoc-disputedrows; the instrument task owns making its controls clean.- Coder tier opus55 throughout (the only tier). Test design: no new suites; the gates are R22, the restruct judge,
type_census --selftest+--check-structs, and T2's known-true case (the current tree must FAIL--check-structswith its current counts before any fold lands). - No
## Developer decidesitems: naming follows G62, file layout and union/split are engineering (T3), agent caps do not apply (no wave lanes this phase).
Interfaces
tools/type_census.py(BFM):main():1596 — flags-j/--jobs,--out-dir(defaultOUT_DIR_DEFAULT=".run/P37/census":53),--no-cache,--sites,--check(:1621-1634: violations =definitions.dup_layout_classes,in_c_file_scope+in_c_block_scope+in_shared_fn_headers+other_headers,casts.deref_total,decls.lying; printstype_census --check: OK|FAIL — …),--selftest,--quiet. Always-on:casts.coverage_ok(COVERAGE FAIL),controls.ok == controls.n(CONTROL FAIL).CANON_HEADERS:55. Summary keys built :1271 (variant_names,dead_canon_names,shadowed_canon_names); dead names computed :1142-1154.run_census(jobs, use_cache=True, out_dir=OUT_DIR_DEFAULT, want_sites=False) -> (summary, txt):1029;controls(defs_all, sites_all, extern_data, by_text, name_layouts):1409;render(s, dup_classes, variants, types):1455;render_struct_map_md(s, types, path):1508;selftest():1559;find_definitions:66,find_decls_and_flows:440,build_struct_map:735.tools/restruct.py(BFM) argparse :3622-3661:--try TU FN,--base bclass:name,--recipes,--plan,--apply,--rung {S,D,L}(default S),--batch N(50),--label,--headers,--only …,--redraw VERDICT[:cause] …,--try-file,--callee,--signature,--body,--all-defs,--map-data,--with REL=FILE,--tu,--unalias,--restore,--status,--check-ledger,--audit-types,--write-types --type … --top N --space ov|… [--pointees],--selftest [--real],--probe,--audit-layouts,--probe-defs(fold a TU's duplicate file-scope definitions onto the canonical header). Declaration verdict causes :1737-1745 (conflicting types …,type not visible: … (T5 canonicalises it)).tools/restruct_cycle.sh START END 400 <RUNG>envLABEL_PREFIX,TASK,CENSUS,HEADERS,REDRAW; stops on "nothing drawable".tools/lift_types.py(BFM):main():120, flags :122-127--types a,b,--candidates,--apply,--exclude a,b; dry-run by default ([dry-run] would lift N type(s) to engine_types.h …:219);HEADER = src/shared/engine_types.h:29;scan(names=None):36,candidates():67; importsbuild_engine_types(brace-awarefind_defs,find_typedefs).tools/delever_oracle.py(BFM):--snapshot-baselineflag :613 →snapshot_baseline():218 (writesbuild/**/*.o+_link/<alias>/+links.jsonto.run/P36/delever/baseline/, records HEAD; refuses a shrunkenbuild/:241 or non-reproducing objects :271);--calibrate [BIN …]:611 →calibrate(aliases, jobs, recipes):532 (untouched compile == build/, stale head rejected :602).tools/lever_progress.py(BFM) :158-161:--snapshot MILESTONE,--render(docs/levers.md block),--table,--check.- Makefile:
kit-corpus:255-256 ($(VENV_PY) tools/tool_census.py --all);tools-health:269-360 (last linetools-health: OK …);check-all:1236 (check-all: N passed, M failed of 218, exit 1 on shortfall); R22 recipe :166/:1209. - Records:
.run/P37/census/{type_census.json,type_census.txt,struct_map.json,struct_map_top.json,sites.jsonl,parked.tsv};.run/P37/restruct/types_preview/ov.h; restruct ledger (read viarestruct.py --status/--check-ledger). - New in T2:
type_census.py --check-structs→ printstype_census --check-structs: OK|FAIL — <violations>and one counts linedup_classes=… outside_canon=… dead=… variants=… pad_names=… parse_error_decls=… controls=a/bplus non-gating reportconflicting_types=… tu_conflict=… types_floor_lying=…; exit 1 on any gating violation.
Cookbook
C0507 (§458 struct spelling per-access dial), C0508 (§459 declaration-layer byte facts), C0067 (§64 lift_types' three laws: fold the tagged typedef, check visibility, strip only textually identical), C0224 (§203 a deduped typedef must precede every splice point), C0012 (§8c a TU's declaration environment), C0013 (§8d templating must not change the declaration environment), C0103 (§92 conforming a declaration: pointer changes caller-neutral, width changes not), C0114 (§103 a file-scope extern constrains every later function), C0257 (§236 the declaration layer's nine gate failures).
Research
- (no new reports) — planning digest of
PhaseEnd_Phase37.md§Carried,logs/Phase37.mdS108 checkpoint, C0507/C0508, and a code retrieval of the tool interfaces above; no report ids were allocated.
Developer decides
- (none)
Triage
- (none)
Tasks
- T1 | next | expert-opus55 | title: phase-start health and baselines | coder: opus55 | effort: medium | files: config/kit_coverage_map.tsv, .run/P38/baseline/ | done-when:
make tools-healthlast linetools-health: OK;make kit-corpusrun and its outputs committed (tree clean of kit drift,config/kit_coverage_map.tsveither regenerated-and-committed or explained); R22 green;delever_oracle --snapshot-baselinethen--calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16OK; freshtype_census.py --out-dir .run/P38/census+restruct.py --audit-typesbaseline numbers recorded in the summary (defs, dup classes, variant camps, dead names, pad names, lying by kind, controls) | verify:make clean && make extract-all JOBS=16 && make check-all JOBS=16 && .venv/bin/python tools/delever_oracle.py --calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16| reads: — | deps: — | est-ctx: 70k | review: no | wait-for: — tools-health:bash tools/run.sh --bg tools-health -- make tools-healththen--wait tools-health --max 280repeated (>15 min); delete leftoversrc/shared/.cdecl_allmacros.c;psfor orphaned workers after. A red item is a product-tool defect → coder fixes, commit, re-run; three distinct red items →blockedwith the list. Order: kit-corpus → tools-health → commit → R22 → snapshot-baseline → calibrate (snapshot only after the green clean run). Recorddf -h .in the summary; do not prune scratch. - T2 | queued | expert-opus55 | title: --check-structs instrument and actor-struct fix | coder: opus55 | effort: medium | files: tools/type_census.py, docs/actor-struct.md | done-when:
type_census.py --check-structsexists per ## Interfaces; gates dup classes, outside-canon defs, dead names, variant names, legacypad<SIZE>names (same rule asrestruct --audit-types, imported not re-implemented), parse-error declarations (restruct ledger latest verdictDECL-KEPTcausetype not visible, via restruct's own latest-verdict function), controls; known-true case: on the T1 tree it FAILS with exactly T1's baseline counts;--selftestgains ≥2 tests for the new mode and passes;docs/actor-struct.md+0x108/+0x10C andD_80078F08/0Ccorrected to 4-byte and the control line shows nodoc-disputedrows | verify:.venv/bin/python tools/type_census.py --selftest && .venv/bin/python tools/type_census.py --out-dir .run/P38/census --check-structs; test $? -eq 1| reads: tasks/T1.md | deps: T1 | est-ctx: 80k | review: no | wait-for: —CANON_HEADERS(:55) becomes the list T3 decides; T2 makes it a single constant both census and--check-structsread. Do not change--checksemantics (3.39/3.41 use it). - T3 | queued | expert-fable | title: head-type layouts and canonical files | coder: opus55 | effort: high | files: src/shared/engine_types.h, src/shared/main/, src/800_shared.h, tools/type_census.py (CANON_HEADERS only), docs/struct-map-decisions.md | done-when:
Unkstruct_80126B58,Unkstruct_800B5CB8,Unkstruct_80078E00defined once each in the canonical file(s) with every overlapping offset resolved (union or split) and a per-offset decision record citing the struct-map access evidence (docs/struct-map-decisions.md); canonical file set decided (ov / main / md_* / slots) and written intoCANON_HEADERS; each layout proven on ≥2 known exemplar TUs per space viarestruct --try/--try-fileIDENTICAL (incl. one main and onemd_*TU);func_801814ACknown-true control unchanged; R22 green | verify:.venv/bin/python tools/restruct.py --selftest --real && make clean && make extract-all JOBS=16 && make check-all JOBS=16| reads: tasks/T1.md, tasks/T2.md | deps: T2 | est-ctx: 120k | review: no | wait-for: — Inputs:.run/P37/restruct/types_preview/ov.h(regenerate withrestruct.py --write-types --type <name> --space <s> --pointees),.run/P37/census/struct_map.json,docs/actor-struct.md,docs/memory-map.md§3.4. Field names only with cited evidence (G62); elseunkHEX. Decide whether main (src/800_shared.h) andmd_*share the ov definition or carry a per-space canonical file, by the layouts' identity in the map, never by name. - T4 | queued | expert-opus55 | title: fold the head types fleet-wide (rung L) | coder: opus55 | effort: medium | files: src//*.c, src//*.h (via restruct cycles) | done-when: every local definition and variant of the three head types folded onto T3's canonical definitions through judged rung-L batches (accesses rewritten where an opaque-tier fold renames members);
--check-structsshows none of the three in any dup class or variant camp; remainder bucketed by ledger verdict (kept units listed with cause); each batch committed after its own R22 | verify:.venv/bin/python tools/type_census.py --out-dir .run/P38/census --check-structs; .venv/bin/python tools/restruct.py --check-ledger && make clean && make extract-all JOBS=16 && make check-all JOBS=16| reads: tasks/T2.md, tasks/T3.md | deps: T3 | est-ctx: 110k | review: no | wait-for: — Launch: cycle form in ## Context withTASK=T4 LABEL_PREFIX=t38_4 … L,--only <type names>; shared headers withHEADERS=1(serial). Onboard main andmd_*in this task.delever_oracle --calibraterefresh when HEAD moved (the cycle does it). - T5 | queued | expert-opus55 | title: long tail — one definition per type | coder: opus55 | effort: medium | files: src/shared/engine_types.h, src/shared/, src//*.c, tools/lift_types.py | done-when: remaining duplicate classes folded via
lift_types.py --applyand rung-L cycles; 40 variant camps uniquified; dead canonical names deleted; the 26 legacypad<SIZE>names renamed tounkHEX/pad_<off>form; PsyQ layouts carry Sony's SDK names; layout twins without evidence left apart and listed;--check-structsgating counts dup_classes/outside_canon/dead/variants/pad_names all 0 | verify:.venv/bin/python tools/type_census.py --out-dir .run/P38/census --check-structs; make clean && make extract-all JOBS=16 && make check-all JOBS=16| reads: tasks/T2.md, tasks/T3.md, tasks/T4.md | deps: T4 | est-ctx: 130k | review: no | wait-for: —lift_types.py --candidates→ dry-run →--applyper type list; C0067's three laws and C0224 (typedef before every splice point). A tool defect inlift_types.pyis fixed in this task (product tooling). Commit per batch (R42) before the next gate. - T6 | queued | expert-opus55 | title: declaration layer on the canonical types | coder: opus55 | effort: medium | files: src//*.c, src//*.h, src/800.c | done-when:
restruct.py --redraw DECL-KEPT:type(type-not-visible) andDECL-KEPT:conflictingredrawn to exhaustion and the remainder bucketed;--check-structsparse_error_decls = 0; the 13src/800.csyntax-error units and the 45 TU-CONFLICT rows each resolved or kept with a named cause; summary reports before/after for types-floor lying (1,537), conflicting types (417), TU-CONFLICT (45) | verify:.venv/bin/python tools/type_census.py --out-dir .run/P38/census --check-structs; .venv/bin/python tools/restruct.py --check-ledger && make clean && make extract-all JOBS=16 && make check-all JOBS=16| reads: tasks/T1.md, tasks/T5.md | deps: T5 | est-ctx: 110k | review: no | wait-for: — Out of scope (other phases): jr-merged callers (3.44), void-def/$v0and zero-arg$a0callers, return-type lies (3.40), cast sites (3.39). Grep callers before any signature unit. - T7 | queued | expert-opus55 | title: milestone gate and record | coder: none | effort: medium | files: docs/struct-map.md, docs/levers.md, cookbook/, phase-ends/current/ | done-when:
--check-structsexit 0 on a fresh census; R22 218/218 at the closing commit;type_census --selftest,restruct --selftest --real,lever_progress --checkOK;docs/struct-map.mdregenerated; cookbook §396(a) correction and the phase's generalizable gotchas added viatools/cookbook_add.sh| verify:.venv/bin/python tools/type_census.py --out-dir .run/P38/census --no-cache --check-structs && make clean && make extract-all JOBS=16 && make check-all JOBS=16| reads: tasks/T1.md, tasks/T4.md, tasks/T5.md, tasks/T6.md | deps: T6 | est-ctx: 60k | review: no | wait-for: —
Risks
tools-healthred on a stale tool after P36/P37 edits → T1 stalls. Detection: its log's failing stage; fix as product-tool work in T1,blockedif three distinct causes.--check-structsmeasures the wrong scope (the P37 blind-spot class). Detection: T2's known-true requirement (current tree FAILS with T1's exact counts) and selftests; T7 re-runs with--no-cache.- An overlapping offset admits no single layout that keeps every TU IDENTICAL (union spelling changes codegen per C0507). Detection: T3's exemplar probes DIFFER; resolution in T3 is a split or a kept per-TU view, recorded; if the kept set is large, T3 returns
blockedwith counts (a scope question for REPLAN). - Header batches are serial (~35 s × 141 includers): T4/T5 wall-clock may be long. Detection: cycle logs; mitigate by batching per space, never by skipping the judge.
- Disk (~22 GB free) under cycle scratch + snapshots. Detection:
dfin T1 and each cycle log; ENOSPC → stop, report; prune only.run/P32/scratch, never.run/P36/delever/. DECL-KEPTremainder after T6 may be non-zero for reasons outside types (arity). Only the type-not-visible cause gates; others are reported and fall to 3.40.- Stale snapshot / early restore corrupts the tree (S108). Detection:
--applyrefusal; the SIGINT-wait-restore order in ## Context.
Changes
- 2026-09-29 developer: launch.py _audit_flag_line sort key made type-safe via natural_key (PhaseEnd_Phase3_5 crashed router seed after approval); fix applied outside the loop by a coder; upstream report filed by developer