From 06da215a66e4c840e083993a8dbc6e1fac76d4c3 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:40:49 -0600 Subject: [PATCH] T3.c2: rung S plan reads a pinned base->type (--bbt) and skips finished files --- decomp-architect/corpus/tools/P10/restruct.py | 81 +++++++++++++++++-- .../corpus/tools/P10/restruct_cycle.sh | 5 +- phase-ends/current/logs/T3.c2.md | 30 +++++++ phase-ends/current/logs/T3.md | 6 ++ tools/restruct.py | 81 +++++++++++++++++-- tools/restruct_cycle.sh | 5 +- 6 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 phase-ends/current/logs/T3.c2.md diff --git a/decomp-architect/corpus/tools/P10/restruct.py b/decomp-architect/corpus/tools/P10/restruct.py index 944de0d7ff..246903e554 100644 --- a/decomp-architect/corpus/tools/P10/restruct.py +++ b/decomp-architect/corpus/tools/P10/restruct.py @@ -3245,11 +3245,39 @@ def fold_report(heads): # ---------------------------------------------------------------------------------------------------------------------- # planning and --apply (the batch: files in leverage order, workers own whole TUs, headers serial and last) # ---------------------------------------------------------------------------------------------------------------------- +BBT_PIN = os.environ.get("RESTRUCT_BBT") or None # P3.39 T3.c2: a pinned body_base_type.json (--bbt / RESTRUCT_BBT) + + +def merge_bbt(cur, pin, can): + """cur with each base whose type is NOT canonical replaced by the pinned entry for the same (body, base) when that one is canonical. + T3.c2: the cycle's per-batch census rewrites body_base_type.json and the map's names drift as sites become members (after t3_S5 the + ov bases' top type was Unkstruct_800D3888, not in a canonical header: 0 of 103469 ov bases canonical, plan_S drew 0 files).""" + out, n = {}, 0 + for key, bases in cur.items(): + pb = pin.get(key, {}) + nb = {} + for bk, ent in bases.items(): + pe = pb.get(bk) + if ent["type"] not in can and pe and pe["type"] in can: + nb[bk], n = pe, n + 1 + else: + nb[bk] = ent + out[key] = nb + return out, n + + def bbt(): p = CENSUS / "body_base_type.json" if not p.exists(): sys.exit("restruct: no body_base_type.json — run `tools/type_census.py -j 16 --sites` first (R43)") - return json.loads(p.read_text()) + cur = json.loads(p.read_text()) + if BBT_PIN: + pp = pathlib.Path(BBT_PIN) + pp = pp if pp.is_absolute() else REPO / pp + if not pp.exists(): + sys.exit(f"restruct: the pinned body_base_type ({BBT_PIN}) does not exist") + cur, _ = merge_bbt(cur, json.loads(pp.read_text()), canon_types()) + return cur def matches_only(only, tu, unit, extra=()): @@ -3263,14 +3291,28 @@ def matches_only(only, tu, unit, extra=()): or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only for h in hay if h) -def plan_S(headers, batch, only, done): - """[(tu, {fn: bases})] — files with typed bases whose type is a CANONICAL definition (visible), ordered by the map's leverage.""" +def s_finished(tu, fns, latest): + """True when every body in fns has a latest ("S", tu, fn) ledger row whose nhash_after is the body's CURRENT nhash (T3.c2: t3_S1–S5 + re-drew finished files — drawable stayed ~2088 after 250 files done).""" + if not all((latest.get(("S", tu, fn)) or {}).get("nhash_after") for fn in fns): + return False + path = REPO / tu + if not path.exists(): + return False + masked, defs, _, _, _ = file_view(tu, path.read_text(errors="surrogateescape")) + cur = {d["name"]: body_nhash(masked, d) for d in defs if d["name"] in fns} + return all(cur.get(fn) == latest[("S", tu, fn)]["nhash_after"] for fn in fns) + + +def plan_S(headers, batch, only, done, latest=None, data=None): + """[(tu, {fn: bases})] — files with typed bases whose type is a CANONICAL definition (visible), ordered by the map's leverage. + latest = ledger_latest(rows): files whose every body is finished at its current text (s_finished) are not drawn; data: a bbt() dict (selftest).""" smap = struct_map() can = canon_types() files = collections.defaultdict(dict) weight = collections.Counter() skipped = collections.Counter() - for key, bases in bbt().items(): + for key, bases in (bbt() if data is None else data).items(): tu, fn = key.split("|", 1) if tu.endswith(".h") != headers: continue @@ -3288,6 +3330,10 @@ def plan_S(headers, batch, only, done): weight[tu] += smap.get(ent["type"], {}).get("sites", 0) if usable: files[tu][fn] = usable + if latest: + for tu in [t for t in files if s_finished(t, set(files[t]), latest)]: + del files[tu] + skipped["file-finished"] += 1 ordered = sorted(files, key=lambda t: (-weight[t], t)) return [(tu, files[tu]) for tu in ordered[:batch]], len(ordered), skipped @@ -3520,7 +3566,7 @@ def apply_batch(a): log_f.close() return 0 if v == "IDENTICAL" else 1 if a.rung == "S": - plan, total, skipped = plan_S(a.headers, a.batch, a.only, done) + plan, total, skipped = plan_S(a.headers, a.batch, a.only, done, ledger_latest(rows)) print(describe_plan("S", plan, total, skipped), flush=True) elif a.rung == "D": if a.redraw: @@ -4706,6 +4752,23 @@ def selftest(real=False): ok("rung D supersede: only lifted latest rows with no lying declaration left (.c only; cause key; latest row)", [(r["tu"], r["unit"]) for r in st_] == [("src/a.c", "decl:f1")] and sup_[0]["verdict"] == "DECL-NONE" and "stale DECL-KEPT" in sup_[0]["cause"] and stale_lifted(sl_led, [], ["DECL-KEPT:conflicting"], True, None)[0]["tu"] == "src/c.h", f"{st_}") + # T3.c2: the pinned base→type fallback, and finished files left out of the plan + key_ = f"{tu}|fx_a" + cur_ = {key_: {"param:a0": dict(type="Unkstruct_DRIFT", sites=12), "param:a1": dict(type="Unkstruct_DRIFT2", sites=1)}} + pin_ = {key_: {"param:a0": dict(type="Unkstruct_TEST", sites=12), "param:a1": dict(type="Unkstruct_GONE", sites=1)}} + mg_, nfb_ = merge_bbt(cur_, pin_, _canon) + ok("bbt pin: a drifted non-canonical type falls back to the pinned canonical one; a non-canonical pin is not taken", + nfb_ == 1 and mg_[key_]["param:a0"]["type"] == "Unkstruct_TEST" and mg_[key_]["param:a1"]["type"] == "Unkstruct_DRIFT2", f"{mg_}") + ok("plan_S: drifted bbt draws nothing; the pinned merge draws the file", + plan_S(False, 50, None, set(), {}, cur_)[1] == 0 and plan_S(False, 50, None, set(), {}, mg_)[1] == 1) + m_, defs_, _, _, _ = file_view(tu, text) + nh_ = body_nhash(m_, next(x for x in defs_ if x["name"] == "fx_a")) + fin_ = ledger_latest([dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after=nh_)]) + old_ = ledger_latest([dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after=nh_), + dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after="stale")]) + p_fin, p_old = plan_S(False, 50, None, set(), fin_, mg_), plan_S(False, 50, None, set(), old_, mg_) + ok("plan_S: a file whose bodies' latest S rows are at the current nhash is not drawn; a stale latest hash draws it", + p_fin[1] == 0 and p_fin[2]["file-finished"] == 1 and p_old[1] == 1, f"{p_fin[1:]} {p_old[1:]}") ok("the selftest never touches the live inflight.json", True) ok("struct_layout selftest", sl.selftest() == 0) JUDGE_STUB = None @@ -4774,8 +4837,11 @@ def main(): ap.add_argument("--rerun-differs", action="store_true", help="re-run only the DIFFERS rows of the last probe (with S2) and re-render") ap.add_argument("--probe-decls", action="store_true", help="probe (ii): the definition's signature as the in-scope declaration, per TU x callee") ap.add_argument("--probe-defs", action="store_true", help="probe (iii): fold a TU's duplicate file-scope definitions onto the canonical header") + ap.add_argument("--bbt", help="a pinned body_base_type.json: a base whose current type is not canonical falls back to its pinned entry (env RESTRUCT_BBT)") ap.add_argument("-j", "--jobs", type=int, default=16) a = ap.parse_args() + global BBT_PIN + BBT_PIN = a.bbt or BBT_PIN if a.selftest: sys.exit(selftest(real=a.real)) if a.restore: @@ -4819,9 +4885,10 @@ def main(): return if a.plan: recipes() - done, _ = ledger_index(load_ledger()) + led = load_ledger() + done, _ = ledger_index(led) if a.rung == "S": - plan, total, skipped = plan_S(a.headers, a.batch, a.only, done) + plan, total, skipped = plan_S(a.headers, a.batch, a.only, done, ledger_latest(led)) print(describe_plan("S", plan, total, skipped)) elif a.rung == "D": if a.redraw: diff --git a/decomp-architect/corpus/tools/P10/restruct_cycle.sh b/decomp-architect/corpus/tools/P10/restruct_cycle.sh index 0e0ca103b2..8bcaf7c43a 100644 --- a/decomp-architect/corpus/tools/P10/restruct_cycle.sh +++ b/decomp-architect/corpus/tools/P10/restruct_cycle.sh @@ -11,7 +11,7 @@ # still in place: inspect, then `tools/restruct.py --restore` (never git checkout, R102). Stops cleanly when nothing is drawable. # With CENSUS=0 too: a final X/Y with X != Y exits 1 BEFORE R22/commit and the tree stays exactly as restruct left it (no auto-revert) — # the OPERATOR must restore it (`tools/restruct.py --restore`, or revert the named files) before the next cycle (T4.c2). -# Usage: [LABEL_PREFIX=t4_] [TASK=T4] [REDRAW="DECL-KEPT"] [CENSUS=0] tools/restruct_cycle.sh START END [BATCH=50] [RUNG=D|S|L] [ONLY="alias1 alias2 …"] [HEADERS=1] +# Usage: [LABEL_PREFIX=t4_] [TASK=T4] [REDRAW="DECL-KEPT"] [CENSUS=0] [RESTRUCT_BBT=.run/P39/census/body_base_type.json] tools/restruct_cycle.sh START END [BATCH=50] [RUNG=D|S|L] [ONLY="alias1 alias2 …"] [HEADERS=1] # RUN IT DETACHED from the Claude Code harness (R115 — its low-memory guard kills a long BACKGROUND task; a SIGKILL skips a restore): # setsid nohup bash -c 'TASK=T4 tools/restruct_cycle.sh 1 8 50 D; echo "cycle exit=$?"' > .run/P37/restruct/cycle_.log 2>&1 & # and watch the log (`until grep -q 'cycle exit=' ; do sleep 60; done`). A killed batch: `tools/restruct.py --restore`. @@ -25,6 +25,7 @@ case "$RUNG" in D|S|L) ;; *) echo "RUNG must be D, S or L"; exit 2;; esac ONLYFLAG=""; [ -n "$ONLY" ] && ONLYFLAG="--only $ONLY" REDRAWFLAG=""; [ -n "${REDRAW:-}" ] && REDRAWFLAG="--redraw $REDRAW" HFLAG=""; [ -n "${HEADERS:-}" ] && HFLAG="--headers" +BBTFLAG=""; [ -n "${RESTRUCT_BBT:-}" ] && BBTFLAG="--bbt $RESTRUCT_BBT" # T3.c2: a pinned body_base_type.json (the census rewrites the live one) FLEET=$(ls config/check.*.sha | wc -l) PY=.venv/bin/python MARKER='## 🛑 SESSION CHECKPOINT' @@ -41,7 +42,7 @@ for k in $(seq "$START" "$END"); do $PY tools/delever_oracle.py --calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16 > .run/P37/restruct/calibrate_${label}.log 2>&1 \ || { echo "cycle: calibration FAILED"; tail -5 .run/P37/restruct/calibrate_${label}.log; exit 1; } } - { /usr/bin/time -f "$label wall=%e s" $PY tools/restruct.py --apply --rung "$RUNG" --batch "$BATCH" --label "$label" $HFLAG $ONLYFLAG $REDRAWFLAG -j 12; echo "exit=$?"; } > "$log" 2>&1 + { /usr/bin/time -f "$label wall=%e s" $PY tools/restruct.py --apply --rung "$RUNG" --batch "$BATCH" --label "$label" $HFLAG $ONLYFLAG $REDRAWFLAG $BBTFLAG -j 12; echo "exit=$?"; } > "$log" 2>&1 grep -q '^exit=0$' "$log" || { echo "cycle: batch $label — restruct exited non-zero"; tail -4 "$log"; exit 1; } if grep -qE 'nothing to do' "$log"; then echo "cycle: nothing drawable at $label — done"; exit 0; fi vline=$(grep -oE "restruct: batch .*" "$log" | tail -1) diff --git a/phase-ends/current/logs/T3.c2.md b/phase-ends/current/logs/T3.c2.md new file mode 100644 index 0000000000..9cffcac577 --- /dev/null +++ b/phase-ends/current/logs/T3.c2.md @@ -0,0 +1,30 @@ +# T3.c2 — rung S plan: pinned base→type + finished files out + +## Cause (confirmed) +- Hypothesis held. `.run/P39/t3c2/cmp_bbt.py` (scratch) over ov bases (non-.h): + - pinned `.run/P39/census/body_base_type.json` (T1, 10:25): 116484 bases, 31408 canonical; top `Unkstruct_80126B58`, `Unkstruct_800B5CB8`. + - current `.run/P37/census/body_base_type.json` (14:18, after t3_S5 census): 103469 bases, **0 canonical**; top `Unkstruct_800D3888` (22682). + - untouched file `src/ov_MAIN_012/ov_MAIN_012.c|func_8012E9C0` (no t3_ S row): `param:param_1` pinned `Unkstruct_800B5CB8` (canon) → current `Unkstruct_800D3888` (not canon). +- So the per-batch `type_census --sites` renames types in body_base_type.json; plan_S's canonical filter drops every base → t3_S6 `0 of 0`. +- Drawable never fell: plan_S had no finished-file exclusion (work_file_S skips done bodies but the file is still drawn). + +## Change +- `tools/restruct.py`: + - `BBT_PIN` (env `RESTRUCT_BBT`, CLI `--bbt PATH`); `merge_bbt(cur, pin, can)`: per (body, base), a non-canonical current type falls back to the pinned entry when that is canonical; keys come from current only. `bbt()` applies it when pinned (exits if the pin path is missing). + - `s_finished(tu, fns, latest)`: every body's latest ("S",tu,fn) row has `nhash_after` == current `body_nhash` (file parsed only when every body has a latest S row with a hash). Any verdict counts (latest row = verdict, R70); TOOL-ERROR rows (nhash_after None) keep the file drawable. + - `plan_S(..., latest=None, data=None)`: drops finished files, counts `skipped["file-finished"]`; both callers (`--plan`, `--apply`) pass `ledger_latest(rows)`. + - selftest +3 controls: merge fallback (canonical pin taken, non-canonical pin not); drifted bbt draws 0 / merged draws 1; finished at current nhash → not drawn, stale latest hash → drawn. +- `tools/restruct_cycle.sh`: `RESTRUCT_BBT=… ` → `--bbt` on the apply line; usage line documents it. + +## Verified +- `.venv/bin/python tools/restruct.py --selftest` → 88/88 OK; `/usr/bin/python3 tools/restruct.py --selftest` → 88/88 OK. +- `make kit-corpus` → corpus materialised (409 copies); `make tools-health` → OK. +- `restruct.py --plan --rung S --only "ov_*"` (no pin) → `0 of 0 drawable files … skipped bases {'type-not-canonical': 103469}` (.run/logs/t3c2_plan_nopin.log). +- same with `--bbt .run/P39/census/body_base_type.json` → `50 of 1901 drawable files · 340 bodies · 397 typed bases · 2063 deref sites · skipped {'type-not-canonical': 83385, 'file-finished': 187}` (.run/logs/t3c2_plan_pin.log). +- full counts (`.run/P39/t3c2/plan_counts.py`, batch unlimited, pinned): no finish filter 2088 files / 17165 bodies; with filter 1901 files / 14621 bodies (187 finished files out). + +## Expert notes +- The pin `.run/P39/census/body_base_type.json` is gitignored and untracked (11.5 MB); nothing in tools/ writes it. Losing it loses the fix's input: copy or `git add -f` if wanted. +- 225 TUs carry t3_ S rows; 187 count as finished; the rest have a body with no / stale / hashless latest row (e.g. TOOL-ERROR) or a body judged by an earlier label at an older hash. +- Bases new since the pin (not in it) stay non-canonical: newly typed bodies are not drawn until a canonical census is pinned again. +- Sweep resume: `RESTRUCT_BBT=.run/P39/census/body_base_type.json LABEL_PREFIX=t3_ TASK=T3 tools/restruct_cycle.sh 6..60 50 S "ov_*"` (not run). diff --git a/phase-ends/current/logs/T3.md b/phase-ends/current/logs/T3.md index 5e4b7985f9..c66607c48a 100644 --- a/phase-ends/current/logs/T3.md +++ b/phase-ends/current/logs/T3.md @@ -7,3 +7,9 @@ - retriever-code (inline, no report): rung S mechanics. Unit = file; plan_S :3119 from CENSUS/body_base_type.json; S1 MEMBERS / S2 greedy_kept :753 / KEPT-ALL / REFUSED; site verdict `KEPT()` (one hint per body, :1117-1122); kept sites are left raw (no respell); no-field skips never add fields; `--apply` allow_local=False; no code writes macro spellings. - restruct_cycle.sh read: per batch calibrate-if-stale → apply -j 12 → R22 → census → commit; S body ≈ 2 s. - T3.c1 briefed (opus55): per-site class, KEPT() / RESIDUAL(), CAST_* respell of kept P sites, `--only` glob, calibrate, exemplar ov_SC05_010_jr_80180F84.c. +- T3.c1 done (3ff15ee462, 3507628e40, fe2e814776): selftest 85/85, --real 90/90, exemplar apply 1/1 IDENTICAL 25.5 s, R22 218/218, check-casts macros CAST_ALIAS=3 CAST_SIGN=10 CAST_WIDTH=6 unbacked 0. Deviation (coder): BACKED_S_VERDICTS += MEMBERS, S+A; new body verdict KEPT. Exemplar +0x34 store = RESIDUAL(TYPE-NOT-CANONICAL) in real apply (KEPT(SCHED-ALIAS) only under try-file local types). +- cycle batch 1 (`LABEL_PREFIX=t3_ TASK=T3 restruct_cycle.sh 1 1 50 S "ov_*"`, log .run/P39/restruct/cycle_t3_a.log): 08b5b03dd0, 50 files/2088 drawable, 3803 bodies, 24546 members / 2835 kept / 9346 skipped, 1707 s apply, R22 218/218 112 s. Batch kept at 50 files though > 10 min: detached (R115), heaviest first; deviation. +- cycle 2..60 launched (.run/P39/restruct/cycle_t3_b.log). +- retriever-code (inline): --write-types is preview-only; canonical = struct defined in engine_types.h / struct_types.h; no-field = no leaf at exact off; head_types.py regenerates only its marked region; no tool adds fields to hand-authored structs. +- cycle 2..60 (cycle_t3_b.log): t3_S2..S5 green (R22 218/218 each, last 69f9da8c0f); members 12384/7786/7878/6694; then t3_S6 `0 of 0 drawable … type-not-canonical 103469` → cycle exit 0 "nothing drawable". Drawable count never fell (2088→2089). Probe: current body_base_type.json 109,359 bases, top type Unkstruct_800D3888 (absent from canonical headers). Hypothesis: census rerun renames struct-map types as sites become members; plan reads drifting map; plan also re-draws finished files. +- T3.c2 briefed: confirm, pin base→type (pre-sweep .run/P39/census/body_base_type.json), exclude finished files. diff --git a/tools/restruct.py b/tools/restruct.py index 944de0d7ff..246903e554 100644 --- a/tools/restruct.py +++ b/tools/restruct.py @@ -3245,11 +3245,39 @@ def fold_report(heads): # ---------------------------------------------------------------------------------------------------------------------- # planning and --apply (the batch: files in leverage order, workers own whole TUs, headers serial and last) # ---------------------------------------------------------------------------------------------------------------------- +BBT_PIN = os.environ.get("RESTRUCT_BBT") or None # P3.39 T3.c2: a pinned body_base_type.json (--bbt / RESTRUCT_BBT) + + +def merge_bbt(cur, pin, can): + """cur with each base whose type is NOT canonical replaced by the pinned entry for the same (body, base) when that one is canonical. + T3.c2: the cycle's per-batch census rewrites body_base_type.json and the map's names drift as sites become members (after t3_S5 the + ov bases' top type was Unkstruct_800D3888, not in a canonical header: 0 of 103469 ov bases canonical, plan_S drew 0 files).""" + out, n = {}, 0 + for key, bases in cur.items(): + pb = pin.get(key, {}) + nb = {} + for bk, ent in bases.items(): + pe = pb.get(bk) + if ent["type"] not in can and pe and pe["type"] in can: + nb[bk], n = pe, n + 1 + else: + nb[bk] = ent + out[key] = nb + return out, n + + def bbt(): p = CENSUS / "body_base_type.json" if not p.exists(): sys.exit("restruct: no body_base_type.json — run `tools/type_census.py -j 16 --sites` first (R43)") - return json.loads(p.read_text()) + cur = json.loads(p.read_text()) + if BBT_PIN: + pp = pathlib.Path(BBT_PIN) + pp = pp if pp.is_absolute() else REPO / pp + if not pp.exists(): + sys.exit(f"restruct: the pinned body_base_type ({BBT_PIN}) does not exist") + cur, _ = merge_bbt(cur, json.loads(pp.read_text()), canon_types()) + return cur def matches_only(only, tu, unit, extra=()): @@ -3263,14 +3291,28 @@ def matches_only(only, tu, unit, extra=()): or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only for h in hay if h) -def plan_S(headers, batch, only, done): - """[(tu, {fn: bases})] — files with typed bases whose type is a CANONICAL definition (visible), ordered by the map's leverage.""" +def s_finished(tu, fns, latest): + """True when every body in fns has a latest ("S", tu, fn) ledger row whose nhash_after is the body's CURRENT nhash (T3.c2: t3_S1–S5 + re-drew finished files — drawable stayed ~2088 after 250 files done).""" + if not all((latest.get(("S", tu, fn)) or {}).get("nhash_after") for fn in fns): + return False + path = REPO / tu + if not path.exists(): + return False + masked, defs, _, _, _ = file_view(tu, path.read_text(errors="surrogateescape")) + cur = {d["name"]: body_nhash(masked, d) for d in defs if d["name"] in fns} + return all(cur.get(fn) == latest[("S", tu, fn)]["nhash_after"] for fn in fns) + + +def plan_S(headers, batch, only, done, latest=None, data=None): + """[(tu, {fn: bases})] — files with typed bases whose type is a CANONICAL definition (visible), ordered by the map's leverage. + latest = ledger_latest(rows): files whose every body is finished at its current text (s_finished) are not drawn; data: a bbt() dict (selftest).""" smap = struct_map() can = canon_types() files = collections.defaultdict(dict) weight = collections.Counter() skipped = collections.Counter() - for key, bases in bbt().items(): + for key, bases in (bbt() if data is None else data).items(): tu, fn = key.split("|", 1) if tu.endswith(".h") != headers: continue @@ -3288,6 +3330,10 @@ def plan_S(headers, batch, only, done): weight[tu] += smap.get(ent["type"], {}).get("sites", 0) if usable: files[tu][fn] = usable + if latest: + for tu in [t for t in files if s_finished(t, set(files[t]), latest)]: + del files[tu] + skipped["file-finished"] += 1 ordered = sorted(files, key=lambda t: (-weight[t], t)) return [(tu, files[tu]) for tu in ordered[:batch]], len(ordered), skipped @@ -3520,7 +3566,7 @@ def apply_batch(a): log_f.close() return 0 if v == "IDENTICAL" else 1 if a.rung == "S": - plan, total, skipped = plan_S(a.headers, a.batch, a.only, done) + plan, total, skipped = plan_S(a.headers, a.batch, a.only, done, ledger_latest(rows)) print(describe_plan("S", plan, total, skipped), flush=True) elif a.rung == "D": if a.redraw: @@ -4706,6 +4752,23 @@ def selftest(real=False): ok("rung D supersede: only lifted latest rows with no lying declaration left (.c only; cause key; latest row)", [(r["tu"], r["unit"]) for r in st_] == [("src/a.c", "decl:f1")] and sup_[0]["verdict"] == "DECL-NONE" and "stale DECL-KEPT" in sup_[0]["cause"] and stale_lifted(sl_led, [], ["DECL-KEPT:conflicting"], True, None)[0]["tu"] == "src/c.h", f"{st_}") + # T3.c2: the pinned base→type fallback, and finished files left out of the plan + key_ = f"{tu}|fx_a" + cur_ = {key_: {"param:a0": dict(type="Unkstruct_DRIFT", sites=12), "param:a1": dict(type="Unkstruct_DRIFT2", sites=1)}} + pin_ = {key_: {"param:a0": dict(type="Unkstruct_TEST", sites=12), "param:a1": dict(type="Unkstruct_GONE", sites=1)}} + mg_, nfb_ = merge_bbt(cur_, pin_, _canon) + ok("bbt pin: a drifted non-canonical type falls back to the pinned canonical one; a non-canonical pin is not taken", + nfb_ == 1 and mg_[key_]["param:a0"]["type"] == "Unkstruct_TEST" and mg_[key_]["param:a1"]["type"] == "Unkstruct_DRIFT2", f"{mg_}") + ok("plan_S: drifted bbt draws nothing; the pinned merge draws the file", + plan_S(False, 50, None, set(), {}, cur_)[1] == 0 and plan_S(False, 50, None, set(), {}, mg_)[1] == 1) + m_, defs_, _, _, _ = file_view(tu, text) + nh_ = body_nhash(m_, next(x for x in defs_ if x["name"] == "fx_a")) + fin_ = ledger_latest([dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after=nh_)]) + old_ = ledger_latest([dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after=nh_), + dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after="stale")]) + p_fin, p_old = plan_S(False, 50, None, set(), fin_, mg_), plan_S(False, 50, None, set(), old_, mg_) + ok("plan_S: a file whose bodies' latest S rows are at the current nhash is not drawn; a stale latest hash draws it", + p_fin[1] == 0 and p_fin[2]["file-finished"] == 1 and p_old[1] == 1, f"{p_fin[1:]} {p_old[1:]}") ok("the selftest never touches the live inflight.json", True) ok("struct_layout selftest", sl.selftest() == 0) JUDGE_STUB = None @@ -4774,8 +4837,11 @@ def main(): ap.add_argument("--rerun-differs", action="store_true", help="re-run only the DIFFERS rows of the last probe (with S2) and re-render") ap.add_argument("--probe-decls", action="store_true", help="probe (ii): the definition's signature as the in-scope declaration, per TU x callee") ap.add_argument("--probe-defs", action="store_true", help="probe (iii): fold a TU's duplicate file-scope definitions onto the canonical header") + ap.add_argument("--bbt", help="a pinned body_base_type.json: a base whose current type is not canonical falls back to its pinned entry (env RESTRUCT_BBT)") ap.add_argument("-j", "--jobs", type=int, default=16) a = ap.parse_args() + global BBT_PIN + BBT_PIN = a.bbt or BBT_PIN if a.selftest: sys.exit(selftest(real=a.real)) if a.restore: @@ -4819,9 +4885,10 @@ def main(): return if a.plan: recipes() - done, _ = ledger_index(load_ledger()) + led = load_ledger() + done, _ = ledger_index(led) if a.rung == "S": - plan, total, skipped = plan_S(a.headers, a.batch, a.only, done) + plan, total, skipped = plan_S(a.headers, a.batch, a.only, done, ledger_latest(led)) print(describe_plan("S", plan, total, skipped)) elif a.rung == "D": if a.redraw: diff --git a/tools/restruct_cycle.sh b/tools/restruct_cycle.sh index 0e0ca103b2..8bcaf7c43a 100644 --- a/tools/restruct_cycle.sh +++ b/tools/restruct_cycle.sh @@ -11,7 +11,7 @@ # still in place: inspect, then `tools/restruct.py --restore` (never git checkout, R102). Stops cleanly when nothing is drawable. # With CENSUS=0 too: a final X/Y with X != Y exits 1 BEFORE R22/commit and the tree stays exactly as restruct left it (no auto-revert) — # the OPERATOR must restore it (`tools/restruct.py --restore`, or revert the named files) before the next cycle (T4.c2). -# Usage: [LABEL_PREFIX=t4_] [TASK=T4] [REDRAW="DECL-KEPT"] [CENSUS=0] tools/restruct_cycle.sh START END [BATCH=50] [RUNG=D|S|L] [ONLY="alias1 alias2 …"] [HEADERS=1] +# Usage: [LABEL_PREFIX=t4_] [TASK=T4] [REDRAW="DECL-KEPT"] [CENSUS=0] [RESTRUCT_BBT=.run/P39/census/body_base_type.json] tools/restruct_cycle.sh START END [BATCH=50] [RUNG=D|S|L] [ONLY="alias1 alias2 …"] [HEADERS=1] # RUN IT DETACHED from the Claude Code harness (R115 — its low-memory guard kills a long BACKGROUND task; a SIGKILL skips a restore): # setsid nohup bash -c 'TASK=T4 tools/restruct_cycle.sh 1 8 50 D; echo "cycle exit=$?"' > .run/P37/restruct/cycle_.log 2>&1 & # and watch the log (`until grep -q 'cycle exit=' ; do sleep 60; done`). A killed batch: `tools/restruct.py --restore`. @@ -25,6 +25,7 @@ case "$RUNG" in D|S|L) ;; *) echo "RUNG must be D, S or L"; exit 2;; esac ONLYFLAG=""; [ -n "$ONLY" ] && ONLYFLAG="--only $ONLY" REDRAWFLAG=""; [ -n "${REDRAW:-}" ] && REDRAWFLAG="--redraw $REDRAW" HFLAG=""; [ -n "${HEADERS:-}" ] && HFLAG="--headers" +BBTFLAG=""; [ -n "${RESTRUCT_BBT:-}" ] && BBTFLAG="--bbt $RESTRUCT_BBT" # T3.c2: a pinned body_base_type.json (the census rewrites the live one) FLEET=$(ls config/check.*.sha | wc -l) PY=.venv/bin/python MARKER='## 🛑 SESSION CHECKPOINT' @@ -41,7 +42,7 @@ for k in $(seq "$START" "$END"); do $PY tools/delever_oracle.py --calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16 > .run/P37/restruct/calibrate_${label}.log 2>&1 \ || { echo "cycle: calibration FAILED"; tail -5 .run/P37/restruct/calibrate_${label}.log; exit 1; } } - { /usr/bin/time -f "$label wall=%e s" $PY tools/restruct.py --apply --rung "$RUNG" --batch "$BATCH" --label "$label" $HFLAG $ONLYFLAG $REDRAWFLAG -j 12; echo "exit=$?"; } > "$log" 2>&1 + { /usr/bin/time -f "$label wall=%e s" $PY tools/restruct.py --apply --rung "$RUNG" --batch "$BATCH" --label "$label" $HFLAG $ONLYFLAG $REDRAWFLAG $BBTFLAG -j 12; echo "exit=$?"; } > "$log" 2>&1 grep -q '^exit=0$' "$log" || { echo "cycle: batch $label — restruct exited non-zero"; tail -4 "$log"; exit 1; } if grep -qE 'nothing to do' "$log"; then echo "cycle: nothing drawable at $label — done"; exit 0; fi vline=$(grep -oE "restruct: batch .*" "$log" | tail -1)