Bootstrap 56 high-signal skeletons; name game_mode/route_branch/scjump_decision_out2, recategorize CONFIG-written globals as non-story. Docs: name-resolution.md registry section, CLAUDE.md canonical+SoT tables + trigger (root, untracked), tools-reference rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
186 lines
8.2 KiB
Python
186 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Static story-state flag miner. Scans the 481-script corpus for global variables that feed
|
|
branch conditions (comparisons / jcc), gathers evidence, classifies candidates, and emits a
|
|
ranked review surface. 100% static -- no runtime, no sweep. See
|
|
docs/superpowers/specs/2026-07-07-globals-registry-and-story-flags-design.md.
|
|
(no flag) -> build/story-flags-candidates.json
|
|
--bootstrap append skeleton [[global]] entries to vm-map/globals.toml (Task 5)"""
|
|
from __future__ import annotations
|
|
import os, sys, re, json, argparse, collections
|
|
from pathlib import Path
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import paths
|
|
import sys4load
|
|
|
|
CMP_OPS = {0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f} # eq ne lt lte gr gre (operands at arg idx 1,2)
|
|
LOGIC_OPS = {0x56, 0x57} # and or (operands at arg idx 1,2)
|
|
JCC_OP = 0xa0 # condition at arg idx 0
|
|
ASSIGN_OP = 0x55 # mov -> writes arg0
|
|
GLOBAL_ATYPES = {3, 4, 5, 6, 8}
|
|
SCENE_RE = re.compile(r"^S[CP]\d{4}\.BIN$")
|
|
KNOWN_UI_TOGGLES = {0x6c9, 0x6ca, 0x6cb, 0x6cd, 0x6cc}
|
|
NEAR_UNIVERSAL = 250 # scene-reach at/above this = ADV-chrome-wide, not a story flag
|
|
|
|
|
|
def _excluded_addrs() -> set[int]:
|
|
"""Addresses the auto shape map classifies as genuine tables/index pointers -- excluded from
|
|
story-flag candidacy. Deliberately does NOT exclude the auto 'string-table' *label* guesses
|
|
(those are unreliable: 0xa57, a real story flag, is mislabelled string-table)."""
|
|
try:
|
|
data = json.loads((paths.BUILD / "global-var-map.json").read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return set()
|
|
excl = {int(c["addr"], 16) for c in data.get("current_entity_index_candidates", [])}
|
|
for addr_s, e in data.get("globals", {}).items():
|
|
if e.get("kind") == "record-table" and e.get("stride_candidates"):
|
|
excl.add(int(addr_s, 16))
|
|
return excl
|
|
|
|
|
|
def assign_category(ev: dict) -> tuple[str, str]:
|
|
"""(category, confidence) from evidence. Never 'high' -- auto-shape."""
|
|
addr = int(ev["address"], 16)
|
|
consts = set(ev["consts"])
|
|
domain_bool = consts <= {0, 1}
|
|
enum_like = len(consts) >= 3 and (max(consts) if consts else 0) <= 32
|
|
has_prog_writer = bool(ev["writers_progression"])
|
|
scene_only_writers = bool(ev["writers"]) and not has_prog_writer
|
|
if addr in KNOWN_UI_TOGGLES or (ev["near_universal"] and domain_bool):
|
|
cat = "ui-toggle"
|
|
elif enum_like and has_prog_writer:
|
|
cat = "story-flag" # chapter-like (0x3234)
|
|
elif has_prog_writer and ev["reach_total"] >= 1:
|
|
cat = "story-flag"
|
|
elif not ev["writers"] and ev["reach_scenes"] >= 3 and domain_bool:
|
|
cat = "story-flag" # externally/natively set form-like (0xa57)
|
|
elif scene_only_writers:
|
|
cat = "choice-output"
|
|
elif ev["reach_scenes"] >= 2 or ev["reach_total"] >= 2:
|
|
cat = "story-flag"
|
|
else:
|
|
cat = "unknown"
|
|
conf = "med" if (has_prog_writer and ev["reach_total"] >= 3) or ev["reach_scenes"] >= 10 else "low"
|
|
return cat, conf
|
|
|
|
|
|
def mine() -> dict[int, dict]:
|
|
reach_total = collections.defaultdict(set)
|
|
reach_scenes = collections.defaultdict(set)
|
|
consts = collections.defaultdict(set)
|
|
writers = collections.defaultdict(set)
|
|
atypes = collections.defaultdict(set)
|
|
for name, path in paths.scripts().items():
|
|
try:
|
|
scr = sys4load.load(path)
|
|
except Exception:
|
|
continue
|
|
is_scene = bool(SCENE_RE.match(name))
|
|
for ins in scr.instructions:
|
|
op, a = ins.opcode, ins.args
|
|
if op in CMP_OPS or op in LOGIC_OPS:
|
|
idxs = [1, 2]
|
|
elif op == JCC_OP:
|
|
idxs = [0]
|
|
else:
|
|
idxs = []
|
|
for i in idxs:
|
|
if i < len(a) and a[i][0] in GLOBAL_ATYPES:
|
|
addr = a[i][1]
|
|
reach_total[addr].add(name)
|
|
atypes[addr].add(a[i][0])
|
|
if is_scene:
|
|
reach_scenes[addr].add(name)
|
|
for j in idxs: # compared-against immediates
|
|
if j != i and j < len(a) and a[j][0] == 0:
|
|
consts[addr].add(a[j][1])
|
|
if op == ASSIGN_OP and len(a) >= 1 and a[0][0] in GLOBAL_ATYPES:
|
|
writers[a[0][1]].add(name)
|
|
excl = _excluded_addrs()
|
|
out: dict[int, dict] = {}
|
|
for addr in reach_total:
|
|
if addr in excl:
|
|
continue
|
|
w = writers.get(addr, set())
|
|
w_prog = {n for n in w if not SCENE_RE.match(n)}
|
|
ev = {"address": f"0x{addr:x}",
|
|
"reach_total": len(reach_total[addr]),
|
|
"reach_scenes": len(reach_scenes.get(addr, set())),
|
|
"consts": sorted(consts.get(addr, set())),
|
|
"writers": sorted(w),
|
|
"writers_progression": sorted(w_prog),
|
|
"atypes": sorted(atypes[addr]),
|
|
"near_universal": len(reach_scenes.get(addr, set())) >= NEAR_UNIVERSAL}
|
|
ev["category"], ev["confidence"] = assign_category(ev)
|
|
out[addr] = ev
|
|
return out
|
|
|
|
|
|
def write_candidates(cands: dict[int, dict]) -> Path:
|
|
ranked = sorted(cands.values(), key=lambda e: (-e["reach_scenes"], -e["reach_total"]))
|
|
paths.BUILD.mkdir(parents=True, exist_ok=True)
|
|
p = paths.BUILD / "story-flags-candidates.json"
|
|
p.write_text(json.dumps({"meta": {"note": "static branch-condition mining; review surface, "
|
|
"not ground truth", "count": len(ranked)},
|
|
"candidates": ranked}, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8")
|
|
return p
|
|
|
|
|
|
import globals_build as _gb
|
|
|
|
|
|
def skeleton_toml(ev: dict) -> str:
|
|
consts = ", ".join(str(c) for c in ev["consts"][:8])
|
|
domain = "{0,1}" if set(ev["consts"]) <= {0, 1} and ev["consts"] else (f"one of {{{consts}}}" if consts else "?")
|
|
usage = (f"TODO: confirm. Branch-read in {ev['reach_scenes']} scenes / {ev['reach_total']} scripts; "
|
|
f"compared against [{consts}]; "
|
|
f"writers={ev['writers'][:4] or 'none (external/native?)'}.")
|
|
lines = ["[[global]]",
|
|
f'address = "{ev["address"]}"',
|
|
'name = ""',
|
|
f'category = "{ev["category"]}"',
|
|
'type = "int"',
|
|
f'value_domain = "{domain}"',
|
|
f'usage = "{usage}"',
|
|
'source = "auto-shape"',
|
|
f'confidence = "{ev["confidence"]}"',
|
|
"depends_on = []"]
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def bootstrap(cands: dict[int, dict], toml_path=None) -> int:
|
|
toml_path = Path(toml_path) if toml_path else (paths.VM_MAP / "globals.toml")
|
|
present = set(_gb.load_toml(toml_path)[0]) if toml_path.exists() else set()
|
|
# Seed only high-signal candidates worth hand-curating: story-flag / choice-output at med
|
|
# confidence that are NOT near-universal ADV-chrome (those 297-scene globals are engine
|
|
# scratch, not story state). The low-confidence long tail stays in the candidates JSON.
|
|
seedable = {a: e for a, e in cands.items()
|
|
if e["category"] in ("story-flag", "choice-output")
|
|
and e["confidence"] == "med" and not e["near_universal"]
|
|
and a not in present}
|
|
if not seedable:
|
|
print(f"bootstrap: nothing new to add ({len(present)} already present).")
|
|
return 0
|
|
blocks = [skeleton_toml(seedable[a]) for a in sorted(seedable)]
|
|
with toml_path.open("a", encoding="utf-8") as f:
|
|
f.write("\n" + "\n".join(blocks))
|
|
print(f"bootstrap: appended {len(blocks)} skeletons -> {toml_path}")
|
|
return 0
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--bootstrap", action="store_true") # implemented in Task 5
|
|
args = ap.parse_args(argv)
|
|
cands = mine()
|
|
if args.bootstrap:
|
|
return bootstrap(cands) # Task 5
|
|
p = write_candidates(cands)
|
|
story = sum(1 for e in cands.values() if e["category"] == "story-flag")
|
|
print(f"mined {len(cands)} branch-read globals ({story} story-flag candidates) -> {p}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|