Files
BFM-decomp/tools/autopsy.py
T
Drew T f72e2344a6 fix(phase-29): T54 — correct the ADDRESSING route, and fix the reason changing it was inert
Item 1 off T53's list. Two changes: the route, and the design flaw underneath it.

THE DEFECT UNDER THE DEFECT (cookbook §106). residual_class answers two questions in one pass:
`klass` is a MEASUREMENT (expensive, from comparing instruction streams); `(profile, bucket)` is a
POLICY (a table lookup over it). autopsy persisted BOTH and verdicts() read BOTH back — so editing
_ROUTE changed nothing until someone re-ran the whole collect, and a weeks-old row could silently
out-vote the live table with no oracle to report it. The corpus on disk is dated Jul 21 and does not
even contain the SESSION-23 targets the recommendation cited.

Fixed by re-deriving at read time: residual_class.route_for(klass, detail), called from
autopsy.verdicts(). R33 — persist the measurement, derive the decision. Subtlety: LENGTH-DRIFT's
route is MAGNITUDE-dependent (permuter only when |delta|<=2 AND explains=="tail", §60b), so a naive
re-derivation from klass alone would have silently demoted 9 rows; both inputs are already in
`detail`, so the override reproduces exactly — VERIFIED 1610/1610 against the stored corpus with the
table UNCHANGED, before touching it.

