From a67ebfa2637838554779f1121959ba861edf592b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 09:20:33 -0400 Subject: [PATCH] feat: emit SCJUMP decision table (json+md, registry-named) (Task 3) build/scjump-decisions.* are generated (build/ gitignored). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/scjump_decode.py | 68 ++++++++++++++++++++++++++++++++++++++++++ tools/test_scjump.py | 12 ++++++++ 2 files changed, 80 insertions(+) diff --git a/tools/scjump_decode.py b/tools/scjump_decode.py index 3d04ad7..41e709e 100644 --- a/tools/scjump_decode.py +++ b/tools/scjump_decode.py @@ -142,6 +142,74 @@ def decode(scr): return decisions +def load_names() -> dict: + """addr:int -> curated registry name (from build/globals.json), or {} if absent.""" + try: + data = json.loads((paths.BUILD / "globals.json").read_text(encoding="utf-8")) + except Exception: + return {} + out = {} + for addr_s, e in data.get("globals", {}).items(): + if e.get("name"): + out[int(addr_s, 16)] = e["name"] + return out + + +def render_guard(g, names) -> str: + if "opaque" in g: return g["opaque"] + if "and" in g: return "(" + " AND ".join(render_guard(x, names) for x in g["and"]) + ")" + if "or" in g: return "(" + " OR ".join(render_guard(x, names) for x in g["or"]) + ")" + lbl = names.get(g["global"], f"0x{g['global']:x}") + return f"{lbl}{g['op']}{g['value']}" + + +def emit_json(decs, names) -> str: + def jguard(g): + if "opaque" in g or "and" in g or "or" in g: return g + return {"global": f"0x{g['global']:x}", "name": names.get(g["global"]), "op": g["op"], "value": g["value"]} + out_decs = [{"site_offset": f"0x{d['site_offset']:x}", "chapter": d["chapter"], + "decision": d["decision"], "guards": [jguard(g) for g in d["guards"]]} for d in decs] + by_chapter = collections.defaultdict(list) + for d in out_decs: + by_chapter[d["chapter"]].append(d["decision"]) + meta = {"note": "GENERATED by tools/scjump_decode.py — do not edit. Decision->scene is native (u00428010), not resolved here.", + "decision_sites": len({d["site_offset"] for d in decs}), + "distinct_decisions": len({d["decision"] for d in decs})} + return json.dumps({"meta": meta, "decisions": out_decs, + "by_chapter": {str(k): sorted(set(v)) for k, v in sorted(by_chapter.items(), key=lambda kv: (kv[0] is None, kv[0]))}}, + ensure_ascii=False, indent=2) + "\n" + + +def emit_md(decs, names) -> str: + L = ["", + "# SCJUMP progression decisions (generated)", "", + f"{len({d['site_offset'] for d in decs})} decision sites. Each rule: guards (all true along the " + "path) -> decision value written to `0x62ccf`. Decision->scene is native (`u00428010`), see " + "`docs/scjump-progression.md`.", ""] + by_ch = collections.defaultdict(list) + for d in decs: + by_ch[d["chapter"]].append(d) + for ch in sorted(by_ch, key=lambda c: (c is None, c)): + L += [f"## chapter {ch}", ""] + for d in sorted(by_ch[ch], key=lambda d: d["site_offset"]): + guards = " AND ".join(render_guard(g, names) for g in d["guards"]) or "(unconditional)" + L.append(f"- `0x{d['site_offset']:x}`: {guards} → **decision {d['decision']}**") + L.append("") + return "\n".join(L) + "\n" + + +def build(scr=None) -> int: + scr = scr or load_scjump() + decs = decode(scr) + names = load_names() + paths.BUILD.mkdir(parents=True, exist_ok=True) + (paths.BUILD / "scjump-decisions.json").write_text(emit_json(decs, names), encoding="utf-8") + (paths.BUILD / "scjump-decisions.md").write_text(emit_md(decs, names), encoding="utf-8") + print(f"decode: {len({d['site_offset'] for d in decs})} decision sites, " + f"{len({d['decision'] for d in decs})} distinct decisions -> build/scjump-decisions.{{json,md}}") + return 0 + + def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("--verify", action="store_true") # implemented in Task 4 diff --git a/tools/test_scjump.py b/tools/test_scjump.py index 291fe3d..ff550b6 100644 --- a/tools/test_scjump.py +++ b/tools/test_scjump.py @@ -29,8 +29,20 @@ def test_decode_anchor_and_count(): check(has_chapter, "anchor guarded by chapter_mode==1") check(has_flag, "anchor guarded by 0x6d3!=1") +def test_emit_json_shape(): + scr = S.load_scjump() + decs = S.decode(scr) + names = S.load_names() + obj = json.loads(S.emit_json(decs, names)) + check(obj["meta"]["decision_sites"] == 1755, "json meta reports 1755 sites") + check(all(set(("site_offset", "chapter", "decision", "guards")) <= set(d) for d in obj["decisions"]), "each decision has required keys") + # rendering uses registry names when present + check(S.render_guard({"global": 0x3234, "op": "==", "value": 7}, {0x3234: "chapter_mode"}) == "chapter_mode==7", + "render_guard uses registry name") + if __name__ == "__main__": test_cfg_acyclic_and_dispatch() test_decode_anchor_and_count() + test_emit_json_shape() print(f"\n{len(FAILS)} failures") sys.exit(1 if FAILS else 0)