#!/usr/bin/env python3 """Generator + linter for the living global-variable registry (vm-map/globals.toml). --build merge curated over build/global-var-map.json -> build/globals.json + docs/global-reference.md --lint run the linter, print errors/warnings, exit nonzero on errors See docs/superpowers/specs/2026-07-07-globals-registry-and-story-flags-design.md.""" from __future__ import annotations import os, sys, json, argparse, collections, tomllib from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import paths TOML_DEFAULT = paths.VM_MAP / "globals.toml" AUTO_MAP = paths.BUILD / "global-var-map.json" CATEGORIES = {"story-flag", "index-pointer", "data-table", "string-table", "ui-toggle", "choice-output", "counter", "unknown"} SOURCES = {"investigation", "harness", "inference", "auto-shape"} CONFIDENCE = {"low": 1, "med": 2, "high": 3} def _parse_addr(v) -> int: return int(v, 16) if isinstance(v, str) else int(v) def load_toml(path) -> tuple[dict[int, dict], dict]: """Return ({addr:int -> entry-dict (with _addr set)}, meta-dict).""" data = tomllib.loads(Path(path).read_text(encoding="utf-8")) out: dict[int, dict] = {} for e in data.get("global", []): addr = _parse_addr(e["address"]) e = dict(e) e["_addr"] = addr out[addr] = e return out, data.get("meta", {}) def lint(entries: dict[int, dict], all_addrs: set[int]) -> tuple[list[str], list[str]]: """Errors: bad vocabulary, auto-shape@high, dangling depends_on. Warnings: (reserved).""" errors: list[str] = [] warnings: list[str] = [] for addr, e in sorted(entries.items()): tag = f"0x{addr:x}" cat, src, conf = e.get("category"), e.get("source"), e.get("confidence") if cat not in CATEGORIES: errors.append(f"{tag}: bad category {cat!r}") if src not in SOURCES: errors.append(f"{tag}: bad source {src!r}") if conf not in CONFIDENCE: errors.append(f"{tag}: bad confidence {conf!r}") if src == "auto-shape" and conf == "high": errors.append(f"{tag}: auto-shape source may not claim high confidence") for dep in e.get("depends_on", []): if _parse_addr(dep) not in all_addrs: errors.append(f"{tag}: depends_on missing address {dep}") return errors, warnings GEN_JSON_META = {"generated_from": "vm-map/globals.toml", "note": "DO NOT EDIT -- generated by tools/globals_build.py --build"} def load_auto(path) -> dict: try: return json.loads(Path(path).read_text(encoding="utf-8")) except Exception: return {} def _auto_category(e: dict) -> str: """Best-effort category for an auto shape-map entry (unreliable; curation overrides).""" kind = e.get("kind") or "" if kind == "record-table": return "data-table" if kind == "array-1d": return "data-table" lbl = e.get("label") or "" if lbl.startswith("string-table"): return "string-table" return "unknown" def merge(curated: dict[int, dict], auto: dict) -> dict[int, dict]: out: dict[int, dict] = {} for addr_s, e in auto.get("globals", {}).items(): if not e.get("label"): continue addr = int(addr_s, 16) out[addr] = {"address": f"0x{addr:x}", "name": None, "category": _auto_category(e), "type": e.get("type"), "value_domain": None, "usage": e.get("label"), "source": "auto-shape", "confidence": e.get("confidence") or "low", "depends_on": [], "provenance": "auto"} for addr, e in curated.items(): out[addr] = {"address": f"0x{addr:x}", "name": e.get("name"), "category": e.get("category", "unknown"), "type": e.get("type"), "value_domain": e.get("value_domain"), "usage": e.get("usage", ""), "source": e.get("source", "inference"), "confidence": e.get("confidence", "low"), "depends_on": [f"0x{_parse_addr(d):x}" for d in e.get("depends_on", [])], "provenance": "curated"} return out def emit_json(merged: dict[int, dict], meta: dict) -> str: out = {"meta": {**GEN_JSON_META, **meta}, "globals": {e["address"]: e for _, e in sorted(merged.items())}} return json.dumps(out, ensure_ascii=False, indent=2) + "\n" def emit_reference_md(merged: dict[int, dict]) -> str: L = ["", "# Global Variable Reference (generated)", "", f"{len(merged)} globals ({sum(1 for e in merged.values() if e['provenance']=='curated')} curated, " f"{sum(1 for e in merged.values() if e['provenance']=='auto')} auto shape-inferred). " "Source of truth: `vm-map/globals.toml`.", ""] by_cat = collections.defaultdict(list) for addr, e in merged.items(): by_cat[e["category"]].append(addr) for cat in sorted(by_cat): L += [f"## {cat}", "", "| address | name | conf | source | usage |", "|---|---|---|---|---|"] # curated first, then by address for addr in sorted(by_cat[cat], key=lambda a: (merged[a]["provenance"] != "curated", a)): e = merged[addr] name = e["name"] or "—" usage = (e["usage"] or "").replace("|", "\\|").replace("\n", " ") L.append(f"| `{e['address']}` | {name} | {e['confidence']} | {e['source']} | {usage} |") L.append("") return "\n".join(L) + "\n" def build(tp: Path) -> int: curated, meta = load_toml(tp) auto = load_auto(AUTO_MAP) all_addrs = set(curated) | {int(a, 16) for a in auto.get("globals", {})} errors, warnings = lint(curated, all_addrs) for m in warnings: print("warn:", m) if errors: for m in errors: print("error:", m) return 1 merged = merge(curated, auto) paths.BUILD.mkdir(parents=True, exist_ok=True) (paths.BUILD / "globals.json").write_text(emit_json(merged, meta), encoding="utf-8") (paths.REPO / "docs" / "global-reference.md").write_text(emit_reference_md(merged), encoding="utf-8") print(f"build: {len(merged)} globals -> build/globals.json, docs/global-reference.md") return 0 def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("--build", action="store_true") ap.add_argument("--lint", action="store_true") ap.add_argument("--toml", default=str(TOML_DEFAULT)) args = ap.parse_args(argv) tp = Path(args.toml) if args.lint: entries, _ = load_toml(tp) errors, warnings = lint(entries, set(entries)) for m in warnings: print("warn:", m) for m in errors: print("error:", m) print(f"lint: {len(errors)} errors, {len(warnings)} warnings") return 1 if errors else 0 if args.build: return build(tp) # implemented in Task 2 ap.error("no action (expected --build/--lint)") if __name__ == "__main__": sys.exit(main())