diff --git a/tools/story_flags.py b/tools/story_flags.py index b2e9c3c..f43e4cc 100644 --- a/tools/story_flags.py +++ b/tools/story_flags.py @@ -126,6 +126,44 @@ def write_candidates(cands: dict[int, dict]) -> Path: 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() + # only story-flag / ui-toggle / choice-output candidates are worth seeding for curation + seedable = {a: e for a, e in cands.items() + if e["category"] in ("story-flag", "ui-toggle", "choice-output") 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 diff --git a/tools/test_globals.py b/tools/test_globals.py index 8b0f483..f7bc9de 100644 --- a/tools/test_globals.py +++ b/tools/test_globals.py @@ -58,11 +58,35 @@ def test_miner_finds_known_flags(): check(cands[0xa57]["reach_scenes"] >= 70, "0xa57 high scene reach") check(cands[0xa57]["category"] == "story-flag", "0xa57 classified story-flag") +def test_bootstrap_is_additive_and_idempotent(): + import tempfile, pathlib, story_flags + # start from a copy of the real toml so curated entries are present + src = (paths.VM_MAP / "globals.toml").read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as d: + tp = pathlib.Path(d) / "globals.toml" + tp.write_text(src, encoding="utf-8") + before, _ = G.load_toml(tp) + cands = story_flags.mine() + story_flags.bootstrap(cands, toml_path=tp) + after, _ = G.load_toml(tp) + check(len(after) > len(before), "bootstrap adds new skeleton entries") + check(after[0xa57]["name"] == "lily_form_a", "bootstrap preserves curated 0xa57") + # idempotent: second run adds nothing + n1 = len(after) + story_flags.bootstrap(story_flags.mine(), toml_path=tp) + after2, _ = G.load_toml(tp) + check(len(after2) == n1, "second bootstrap is a no-op (idempotent)") + # every skeleton is auto-shape and not high-confidence + added = set(after) - set(before) + check(all(after[a]["source"] == "auto-shape" for a in added), "skeletons are source=auto-shape") + check(all(after[a]["confidence"] != "high" for a in added), "skeletons never high confidence") + if __name__ == "__main__": test_load_and_lint() test_lint_catches_bad_vocab() test_merge_precedence() test_sys4load_labels_from_registry() test_miner_finds_known_flags() + test_bootstrap_is_additive_and_idempotent() print(f"\n{len(FAILS)} failures") sys.exit(1 if FAILS else 0)