mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 14:02:04 -04:00
e7ebb1cd45
R42: gate_main reverted 61 byte-proven overlay banks it could not distinguish from its own substitution (sweep_parallel gates commit=False by design). Fixed by committing overlay banks before the main batch, chunking main at 8 to bound bisect cost, and replacing every blind 'git checkout -- src/ config/' with commit-or-refuse in ox_campaign and idiom_serial. R43: sweep_parallel had an explicit branch admitting main, which cannot be gated incrementally — wave ab banked 0/105 main cards while its non-main cards banked 94/115 (82%), and the wave read as a drafting failure. sweep_parallel now refuses main and names gate_main.py. Also: validate_targets now prefers the card's own addr field (named symbols like SYS_OBJ_F00 were MALFORMED and discarded whole 220-card waves); ox_campaign deals model lanes by smallest-ratio scheduling (a 73-card wave had put 73 shards on ox and 0 on deepseek); docs/accelerators.md gains the four vacuous-check defects.
247 lines
12 KiB
Python
247 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""idiom_serial.py — the SERIAL idiom-learning lane (P31 S58, Drew's design).
|
|
|
|
The wide ox campaign (tools/ox_campaign.py) is parallel and stateless: every shard starts from the
|
|
same knowledge and nothing one shard learns reaches another. That is the right shape for a pool of
|
|
independent functions and the WRONG shape for a wall class, where the whole point is that cracking
|
|
member N teaches you something that makes member N+1 cheaper.
|
|
|
|
Measured precedent (P31 S57): ox spent 40 turns / 5 oracle calls learning the jtbl exemplar, then
|
|
took 11 turns / 3 oracle calls on a WITHIN-FAMILY sibling — and 56 turns / 0 compiles on a
|
|
cross-family one. Learning transfers inside a family and dies at its border. So the lane is serial
|
|
BY FAMILY: crack the exemplar, distill what was learned, feed that forward, crack the next.
|
|
|
|
THE LOOP, per target:
|
|
1. prepare — lever-specific. jtbl-carve => tools/jtbl_carve.py + `make extract` for that binary,
|
|
because a verified match_one is NOT a bankable card until the table is carved.
|
|
2. draft — ONE api_agent run on ONE function, with --cards fuel AND --brief carrying every
|
|
idiom banked so far this run. This is the compounding channel.
|
|
3. gate — the whole-binary byte-gate is the sole arbiter (G3/P9). Nothing else counts.
|
|
4. distill — on a MATCH only, a second api_agent call asks the model for the generalizable idiom
|
|
and appends it to the notes file that step 2 reads next time.
|
|
|
|
Targets come from the frontier atlas, largest leverage first. md_* jtbl members are EXCLUDED by
|
|
default: `jtbl_carve` refuses leading-island modules (byte-measured P30 S48), so they cost a
|
|
preparation failure every time. ov_* is where the carve works — 169 of the 245 jtbl members.
|
|
|
|
tools/idiom_serial.py --lever jtbl-carve --n 6
|
|
tools/idiom_serial.py --lever extend-tell --n 4 --min-ins 120
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
os.chdir(REPO)
|
|
PY = ".venv/bin/python"
|
|
NOTES = ".run/idioms_learned.md"
|
|
LEDGER = ".run/idiom_serial_ledger.jsonl"
|
|
|
|
|
|
def log(m):
|
|
print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)
|
|
|
|
|
|
def sh(cmd, timeout=None):
|
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
|
|
|
|
def env_for_api():
|
|
key = ""
|
|
for line in open(".env"):
|
|
if line.startswith("open_router_key="):
|
|
key = line.split("=", 1)[1].strip(); break
|
|
return dict(os.environ, API_BASE="https://openrouter.ai/api/v1", API_KEY=key,
|
|
MODEL=os.environ.get("MODEL", "stealth/ox-alpha"),
|
|
MAXTOK=os.environ.get("MAXTOK", "8000"))
|
|
|
|
|
|
def pick_targets(lever, n, min_ins, allow_md):
|
|
a = json.load(open(".run/atlas.json"))
|
|
groups = a["groups"] if isinstance(a, dict) and "groups" in a else a
|
|
out = []
|
|
for g in groups:
|
|
if g.get("lever") != lever:
|
|
continue
|
|
ex = g["exemplar"]
|
|
if ex["nins"] < min_ins:
|
|
continue
|
|
if not allow_md and ex["b"].startswith("md_"):
|
|
continue
|
|
out.append({"gid": g["gid"], "name": ex["name"], "binary": ex["b"], "addr": ex["a"],
|
|
"nins": ex["nins"], "group_ins": g["ins"], "inst": g["inst"],
|
|
"lever": lever})
|
|
out.sort(key=lambda x: -x["group_ins"]) # leverage first: the whole family's mass
|
|
return out[:n]
|
|
|
|
|
|
def prepare(t):
|
|
"""Lever-specific preparation. Returns (ok, note)."""
|
|
if t["lever"] != "jtbl-carve":
|
|
return True, "no preparation needed"
|
|
r = sh(f"{PY} tools/jtbl_carve.py {t['binary']} --func {t['name']}", timeout=1800)
|
|
blob = (r.stdout + r.stderr).strip()
|
|
if r.returncode != 0 or "LEADING-ISLAND" in blob or "NON-CONTIGUOUS" in blob:
|
|
# Report the LAST meaningful lines, not the first. jtbl_carve prints informational
|
|
# progress before it fails, so `splitlines()[0]` reported `trimmed 1 trailing .align pad
|
|
# word(s) — 13 real entries` — an INFO line — as the refusal reason for ov_SC06_029, while
|
|
# the actual cause was never shown. A Traceback likewise begins "Traceback (most recent
|
|
# call last):", which names nothing. The last lines carry the exception and the message.
|
|
lines = [l for l in blob.splitlines() if l.strip()]
|
|
why = " | ".join(lines[-3:])[:400] if lines else f"rc={r.returncode}"
|
|
cls = ("LEADING-ISLAND" if "LEADING-ISLAND" in blob else
|
|
"NON-CONTIGUOUS" if "NON-CONTIGUOUS" in blob else
|
|
"CRASH" if "Traceback" in blob else "REFUSED")
|
|
return False, f"carve {cls}: {why}"
|
|
r = sh(f"make --no-print-directory extract BINARY={t['binary']}", timeout=3600)
|
|
if r.returncode != 0:
|
|
sh(f"{PY} tools/jtbl_carve.py {t['binary']} --revert")
|
|
return False, "make extract failed after carve (carve reverted)"
|
|
return True, "carved + re-extracted"
|
|
|
|
|
|
def revert_prepare(t):
|
|
if t["lever"] == "jtbl-carve":
|
|
sh(f"{PY} tools/jtbl_carve.py {t['binary']} --revert")
|
|
sh(f"make --no-print-directory extract BINARY={t['binary']}", timeout=3600)
|
|
|
|
|
|
def build_brief(t):
|
|
"""The accumulating brief — every idiom banked so far, plus this target's framing."""
|
|
learned = open(NOTES).read() if os.path.exists(NOTES) else ""
|
|
head = (f"You are cracking ONE function to byte-identical MIPS: {t['name']} in {t['binary']}, "
|
|
f"{t['nins']} instructions, lever {t['lever']}.\n"
|
|
f"Its group holds {t['inst']} members / {t['group_ins']} instructions, so what you learn "
|
|
f"here is worth far more than this one function.\n\n"
|
|
"The .s is the ONLY ground truth. match_one masks jal/HI16/LO16 relocations, so it "
|
|
"verifies instruction SHAPE, not symbol IDENTITY — a wrong-symbol draft reports a clean "
|
|
"MATCH. Check the symbols yourself against the .s.\n")
|
|
if learned.strip():
|
|
head += ("\n--- IDIOMS ALREADY LEARNED IN THIS RUN (each one was byte-confirmed; use them) ---\n"
|
|
+ learned + "\n--- end idioms ---\n")
|
|
return head
|
|
|
|
|
|
def distill(t, outdir, env):
|
|
"""Ask for the generalizable idiom and append it to the notes the next target will read."""
|
|
brief = (f"You just matched {t['name']} ({t['binary']}, {t['nins']} instructions) to "
|
|
"byte-identical output. Write the GENERALIZABLE idiom you used, for a decompilation "
|
|
"cookbook read by other agents.\n\n"
|
|
"Rules: describe the asm TELL that identifies this pattern, the C construct that "
|
|
"reproduces it, and the boundary — when it does NOT apply. Name one thing you tried "
|
|
"that FAILED and why. No narration of your session; write it as a reusable rule. "
|
|
"Under 300 words. Reply with the note itself, no preamble.")
|
|
bp = f"{outdir}/distill_brief.txt"
|
|
open(bp, "w").write(brief)
|
|
tf = f"{outdir}/distill_target.json"
|
|
json.dump([{"name": t["name"], "addr": t["addr"], "nins": t["nins"], "binary": t["binary"],
|
|
"sub": t.get("sub", ""), "asm": t.get("asm", ""), "tu": t.get("tu")}], open(tf, "w"))
|
|
r = subprocess.run([PY, "-u", "tools/api_agent.py", "--targets", tf, "--brief", bp,
|
|
"--out", f"{outdir}/distill", "--max-turns", "4", "--max-cost", "0.50"],
|
|
capture_output=True, text=True, env=env, timeout=3600)
|
|
text = ""
|
|
for p in sorted(glob.glob(f"{outdir}/distill/*.txt")) + sorted(glob.glob(f"{outdir}/distill/*.md")):
|
|
text = open(p).read(); break
|
|
if not text:
|
|
tail = [l for l in r.stdout.splitlines() if l.strip()]
|
|
text = "\n".join(tail[-25:])
|
|
with open(NOTES, "a") as fh:
|
|
fh.write(f"\n\n## {t['name']} ({t['binary']}, {t['nins']} ins, lever {t['lever']})\n"
|
|
f"_banked {time.strftime('%Y-%m-%d %H:%M')} — group worth {t['group_ins']} ins_\n\n"
|
|
f"{text.strip()}\n")
|
|
return len(text.strip())
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--lever", default="jtbl-carve")
|
|
ap.add_argument("--n", type=int, default=6, help="how many targets to work, serially")
|
|
ap.add_argument("--min-ins", type=int, default=60)
|
|
ap.add_argument("--max-turns", type=int, default=60)
|
|
ap.add_argument("--allow-md", action="store_true",
|
|
help="include md_* members (jtbl_carve refuses leading-island modules — off by default)")
|
|
a = ap.parse_args()
|
|
|
|
targets = pick_targets(a.lever, a.n, a.min_ins, a.allow_md)
|
|
if not targets:
|
|
sys.exit(f"no {a.lever} targets at >={a.min_ins} ins (allow_md={a.allow_md})")
|
|
log(f"serial idiom lane: {len(targets)} targets, lever {a.lever}, "
|
|
f"{sum(t['group_ins'] for t in targets)} group-instructions of leverage")
|
|
env = env_for_api()
|
|
|
|
for i, t in enumerate(targets, 1):
|
|
log(f"=== [{i}/{len(targets)}] {t['name']} @ {t['binary']} · {t['nins']} ins · "
|
|
f"group {t['inst']} members / {t['group_ins']} ins ===")
|
|
row = {"t": time.time(), "target": t, "stage": "prepare"}
|
|
# Do NOT blind-revert. A concurrent lane (the campaign gates with commit=False) leaves
|
|
# real banked work uncommitted, and `git checkout --` cannot tell it from residue. Commit
|
|
# it instead; refuse to proceed if that fails. Same footgun as ox_campaign.gate().
|
|
if sh("git status --porcelain -- src/ config/").stdout.strip():
|
|
log(" tree dirty at entry — committing it rather than reverting")
|
|
sh("git add -A src/ config/")
|
|
sh('git commit -q -m "chore(decomp): commit in-tree banked work before the serial lane"')
|
|
if sh("git status --porcelain -- src/ config/").stdout.strip():
|
|
log(" ! could not commit — refusing to run the serial lane on a dirty tree")
|
|
return 1
|
|
|
|
ok, note = prepare(t)
|
|
log(f" prepare: {note}")
|
|
if not ok:
|
|
row.update(outcome="prepare-failed", note=note)
|
|
open(LEDGER, "a").write(json.dumps(row) + "\n"); continue
|
|
|
|
outdir = f".run/idiom_serial/{t['name']}"
|
|
shutil.rmtree(outdir, ignore_errors=True); os.makedirs(outdir, exist_ok=True)
|
|
sub = os.path.dirname(sh(f"ls asm/{t['binary']}/nonmatchings/*/{t['name']}.s 2>/dev/null "
|
|
"| head -1").stdout.strip())
|
|
t["sub"], t["asm"] = sub, f"{sub}/{t['name']}.s"
|
|
t["tu"] = None
|
|
if not sub:
|
|
row.update(outcome="no-asm", note="target .s not found after prepare")
|
|
open(LEDGER, "a").write(json.dumps(row) + "\n"); revert_prepare(t); continue
|
|
|
|
bp = f"{outdir}/brief.txt"; open(bp, "w").write(build_brief(t))
|
|
tf = f"{outdir}/target.json"; json.dump([t], open(tf, "w"), indent=1)
|
|
log(f" drafting (max {a.max_turns} turns, brief carries "
|
|
f"{os.path.getsize(NOTES) if os.path.exists(NOTES) else 0} bytes of learned idioms)")
|
|
subprocess.run([PY, "-u", "tools/api_agent.py", "--targets", tf, "--cards",
|
|
".run/aprop_cards.json", "--brief", bp, "--out", f"{outdir}/draft",
|
|
"--max-turns", str(a.max_turns), "--max-cost", "2.0"],
|
|
stdout=open(f"{outdir}/draft.log", "w"), stderr=subprocess.STDOUT,
|
|
env=env, timeout=14400)
|
|
|
|
cfile = f"{outdir}/draft/{t['name']}.c"
|
|
if not os.path.isfile(cfile):
|
|
row.update(outcome="no-draft")
|
|
open(LEDGER, "a").write(json.dumps(row) + "\n"); revert_prepare(t); continue
|
|
|
|
gd = f"{outdir}/gate/{t['binary']}"; os.makedirs(gd, exist_ok=True)
|
|
shutil.copy(cfile, f"{gd}/{t['name']}.c")
|
|
t0 = time.time()
|
|
sh(f"{PY} tools/sweep_parallel.py --drafts {outdir}/gate -j 2", timeout=7200)
|
|
banked = any(os.path.getmtime(f) >= t0 and t["name"] in open(f).read()
|
|
for f in glob.glob(".run/auto/bulk/*.verified.txt"))
|
|
if banked:
|
|
sh("git add -A src/ config/")
|
|
sh(f'git commit -q -m "feat(decomp): serial idiom lane — {t["name"]} '
|
|
f'({t["nins"]} ins, {t["lever"]}, group worth {t["group_ins"]})"')
|
|
nb = distill(t, outdir, env)
|
|
log(f" BANKED + distilled {nb} chars of idiom -> {NOTES}")
|
|
row.update(outcome="banked", idiom_chars=nb,
|
|
commit=sh("git rev-parse --short HEAD").stdout.strip())
|
|
else:
|
|
log(" not banked — reverting preparation")
|
|
revert_prepare(t)
|
|
row.update(outcome="gate-failed")
|
|
open(LEDGER, "a").write(json.dumps(row) + "\n")
|
|
log("serial lane finished")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main() or 0)
|