Files
BFM-decomp/tools/sunset/rtu_shadow.py
T
Drew T 827295e241 tools+docs(phase-33.5): task 13.5 — the tools audit + the two dictionaries: tools/tool_census.py (two agreeing enumerations of 327 tool files; docstring/SETUP row/consumers/class derived from the tree; the authored half in config/tool_dictionary.tsv — phase · portability · the NEED each tool answers · what · adapts · verdict — with coverage asserted both ways) → docs/tool-index.md (need-keyed, KEEP-GEN, Reference-index row, wiki + how-to pointers), the kit's tools/MANIFEST.md regenerated (header states live 293 + superseded 28 = 321 rows), and the two verbatim corpora in-tree (Drew, confirmed S91): decomp-architect/corpus/tools/<phase>/ (302 copies + 28 superseded pointers + INDEX) and corpus/cookbook/ (the cookbook, its symptom index, the codegen map, a front page stating what transfers per compiler) — sha1-equal to their sources by tool_census --check in tools-health, regenerated by make kit-corpus; kit_lint exempts the corpus dirs (verbatim evidence) but syntax-checks them; G66 (consult the tool dictionary first) + G67 (translate an inherited idiom through its pass) + two memory seeds (34 at install); SETUP Step 6 installs docs/knowledge-corpus.md and checks the manifest against its own stated total; the ops-setup dictionary rows; the intake's Phase 7 cites G66/G67 and Phase 10 + Part C name the raw-cast → declared-symbol step; templates/layout-contract.md (the five-tool probe, a draft for the split). The review under Drew's criterion: 93 no-consumer tools (one Opus agent's draft, verified: 0 defects, every successor live, 0 live consumers, 0 collisions; four one-off verdicts overturned to STILL-NEEDED) → 34 retired by git mv to tools/sunset/ (28 superseded, 6 one-offs; README review table; SETUP rows moved; Archive-index group). Run 4 (fresh throwaway, the final kit): stopped on my Step-6 check (321 vs the live 293) → both sides derived → resumed → PASS 10/10, manifest 56 == 56, 4 commits, guardrails held (the one foreign path was the timeline regenerated by the detached tools-health). tools-health OK; doc_links --strict rc 0; audit_public OK over 6,842 paths; the purge probe PASSED (Phase 34's gate open). decision-log "P33.5 S91" + accelerators "P33.5 S91" banked; log + checkpoint (NEXT = task 14, xHigh, fresh session)
2026-09-07 22:09:15 -06:00

226 lines
10 KiB
Python

#!/usr/bin/env python3
"""rtu_shadow.py — shadow-mode rtu_match beside the wave gate, to decide whether to INVERT the gate.
WHY (P31 S61, frontier-analysis-s60 §3). The gate ladder spends ~3 whole-binary builds per FAILING
draft (stage 0 raw, stage 1 after transforms, stage 2 after sig_unify) and ~1 per banking draft, so
at 1-6% conversion almost every build is spent confirming a failure. `rtu_match` compiles the real
split TU with the draft spliced in — no build tree, no locks, ~2 s, parallel-safe — and could be the
gate's stage 0, so that whole-binary builds are spent only on rtu-MATCH drafts (builds proportional
to BANKS, not drafts). That is a ~20x reduction at current conversion — IF rtu-MATCH predicts
banking and rtu-non-MATCH does not throw away banks. This tool measures exactly that, with zero
behavioural change to the gate (G3/P9 untouched: the SHA gate stays the sole arbiter).
PROSPECTIVE (run BEFORE the wave is gated — no peeking; and NOT while a gate is in flight, because
a gate transiently splices bodies into the very src/ TUs rtu reads):
tools/rtu_shadow.py --wave fa --wave fb -j 32
-> .run/rtu_shadow/<wave>.jsonl one row per draft: rtu verdict, ndiff, reloc pre-filter status
JOIN (run AFTER the gate committed):
tools/rtu_shadow.py --join fa --join fb
-> confusion matrix rtu-verdict x outcome, the false-negative rate (banks rtu would have refused),
and the build accounting (current ladder vs inverted), every figure with its denominator (R41).
Drafts are mapped to their binary through the SHARD'S targets file, never by bare function name — a
wave drafts siblings under the same name in several binaries (54 of wave fa's 470 cards).
"""
import argparse
import collections
import glob
import json
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import corpus # noqa: E402
import integration_resolver as ir # noqa: E402
REPO = ir.REPO
OUT = ".run/rtu_shadow"
def wave_drafts(w):
"""[(shard, card, draft_path)] with the card taken from that shard's own targets file."""
rows, unmapped, ambiguous = [], 0, 0
for tp in sorted(glob.glob(f".run/wave_{w}_targets.*.json"),
key=lambda p: int(p.rsplit(".", 2)[1])):
i = int(tp.rsplit(".", 2)[1])
try:
targets = json.load(open(tp))
except Exception:
continue
by_name = collections.defaultdict(list)
for c in targets:
by_name[c.get("name") or c.get("fn")].append(c)
for d in glob.glob(f".run/wave_{w}/shard{i}/*.c"):
fn = os.path.basename(d)[:-2]
cs = by_name.get(fn) or []
if not cs:
unmapped += 1
continue
if len(cs) > 1:
ambiguous += 1
continue
rows.append((i, cs[0], d))
return rows, unmapped, ambiguous
def reloc_status(w):
out = {}
if not os.path.exists(ir.REJECTS):
return out
for line in open(ir.REJECTS, errors="replace"):
try:
r = json.loads(line)
except Exception:
continue
if r.get("wave") == w and r.get("fn") and r.get("binary"):
out[(r["binary"], r["fn"])] = (r.get("status"), r.get("shape"))
return out
def shadow(w, jobs):
rows, unmapped, ambiguous = wave_drafts(w)
rl = reloc_status(w)
head = ir.sh(["git", "rev-parse", "--short", "HEAD"], 30).stdout.strip()
ir.say(f"wave {w}: {len(rows)} drafts mapped via shard targets (unmapped {unmapped}, ambiguous-name {ambiguous}); "
f"reloc pre-filter rows for this wave: {len(rl)}; HEAD {head}")
def one(item):
i, c, d = item
b, fn = c["binary"], c.get("name") or c.get("fn")
row = {"wave": w, "shard": i, "binary": b, "fn": fn, "draft": d, "draft_sha": ir.sha(d),
"nins_card": c.get("nins"), "reloc": rl.get((b, fn), ("AGREE?", None))[0],
"reloc_shape": rl.get((b, fn), (None, None))[1], "head": head,
"ts": time.strftime("%Y-%m-%d %H:%M:%S")}
rb = ir.real_binary(b)
if rb is None or rb == "main":
row.update(rtu="UNSUPPORTED", ndiff=None, nins=None, note="main / unknown binary")
return row
idx, err = ir.stub_index(rb)
if err:
row.update(rtu="HARNESS-CORPUS", ndiff=None, nins=None, note=err)
return row
stub = idx.get(fn)
if stub is None:
row.update(rtu="NOT-A-STUB", ndiff=None, nins=None, note="already banked")
return row
work = os.path.join(OUT, "work", w, f"{b}__{fn}")
v = ir.rtu(rb, fn, stub, d, work, corpus.is_o0(stub.path))
row.update(rtu=v["verdict"], ndiff=v.get("ndiff"), nins=v.get("nins"), note=v.get("note", "")[:160])
return row
t0 = time.time()
with ThreadPoolExecutor(max_workers=jobs) as ex:
res = list(ex.map(one, rows))
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, f"{w}.jsonl"), "w") as f:
for r in res:
f.write(json.dumps(r) + "\n")
c = collections.Counter(r["rtu"] for r in res)
ir.say(f"wave {w}: shadow verdicts in {time.time() - t0:.0f}s: " + " ".join(f"{k}={v}" for k, v in sorted(c.items())))
cg = collections.Counter(r["rtu"] for r in res if r["reloc"] == "AGREE?")
ir.say(f"wave {w}: among drafts the pre-filter will pass (no reject row): " + " ".join(f"{k}={v}" for k, v in sorted(cg.items())))
return res
def wave_commits(w):
out = []
for line in ir.sh(["git", "log", "--format=%h %s", "-600"], 60).stdout.splitlines():
h, _, s = line.partition(" ")
if re.search(r"ox wave %s(\b|$)" % re.escape(w), s):
out.append(h)
return out
def join(w):
p = os.path.join(OUT, f"{w}.jsonl")
if not os.path.exists(p):
ir.say(f"wave {w}: no shadow ledger at {p}")
return
rows = [json.loads(l) for l in open(p) if l.strip()]
# the ox ledger is the authority on whether the wave was GATED: a 0-bank wave has NO commit
# (ox_campaign.commit() returns None on a clean tree), so "no commit" must not read as "not gated"
led = [json.loads(l) for l in open(".run/ox_campaign_ledger.jsonl", errors="replace") if l.strip()]
gated_rows = [r for r in led if r.get("wave") == w and r.get("gated") is not None]
if not gated_rows:
ir.say(f"wave {w}: no gate row in the ox ledger — not gated yet")
return
commits = [r["commit"] for r in gated_rows if r.get("commit")] or wave_commits(w)
banked = set()
pat = re.compile(r'^-.*INCLUDE_ASM\("(asm/[^"]+)",\s*(\w+)\)')
for h in commits:
for line in ir.sh(["git", "show", "--unified=0", h, "--", "src/"], 120).stdout.splitlines():
m = pat.match(line)
if m:
parts = m.group(1).split("/")
banked.add((parts[1] if parts[1] != "nonmatchings" else "main", m.group(2)))
# RED binaries make every verdict about the BINARY, not the draft (S61 fleet audit): report them
# separately and exclude them from the prediction metrics (R41: say what the denominator is).
red = set()
try:
red = {l.strip() for l in open(".run/baseline_red.txt") if l.strip()}
except OSError:
pass
n_red = sum(1 for r in rows if r["binary"] in red)
if n_red:
ir.say(f"wave {w}: {n_red} of {len(rows)} shadowed drafts sit in {len(red & {r['binary'] for r in rows})} "
f"baseline-RED binaries — excluded from the prediction metrics below")
rows = [r for r in rows if r["binary"] not in red]
outcome = collections.Counter()
table = collections.Counter()
for r in rows:
k = (r["binary"], r["fn"])
if r["rtu"] == "NOT-A-STUB":
o = "ALREADY-BANKED"
elif k in banked:
o = "BANKED"
elif r["reloc"] != "AGREE?":
o = "PREFILTER-DROPPED"
else:
idx, err = ir.stub_index(r["binary"])
o = "GATE-REJECTED" if (idx and r["fn"] in idx) else "BANKED-ELSEWHERE"
outcome[o] += 1
table[(r["rtu"], o)] += 1
ir.say(f"wave {w}: gate commit(s) {commits}; outcomes: " + " ".join(f"{k}={v}" for k, v in sorted(outcome.items())))
print(" %-16s %-8s %-14s %-18s %-16s %s" % ("rtu verdict", "BANKED", "GATE-REJECTED", "PREFILTER-DROPPED", "BANKED-ELSEWHERE", "ALREADY"))
for v in sorted({r["rtu"] for r in rows}):
print(" %-16s %-8d %-14d %-18d %-16d %d" % (v, table[(v, "BANKED")], table[(v, "GATE-REJECTED")],
table[(v, "PREFILTER-DROPPED")], table[(v, "BANKED-ELSEWHERE")],
table[(v, "ALREADY-BANKED")]))
gated = [r for r in rows if r["reloc"] == "AGREE?" and r["rtu"] not in ("NOT-A-STUB", "UNSUPPORTED", "HARNESS-CORPUS")]
g_b = [r for r in gated if (r["binary"], r["fn"]) in banked]
g_r = [r for r in gated if (r["binary"], r["fn"]) not in banked]
m_b = sum(1 for r in g_b if r["rtu"] == "MATCH")
m_r = sum(1 for r in g_r if r["rtu"] == "MATCH")
n_m = m_b + m_r
print(f" gated drafts {len(gated)}: banked {len(g_b)}, rejected {len(g_r)}")
print(f" P(bank | rtu MATCH) = {m_b}/{n_m}" + (f" = {100.0 * m_b / n_m:.1f}%" if n_m else ""))
print(f" false-negative (banks rtu would refuse) = {len(g_b) - m_b}/{len(g_b)}"
+ (f" = {100.0 * (len(g_b) - m_b) / len(g_b):.1f}%" if g_b else ""))
cur = len(g_b) * 1 + len(g_r) * 3
inv = m_b * 1 + m_r * 3
print(f" whole-binary builds, current ladder ~ {cur} (1/bank + 3/reject) vs inverted ~ {inv} (rtu-MATCH only)"
+ (f" -> {cur / max(1, inv):.1f}x fewer" if inv else ""))
dropped_match = sum(1 for r in rows if r["reloc"] != "AGREE?" and r["rtu"] == "MATCH")
print(f" pre-filter-dropped drafts that rtu-MATCH the real TU (never gated): {dropped_match}")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--wave", action="append", default=[], help="prospective shadow over this wave's drafts")
ap.add_argument("--join", action="append", default=[], help="join a shadowed wave with its gate outcome")
ap.add_argument("-j", "--jobs", type=int, default=32)
a = ap.parse_args()
os.chdir(REPO)
for w in a.wave:
shadow(w, a.jobs)
for w in a.join:
join(w)
return 0
if __name__ == "__main__":
sys.exit(main())