THE ROUTE CHANGE: ADDRESSING ("cse","permuter") -> ("cse","structural"). It contradicted this file's
own bucket definition ("structural — local mutation CANNOT introduce it ... it wants a C-level
idiom"): the §10/§20 hoist-vs-remat shape is a multi-instruction change with a documented recipe
(gcc-2.7.2-map/cse_expr.md §2, byte-proven on func_80149374/func_801493D0). Measured (T31): both
admitted ADDRESSING targets plateaued under a §31-directed permuter, and the class was 32% of the
admission pool. After: pool 56 -> 38, exactly 18 rows changed, ALL ADDRESSING, nothing else moved;
grinder admits 45, structural skips 512 -> 530.

THE BOUND (R14), kept in the _ROUTE comment: I read the T31 record instead of the summary line, and
the summary was looser than the evidence. T31 finding 4 byte-tested the §2 recipe on func_80132F40
across six variants and it never closed (best 40 mismatches). `structural` does NOT promise a free
fix — it means "a search over local mutations is the wrong tool, try the documented idiom", exactly
what WIDTH / BRANCH-POLARITY / IMM-OFFSET already mean. Also corrected: the checkpoint cited
func_80176734 as the flat-for-32-min evidence, but that function is not in the corpus at all.

Tooling-only: no src/ or config/ change, no bank, no metric move.
2026-07-28 19:34:00 -06:00

340 lines
17 KiB
Python

#!/usr/bin/env python3
"""autopsy.py — the permuter-failure autopsy: mine the search-closer's FAILURES (Phase-29 Task-13).
docs/hindsight-study.md §7. Our flywheel (R16) has always evolved the offline tooling REACTIVELY
FROM SUCCESSES: a human or an LLM cracks a function, we distil the idiom into the cookbook and the
permuter weights. The permuter's FAILURES — every near-miss it plateaued on — are a larger and
completely unused signal. This tool turns that log into a corpus, classifies it deterministically,
and routes each cluster to one of:
missing-transform the byte-match IS reachable by local mutation but the permuter's mutation set
never tried the needed move -> extend permuter_weights. A PERMANENT offline
win: that whole class now closes with no LLM. The highest-value bucket.
mis-classified the recorded class is wrong, so the search was directed at the wrong levers
-> fix the routing (free).
seed-structural the C STRUCTURE is wrong; local mutation cannot introduce it -> one LLM seed
rewrite, then hand back to the permuter. The LLM spent once, minimally.
genuine-wall compiler-internal at this tier -> file WITH A REASON and an expiry (R35: the
verdict dies when the gate or the model tier changes). Never a silent give-up.
WHY A COLLECT STEP EXISTS AT ALL. Task-12 plumbed structured-residual telemetry into the backlog,
but telemetry only fills records written AFTER it landed: 1 of 6,169 records carries a `residual`
and 0 carry `passes_tried`. Meanwhile 1,752 open near-misses have their best draft ON DISK and
their target .s ON DISK, so the residual is not missing — it is one second of CPU away, and
recomputing it is strictly better than trusting a stored one (uncapped, current toolchain, current
tree). `collect` recomputes the whole corpus through the EXISTING validated compile path
(tools/match_one.py, R33 — never a second copy of the pipeline).
THE TWO INPUTS THAT MUST BE DERIVED, NOT GUESSED — both are silent-artefact generators:
* the asm subdir — an overlay has up to 14 TUs; the wrong one scores a draft against a
DIFFERENT function's asm (Phase 26-A).
* the -O0 flag — an -O0 TU's target bytes are -O0; scoring its draft at -O2 mismatches
everywhere no matter how correct the C is (§53/§54, Task-1's phantom "~3%").
Both come from corpus.py: the stub's self-describing INCLUDE_ASM line, and the Makefile's own -O0
rules. A record whose stub cannot be found is REPORTED, never silently dropped (R32) — a skipped
target produces silence, not an error, which is how 91.6% of remaining gain once went invisible.
tools/autopsy.py collect [--limit N] [--sample N] [-j 12] -> .run/autopsy/residuals.jsonl
tools/autopsy.py report [--top N] -> docs/autopsy.md + stdout
"""
import argparse
import collections
import json
import os
import random
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import backlog
import corpus
import residual_class
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PY = ".venv/bin/python"
OUT = ".run/autopsy/residuals.jsonl"
MD = "docs/autopsy.md"
DEFAULT_BINARY = "ov_SC01_077" # the legacy-record convention (backlog.load_best)
# --------------------------------------------------------------------------------------------
# collect
# --------------------------------------------------------------------------------------------
def _targets():
"""Open backlog near-misses with a draft on disk, plus the reason any record is excluded.
Returns (targets, excluded) where excluded is a Counter — every input is accounted for, so a
shrinking corpus can never be mistaken for a shrinking frontier (R32)."""
todo, skip = [], collections.Counter()
for r in backlog.load_best():
name, draft = r.get("name"), r.get("best_draft")
if not name:
skip["no name"] += 1
continue
if not draft:
skip["no saved draft"] += 1
continue
if not os.path.exists(os.path.join(REPO, draft)):
skip["draft path gone"] += 1
continue
todo.append(r)
return todo, skip
def _resolve(rec):
"""(asm_dir, o0, src_path) for a record, DERIVED — or (None, None, reason)."""
binary = rec.get("binary") or DEFAULT_BINARY
try:
st = corpus.stubs(binary)
except corpus.CorpusError as e:
return None, None, "corpus error (%s): %s" % (binary, str(e).split("\n")[0][:80])
hit = next((s for s in st.values() if s.symbol == rec["name"]), None)
if hit is None:
return None, None, "not a live stub in %s" % binary
return hit.asm_dir, corpus.is_o0(hit.path), hit.path
def _one(rec):
"""Recompile one draft through match_one --json and return an enriched autopsy row."""
name = rec["name"]
binary = rec.get("binary") or DEFAULT_BINARY
asm, o0, why = _resolve(rec)
row = {"name": name, "addr": rec.get("addr"), "binary": binary,
"reach": rec.get("reach"), "nins_logged": rec.get("nins"),
"closeness_logged": rec.get("closeness"), "klass_logged": rec.get("klass"),
"where_logged": rec.get("where_stuck"), "source": rec.get("source"),
"draft": rec.get("best_draft")}
if asm is None:
row.update({"status": "unresolved", "note": why})
return row
row.update({"asm_dir": asm, "o0": bool(o0), "src": why})
cmd = [PY, "tools/match_one.py", name, "--c", rec["best_draft"], "--asm-subdir", asm, "--json"]
if o0:
cmd.append("--o0")
try:
p = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO, timeout=180)
except subprocess.TimeoutExpired:
row.update({"status": "timeout"})
return row
lines = [ln for ln in p.stdout.strip().splitlines() if ln.strip()]
try:
j = json.loads(lines[-1])
except Exception:
# a compile failure (CPP/CC1/MASPSX/AS FAIL) is itself a CLASS — the draft does not build
# in isolation, which is plumbing, not codegen. Keep the reason; never drop the row.
row.update({"status": "nobuild",
"note": (lines[0] if lines else (p.stderr.strip().splitlines() or ["?"])[0])[:120]})
return row
v = j.get("verdict") or {}
row.update({"status": j.get("status"), "closeness": j.get("closeness"), "nins": j.get("nins"),
"klass": v.get("klass"), "bucket": v.get("bucket"), "profile": v.get("profile"),
"sig": v.get("sig"), "detail": v.get("detail"),
"disagree": v.get("closeness_disagree"), "residual": j.get("residual")})
return row
def collect(limit=None, sample=None, jobs=12):
todo, skip = _targets()
if sample:
random.seed(1729) # fixed seed: a re-run samples the SAME functions
todo = random.sample(todo, min(sample, len(todo)))
if limit:
todo = todo[:limit]
print("autopsy collect: %d target(s)%s; excluded %s"
% (len(todo), " (sampled)" if sample else "", dict(skip) or "{}"), flush=True)
rows, done = [], 0
with ThreadPoolExecutor(max_workers=jobs) as ex:
for row in ex.map(_one, todo):
rows.append(row)
done += 1
if done % 100 == 0:
print(" ... %d/%d" % (done, len(todo)), flush=True)
os.makedirs(os.path.join(REPO, os.path.dirname(OUT)), exist_ok=True)
with open(os.path.join(REPO, OUT), "w") as f:
for r in rows:
f.write(json.dumps(r) + "\n")
# COVERAGE + the second-oracle cross-check, both loud (R32/R34)
st = collections.Counter(r.get("status") for r in rows)
dis = [r for r in rows if r.get("disagree")]
err = [r for r in rows if r.get("klass") == "ERROR"]
print("\nwrote %s (%d rows)" % (OUT, len(rows)))
print("status:", dict(st))
if len(rows) != len(todo):
raise SystemExit("COVERAGE DEFECT: %d targets in, %d rows out" % (len(todo), len(rows)))
if dis:
raise SystemExit("ORACLE DISAGREEMENT on %d row(s): residual_class closeness != "
"structured_diff closeness. Fix before trusting any verdict.\n %s"
% (len(dis), ", ".join(r["name"] for r in dis[:8])))
if err:
raise SystemExit("CLASSIFIER ERROR on %d row(s): %s"
% (len(err), err[0].get("detail", {}).get("error")))
print("cross-check: closeness agrees with masked_diff.structured_diff on all %d built rows; "
"0 classifier errors." % sum(1 for r in rows if r.get("klass")))
return rows
# --------------------------------------------------------------------------------------------
# report
# --------------------------------------------------------------------------------------------
def _rows():
p = os.path.join(REPO, OUT)
if not os.path.exists(p):
raise SystemExit("no %s — run: tools/autopsy.py collect" % OUT)
return [json.loads(l) for l in open(p) if l.strip()]
def verdicts():
"""{fn_name: row} for the collected corpus, or {} if it has not been collected.
The consumer-facing accessor (tools/grinder.py). Returns {} rather than raising so a missing
corpus degrades to the previous undirected behaviour instead of breaking the daemon — but the
caller must SAY which mode it is in (R32: a filter that silently does nothing is the defect).
THE ROUTE IS RE-DERIVED HERE, NEVER READ FROM THE FILE (R33, Phase 29 T54). `klass` is the
measurement and is trusted; `profile`/`bucket` are a policy lookup over it, and a stored lookup
output makes a weeks-old file authoritative for a decision `residual_class._ROUTE` owns — a
route correction would be inert until someone re-ran the whole collect, and the stale row would
silently out-vote the live table. `route_for` reproduces the stored routes exactly under an
unchanged table (verified 1610/1610), so this is a no-op except where the table has since been
corrected — which is precisely when it must not be a no-op."""
p = os.path.join(REPO, OUT)
if not os.path.exists(p):
return {}
out = {}
for line in open(p):
if line.strip():
r = json.loads(line)
if r.get("name") and r.get("klass"):
r["profile"], r["bucket"] = residual_class.route_for(r["klass"], r.get("detail"))
out[r["name"]] = r
return out
def report(top=25):
rows = _rows()
built = [r for r in rows if r.get("klass")]
by_bucket = collections.Counter(r["bucket"] for r in built)
by_klass = collections.Counter(r["klass"] for r in built)
by_status = collections.Counter(r.get("status") for r in rows)
# weight by REACH: a class that blocks a reach-138 core is worth ~138 members
reach_w = collections.Counter()
for r in built:
reach_w[r["klass"]] += (r.get("reach") or 1)
# the SECOND ORACLE (R34): where the deterministic verdict disagrees with the human/LLM label
# a wave agent typed. Neither is authoritative — the DISAGREEMENT is the finding, and it is
# precisely the autopsy's `mis-classified` bucket.
agree, disagree = 0, []
for r in built:
kl = (r.get("klass_logged") or "").strip()
if not kl or kl.upper() in ("WAVE", "GIANT", "STUB", "OTHER", "PINS", "STRUCT"):
continue # generic tags carry no class claim
want = residual_class._ROUTE.get(r["klass"], (None, None))[0]
import permuter_weights
got = permuter_weights.classify(kl, r.get("where_logged") or "")
if want and got and want == got:
agree += 1
elif want and got:
disagree.append((r["name"], kl, got, r["klass"], want))
clusters = collections.Counter(r["sig"] for r in built)
L = []
L.append("# Autopsy — the permuter's failures, classified\n")
L.append("> Generated by `tools/autopsy.py report` from `.run/autopsy/residuals.jsonl` "
"(built by `collect`, which recompiles every open backlog draft through "
"`tools/match_one.py` and classifies the FULL residual with `tools/residual_class.py`).\n")
L.append("> A verdict here is a **candidate**, never a bank: the whole-binary byte-gate "
"remains the sole arbiter (G3/P9).\n")
L.append("**Corpus:** %d record(s) · %d classified · status %s\n"
% (len(rows), len(built), dict(by_status)))
L.append("\n## Buckets — what tool each failure actually wants\n")
L.append("| bucket | fns | reach-weighted | meaning |")
L.append("|--------|----:|---------------:|---------|")
MEAN = {"permuter": "local mutation CAN reach it — permuter fuel (a plateau here is a "
"**missing-transform** candidate)",
"structural": "local mutation CANNOT introduce it — wants a C-level idiom, not CPU",
"integration": "byte-correct standalone; blocked on plumbing (§58/§59 reconcile ladder)",
"redraft": "the stored draft is NOT this function — un-attempted work misfiled as a "
"near-miss; route to a fresh crack, not to a wall",
"unknown": "the residue the LLM tier is for"}
for b, n in by_bucket.most_common():
rw = sum(r.get("reach") or 1 for r in built if r["bucket"] == b)
L.append("| %s | %d | %d | %s |" % (b, n, rw, MEAN.get(b, "")))
L.append("\n## Classes\n")
L.append("| class | fns | reach-weighted | bucket | permuter profile |")
L.append("|-------|----:|---------------:|--------|------------------|")
for k, n in by_klass.most_common():
prof, bucket = residual_class._ROUTE.get(k, (None, "?"))
L.append("| %s | %d | %d | %s | %s |" % (k, n, reach_w[k], bucket, prof or "—"))
L.append("\n## Top clusters (the batch-autopsy work items)\n")
L.append("One representative per signature diagnoses the whole cluster — that is what makes "
"the LLM pass a *batch* autopsy rather than N per-function reads.\n")
L.append("| # | signature | fns | reach-weighted | example |")
L.append("|--:|-----------|----:|---------------:|---------|")
for i, (sg, n) in enumerate(clusters.most_common(top), 1):
members = [r for r in built if r["sig"] == sg]
rw = sum(r.get("reach") or 1 for r in members)
ex = min(members, key=lambda r: (r.get("closeness") or 10**9))
L.append("| %d | `%s` | %d | %d | %s (close=%s, %s ins) |"
% (i, sg, n, rw, ex["name"], ex.get("closeness"), ex.get("nins")))
L.append("\n## Second oracle — deterministic verdict vs the recorded label\n")
L.append("The recorded `klass` is what a wave agent typed months ago; the verdict is derived "
"from today's bytes. Neither is authoritative — **the disagreement is the finding**, "
"and it is exactly the `mis-classified` bucket (a wrongly-labelled residual sends the "
"directed search at the wrong levers, so it plateaus and then reads as a wall).\n")
L.append("- labelled records comparable: **%d** · agree **%d** · disagree **%d**\n"
% (agree + len(disagree), agree, len(disagree)))
if disagree:
L.append("| fn | recorded label | label routes to | measured class | measured routes to |")
L.append("|----|----------------|-----------------|----------------|--------------------|")
for nm, kl, got, mk, want in disagree[:top]:
L.append("| %s | `%s` | %s | %s | %s |" % (nm, kl, got, mk, want))
open(os.path.join(REPO, MD), "w").write("\n".join(L) + "\n")
print("\n== buckets ==")
for b, n in by_bucket.most_common():
print(" %-12s %5d fns reach-weighted %7d %s"
% (b, n, sum(r.get("reach") or 1 for r in built if r["bucket"] == b), MEAN.get(b, "")))
print("\n== classes ==")
for k, n in by_klass.most_common():
print(" %-18s %5d reach-weighted %7d -> %s / %s"
% (k, n, reach_w[k], residual_class._ROUTE.get(k, (None, "?"))[1],
residual_class._ROUTE.get(k, (None, None))[0] or "—"))
print("\n== top clusters ==")
for sg, n in clusters.most_common(top):
print(" %-34s %4d" % (sg, n))
print("\n== second oracle ==")
print(" labelled+comparable %d: agree %d, disagree %d" % (agree + len(disagree), agree, len(disagree)))
for nm, kl, got, mk, want in disagree[:12]:
print(" %-18s recorded %-16s -> %-9s | measured %-17s -> %s" % (nm, kl, got, mk, want))
print("\nwrote %s" % MD)
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("collect")
c.add_argument("--limit", type=int)
c.add_argument("--sample", type=int, help="random SAMPLE (fixed seed) — probe before scaling")
c.add_argument("-j", "--jobs", type=int, default=12)
r = sub.add_parser("report")
r.add_argument("--top", type=int, default=25)
a = ap.parse_args()
if a.cmd == "collect":
collect(a.limit, a.sample, a.jobs)
else:
report(a.top)
if __name__ == "__main__":
main()