Files
BFM-decomp/phase-ends/current/PHASE_PLAN.md
T
2026-09-29 19:30:25 -06:00

25 KiB
Raw Blame History

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, restruct engine, 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.md S108 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 (Makefile VENV_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.py are BFM's own product tooling (not Project Architect's) and are in scope to edit.
  • Census state: .run/P37/census/type_census.txt is STALE (2026-09-13 at bd7d32ec2, before T4). Its DEFINITIONS line: 7,255 bodies = canonical header 1,179 · .c file-scope 4,085 · .c block-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-empty 1,373, narrow 423), 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",) at tools/type_census.py:55; comment there says T3/T5 add src/shared/main/types.h and 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). Only include/{common.h,gte_inline.h,include_asm.h} in include/.
  • 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 (from restruct.py --write-types --top 6 --space ov); 0 occurrences in src/, 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 %), rendered docs/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, unkHEX fields 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 no MEM_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,120 parse 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) and make kit-corpus (the kit's tool_census --check may read red until then); docs/actor-struct.md: +0x108/+0x10C are 4-byte and D_80078F08/0C are code-width 4, not 2 (the census control prints them as doc-disputed); the 45 TU-CONFLICT rows; the 13 syntax error units in src/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.tsv modified (uncommitted), phase-ends/PhaseEnd_Phase37.md untracked (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.jsonl are live inputs of delever_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 consumes inflight.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 ps for make.
    • delever_oracle --snapshot-baseline only after a GREEN clean fleet run; --apply refuses a stale snapshot; restruct_cycle.sh refreshes snapshot + calibration itself when HEAD moved.
    • DECL-NONE is a DONE verdict: redraw with --redraw VERDICT:cause, never bare DECL-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*\(' src before 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=1 for header batches; REDRAW="<VERDICT>:<cause>" to redraw. Scratch for this phase: .run/P38/.
    • type_census --selftest (21 tests) and restruct --selftest (54; --real adds 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.
  • Long commands: one tool call stays under 285 s. Anything longer: bash tools/run.sh --bg <name> -- <cmd> then bash 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-structs that asserts it, and the closing expert runs that. --check itself 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: high on T3 only (1 of 7): the union-vs-split decision per overlapping offset (28+1 on the entity, 44 on Unkstruct_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 in struct_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-structs shows the head types gone from the dup classes.
  • T6 (declarations) after T5: the gen plan's "most of the 417 conflicting types fall to the canonical files" and the 1,120 parse error redraw only pay once every type is in a canonical file every TU includes. The 13 src/800.c syntax-error units and 45 TU-CONFLICT rows are the same class (declaration environment) and ride along.
  • docs/actor-struct.md fix goes in T2: the census control player_block_widths reads it and currently prints two doc-disputed rows; 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-structs with its current counts before any fold lands).
  • No ## Developer decides items: 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 (default OUT_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; prints type_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> env LABEL_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; imports build_engine_types (brace-aware find_defs, find_typedefs).
  • tools/delever_oracle.py (BFM): --snapshot-baseline flag :613 → snapshot_baseline() :218 (writes build/**/*.o + _link/<alias>/ + links.json to .run/P36/delever/baseline/, records HEAD; refuses a shrunken build/ :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 line tools-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 via restruct.py --status / --check-ledger).
  • New in T2: type_census.py --check-structs → prints type_census --check-structs: OK|FAIL — <violations> and one counts line dup_classes=… outside_canon=… dead=… variants=… pad_names=… parse_error_decls=… controls=a/b plus non-gating report conflicting_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.md S108 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-health last line tools-health: OK; make kit-corpus run and its outputs committed (tree clean of kit drift, config/kit_coverage_map.tsv either regenerated-and-committed or explained); R22 green; delever_oracle --snapshot-baseline then --calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16 OK; fresh type_census.py --out-dir .run/P38/census + restruct.py --audit-types baseline 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-health then --wait tools-health --max 280 repeated (>15 min); delete leftover src/shared/.cdecl_allmacros.c; ps for orphaned workers after. A red item is a product-tool defect → coder fixes, commit, re-run; three distinct red items → blocked with the list. Order: kit-corpus → tools-health → commit → R22 → snapshot-baseline → calibrate (snapshot only after the green clean run). Record df -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-structs exists per ## Interfaces; gates dup classes, outside-canon defs, dead names, variant names, legacy pad<SIZE> names (same rule as restruct --audit-types, imported not re-implemented), parse-error declarations (restruct ledger latest verdict DECL-KEPT cause type 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; --selftest gains ≥2 tests for the new mode and passes; docs/actor-struct.md +0x108/+0x10C and D_80078F08/0C corrected to 4-byte and the control line shows no doc-disputed rows | 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-structs read. Do not change --check semantics (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_80078E00 defined 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 into CANON_HEADERS; each layout proven on ≥2 known exemplar TUs per space via restruct --try/--try-file IDENTICAL (incl. one main and one md_* TU); func_801814AC known-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 with restruct.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); else unkHEX. Decide whether main (src/800_shared.h) and md_* 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-structs shows 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 with TASK=T4 LABEL_PREFIX=t38_4 … L, --only <type names>; shared headers with HEADERS=1 (serial). Onboard main and md_* in this task. delever_oracle --calibrate refresh 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 --apply and rung-L cycles; 40 variant camps uniquified; dead canonical names deleted; the 26 legacy pad<SIZE> names renamed to unkHEX/pad_<off> form; PsyQ layouts carry Sony's SDK names; layout twins without evidence left apart and listed; --check-structs gating 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 → --apply per type list; C0067's three laws and C0224 (typedef before every splice point). A tool defect in lift_types.py is 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) and DECL-KEPT:conflicting redrawn to exhaustion and the remainder bucketed; --check-structs parse_error_decls = 0; the 13 src/800.c syntax-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/$v0 and zero-arg $a0 callers, 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-structs exit 0 on a fresh census; R22 218/218 at the closing commit; type_census --selftest, restruct --selftest --real, lever_progress --check OK; docs/struct-map.md regenerated; cookbook §396(a) correction and the phase's generalizable gotchas added via tools/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-health red on a stale tool after P36/P37 edits → T1 stalls. Detection: its log's failing stage; fix as product-tool work in T1, blocked if three distinct causes.
  • --check-structs measures 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 blocked with 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: df in T1 and each cycle log; ENOSPC → stop, report; prune only .run/P32/ scratch, never .run/P36/delever/.
  • DECL-KEPT remainder 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: --apply refusal; 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