mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 05:32:17 -04:00
1748eabe62
§63 has defined T0/T1/T2 since Phase 26 and never enforced it. Two measured consequences in SESSION-20: ~13 full clean-fleet verifies (~15 min each) for batches that were provably T1 (over-verification), AND two cases where the write set was LARGER than the belief about it — the §85 widen believed contained to one overlay broke ov_SC01_077, and gate_stage's ARITY pre-pass silently rewrote 40 TUs (under-verification, the dangerous half). A tier is a CLAIM about the write set; this turns it into a MEASUREMENT. --expect t1 fails loud when the tree disagrees. Binary list DERIVED from config/splat.*.yaml (R33, never hardcoded — R36's incident was a hardcoded set missing 4 real binaries). Coverage ASSERTED (R32): an unclassified path exits 2 and names itself rather than being silently skipped. overlays.mk is attributed per-binary by parsing its diff, so a change confined to one binary's var-block stays T1.
180 lines
8.1 KiB
Python
180 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""blast_radius.py — MEASURE the write set and name the gate it actually requires (§63 enforcement).
|
|
|
|
THE PROBLEM THIS SOLVES. §63 has defined a blast-radius taxonomy since Phase 26:
|
|
|
|
T0 draft-only (.run/**) -> no gate; nothing in the build tree changed
|
|
T1 binary-local (src/<bin>/**) -> the PER-BINARY gate is SUFFICIENT
|
|
T2 fleet-shared (src/shared/**, -> R22 clean-fleet is MANDATORY
|
|
tools/, Makefile, us-wide config)
|
|
|
|
...and it has never been ENFORCED. Two consequences, both measured in Phase-29 SESSION-20:
|
|
|
|
* OVER-VERIFICATION. That session ran ~13 full clean-fleet verifies (~15 min each), most of them
|
|
for batches that were provably T1. Hours of serialised wall-clock spent proving something the
|
|
written rule already guaranteed.
|
|
* UNDER-VERIFICATION, twice, and this is the dangerous half. The §85 return-type widen was BELIEVED
|
|
contained to one overlay; it broke ov_SC01_077 (R22 139/140) because the source overlay carries
|
|
its own local `extern void` decls. And gate_stage's ARITY pre-pass silently rewrote caller decls
|
|
in 40 TUs. In BOTH cases the write set was larger than the belief about it.
|
|
|
|
So the value is not only speed. **A tier is a CLAIM about the write set; this tool turns it into a
|
|
measurement.** `--expect t1` fails loud when the tree disagrees, which is exactly the shape of error
|
|
that produced both incidents above.
|
|
|
|
COVERAGE IS ASSERTED (R32). Every changed path must match a rule. An unclassified path is a DEFECT,
|
|
not a no-op: it exits non-zero and names the path, because a silent skip here would re-create the
|
|
exact blindness the taxonomy exists to remove. The binary list is DERIVED from config/splat.*.yaml
|
|
(R33), never hardcoded — Phase-28's R36 incident was a hardcoded set missing 4 real binaries.
|
|
|
|
tools/blast_radius.py # report the tier + the gate it requires
|
|
tools/blast_radius.py --expect t1 --binary ov_SC01_000 # assert containment; non-zero if exceeded
|
|
tools/blast_radius.py --json
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def binaries():
|
|
"""DERIVED (R33): every binary that has a splat config. Never a hardcoded list (R36)."""
|
|
out = set()
|
|
for p in glob.glob(os.path.join(REPO, "config", "splat.*.yaml")):
|
|
b = os.path.basename(p)[len("splat."):-len(".yaml")]
|
|
if b == "us.overlay.template":
|
|
continue
|
|
out.add("main" if b == "us.exe" else b)
|
|
return out
|
|
|
|
|
|
def changed_paths():
|
|
r = subprocess.run(["git", "status", "--porcelain"], cwd=REPO,
|
|
capture_output=True, text=True, check=True)
|
|
paths = []
|
|
for line in r.stdout.splitlines():
|
|
if not line.strip():
|
|
continue
|
|
p = line[3:].strip()
|
|
if " -> " in p: # rename
|
|
p = p.split(" -> ", 1)[1]
|
|
paths.append(p.strip('"'))
|
|
return paths
|
|
|
|
|
|
def overlays_mk_binaries():
|
|
"""Which binaries' var-blocks changed in config/overlays.mk? A change confined to ONE binary's
|
|
block is binary-local despite living in a shared file."""
|
|
r = subprocess.run(["git", "diff", "-U0", "--", "config/overlays.mk"], cwd=REPO,
|
|
capture_output=True, text=True)
|
|
hits = set()
|
|
for line in r.stdout.splitlines():
|
|
if line.startswith(("+", "-")) and not line.startswith(("+++", "---")):
|
|
for m in re.finditer(r'\b(ov_[A-Za-z0-9_]+?)_(?:JTBL|O0|WHALE|[A-Z])', line):
|
|
hits.add(m.group(1))
|
|
for m in re.finditer(r'\b(ov_[A-Za-z0-9_]+)\.o\b', line):
|
|
hits.add(m.group(1))
|
|
return hits
|
|
|
|
|
|
def classify(paths, bins):
|
|
"""-> (tier, affected_binaries, reasons, unclassified). Tier is the MAX over all paths."""
|
|
tier, affected, reasons, unknown = 0, set(), [], []
|
|
for p in paths:
|
|
if p.startswith("ghidra/"): # R23 restart churn — never staged
|
|
continue
|
|
if p.startswith((".run/", "build/", "expected/", "asm/", "assets/", ".venv/")):
|
|
continue # T0: scratch / regenerated
|
|
if p.startswith(("docs/", "phase-ends/", "README", ".gitignore", ".claude/")):
|
|
continue # no build impact
|
|
if p.startswith("src/shared/"):
|
|
tier = max(tier, 2); reasons.append(f"T2 shared source: {p}"); continue
|
|
if p.startswith("src/"):
|
|
parts = p.split("/")
|
|
if len(parts) >= 3 and parts[1] in bins:
|
|
tier = max(tier, 1); affected.add(parts[1])
|
|
reasons.append(f"T1 binary-local: {p}"); continue
|
|
unknown.append(p); continue
|
|
if p.startswith("config/"):
|
|
base = os.path.basename(p)
|
|
m = re.fullmatch(r'(?:splat|check|symbols)\.(.+?)\.(?:yaml|sha|txt)', base)
|
|
if m:
|
|
b = "main" if m.group(1) == "us.exe" else m.group(1)
|
|
if b in bins:
|
|
tier = max(tier, 1); affected.add(b)
|
|
reasons.append(f"T1 binary config: {p}"); continue
|
|
if base == "overlays.mk":
|
|
hit = overlays_mk_binaries() & bins
|
|
if hit:
|
|
tier = max(tier, 1); affected |= hit
|
|
reasons.append(f"T1 overlays.mk (blocks: {','.join(sorted(hit))})"); continue
|
|
tier = max(tier, 2); reasons.append(f"T2 overlays.mk (blocks unattributable): {p}"); continue
|
|
tier = max(tier, 2); reasons.append(f"T2 fleet config: {p}"); continue
|
|
if p.startswith(("tools/", "Makefile", "diff_settings.py", "requirements")):
|
|
tier = max(tier, 2); reasons.append(f"T2 toolchain: {p}"); continue
|
|
unknown.append(p)
|
|
return tier, affected, reasons, unknown
|
|
|
|
|
|
GATE = {
|
|
0: "none — nothing in the build tree changed",
|
|
1: "PER-BINARY: make extract BINARY=<b> && make build BINARY=<b> (for each affected binary)",
|
|
2: "R22 CLEAN-FLEET MANDATORY: make clean && make extract-all && make check-all",
|
|
}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--expect", choices=["t0", "t1", "t2"],
|
|
help="assert the measured tier does not EXCEED this; non-zero exit if it does")
|
|
ap.add_argument("--binary", help="with --expect t1: assert the write set touches only this binary")
|
|
ap.add_argument("--json", action="store_true")
|
|
a = ap.parse_args()
|
|
|
|
bins = binaries()
|
|
paths = changed_paths()
|
|
tier, affected, reasons, unknown = classify(paths, bins)
|
|
|
|
if unknown: # R32: an unclassified path is a DEFECT
|
|
print("blast_radius: UNCLASSIFIED PATHS (refusing to guess a tier):", file=sys.stderr)
|
|
for p in unknown:
|
|
print(f" {p}", file=sys.stderr)
|
|
print("Add a rule for these before trusting any tier.", file=sys.stderr)
|
|
return 2
|
|
|
|
if a.json:
|
|
print(json.dumps({"tier": tier, "affected": sorted(affected),
|
|
"gate": GATE[tier], "reasons": reasons}))
|
|
else:
|
|
print(f"blast_radius: TIER {tier} ({len(bins)} binaries known)")
|
|
for r in reasons[:12]:
|
|
print(f" {r}")
|
|
if len(reasons) > 12:
|
|
print(f" … and {len(reasons)-12} more")
|
|
if affected:
|
|
print(f" affected binaries: {', '.join(sorted(affected))}")
|
|
print(f" REQUIRED GATE -> {GATE[tier]}")
|
|
|
|
if a.expect:
|
|
want = int(a.expect[1])
|
|
if tier > want:
|
|
print(f"\nBLAST-RADIUS EXCEEDED: measured T{tier}, expected T{want}.\n"
|
|
f" The write set is LARGER than the claim. This is the §85/ARITY failure shape:\n"
|
|
f" a change believed contained that reaches further. Run: {GATE[tier]}",
|
|
file=sys.stderr)
|
|
return 1
|
|
if want == 1 and a.binary and affected - {a.binary}:
|
|
print(f"\nBLAST-RADIUS EXCEEDED: expected only {a.binary}, also touched "
|
|
f"{', '.join(sorted(affected - {a.binary}))}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|