193 lines
8.1 KiB
Python
193 lines
8.1 KiB
Python
#!/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")
|
|
columns = e.get("columns", {})
|
|
if not isinstance(columns, dict):
|
|
errors.append(f"{tag}: columns must be a table")
|
|
else:
|
|
for column, name in columns.items():
|
|
try:
|
|
column_index = int(column)
|
|
except (TypeError, ValueError):
|
|
errors.append(f"{tag}: bad column index {column!r}")
|
|
continue
|
|
if column_index < 0:
|
|
errors.append(f"{tag}: negative column index {column_index}")
|
|
if not isinstance(name, str) or not name:
|
|
errors.append(f"{tag}: column {column_index} has no semantic name")
|
|
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"}
|
|
if e.get("columns"):
|
|
out[addr]["columns"] = {
|
|
str(column): name for column, name in e["columns"].items()
|
|
}
|
|
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 = ["<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->",
|
|
"# 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", " ")
|
|
if columns := e.get("columns"):
|
|
mapping = ", ".join(
|
|
f"{column}={column_name}"
|
|
for column, column_name in sorted(
|
|
columns.items(), key=lambda item: int(item[0])
|
|
)
|
|
)
|
|
usage += f" Columns: {mapping}."
|
|
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())
|