feat(content-build): CLI build + --check staleness gate
This commit is contained in:
72
tools/content_build/__main__.py
Normal file
72
tools/content_build/__main__.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""CLI entry: `python -m content_build [--check]`. Orchestrates
|
||||||
|
parse -> model -> resolve -> emit. --check builds in memory and diffs against
|
||||||
|
the on-disk committed JSON (stale-or-invalid -> exit 1)."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .emit import build_trees, check_secrecy, dump_json, write_trees
|
||||||
|
from .errors import BuildError
|
||||||
|
from .model import entry_from_raw
|
||||||
|
from .parse import parse_bible
|
||||||
|
from .resolve import resolve
|
||||||
|
|
||||||
|
|
||||||
|
def _load_world(lore_dir: Path):
|
||||||
|
raws = []
|
||||||
|
for md in sorted(Path(lore_dir).glob("*.md")):
|
||||||
|
raws.extend(parse_bible(md))
|
||||||
|
return resolve([entry_from_raw(r) for r in raws])
|
||||||
|
|
||||||
|
|
||||||
|
def build(lore_dir, world_dir, server_dir) -> None:
|
||||||
|
world = _load_world(lore_dir)
|
||||||
|
client_files, server_files = build_trees(world)
|
||||||
|
check_secrecy(client_files, world)
|
||||||
|
write_trees(client_files, server_files, world_dir, server_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _diff(path: Path, obj) -> list[str]:
|
||||||
|
want = dump_json(obj)
|
||||||
|
if not path.exists():
|
||||||
|
return [f"missing {path}"]
|
||||||
|
if path.read_text() != want:
|
||||||
|
return [f"differs {path}"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def check(lore_dir, world_dir, server_dir) -> int:
|
||||||
|
world = _load_world(lore_dir)
|
||||||
|
client_files, server_files = build_trees(world)
|
||||||
|
check_secrecy(client_files, world)
|
||||||
|
stale: list[str] = []
|
||||||
|
for relpath, obj in client_files.items():
|
||||||
|
stale += _diff(Path(world_dir) / relpath, obj)
|
||||||
|
for relpath, obj in server_files.items():
|
||||||
|
stale += _diff(Path(server_dir) / relpath, obj)
|
||||||
|
for s in stale:
|
||||||
|
print("STALE:", s, file=sys.stderr)
|
||||||
|
return 1 if stale else 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
ap = argparse.ArgumentParser(prog="content_build")
|
||||||
|
ap.add_argument("--check", action="store_true",
|
||||||
|
help="verify committed JSON is fresh + valid; no writes")
|
||||||
|
ap.add_argument("--lore", default="content/lore")
|
||||||
|
ap.add_argument("--world", default="content/world")
|
||||||
|
ap.add_argument("--server", default="content/server")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
try:
|
||||||
|
if args.check:
|
||||||
|
return check(args.lore, args.world, args.server)
|
||||||
|
build(args.lore, args.world, args.server)
|
||||||
|
return 0
|
||||||
|
except BuildError as e:
|
||||||
|
print("BUILD FAILED:", e, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
54
tools/content_build/tests/test_cli.py
Normal file
54
tools/content_build/tests/test_cli.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from content_build.__main__ import main
|
||||||
|
|
||||||
|
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "valid"
|
||||||
|
|
||||||
|
|
||||||
|
def _lore(tmp_path):
|
||||||
|
lore = tmp_path / "lore"
|
||||||
|
lore.mkdir()
|
||||||
|
shutil.copy(FIXTURE_DIR / "world.md", lore / "world.md")
|
||||||
|
return lore
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_then_check_is_clean(tmp_path):
|
||||||
|
lore = _lore(tmp_path)
|
||||||
|
world, server = tmp_path / "world", tmp_path / "server"
|
||||||
|
argv = ["--lore", str(lore), "--world", str(world), "--server", str(server)]
|
||||||
|
assert main(argv) == 0
|
||||||
|
assert (world / "npcs" / "tess.json").exists()
|
||||||
|
assert (server / "topics" / "big-twist.json").exists()
|
||||||
|
# a fresh --check against just-written output is clean
|
||||||
|
assert main(argv + ["--check"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_detects_stale(tmp_path):
|
||||||
|
lore = _lore(tmp_path)
|
||||||
|
world, server = tmp_path / "world", tmp_path / "server"
|
||||||
|
argv = ["--lore", str(lore), "--world", str(world), "--server", str(server)]
|
||||||
|
assert main(argv) == 0
|
||||||
|
# mutate a committed artifact -> --check must fail
|
||||||
|
(world / "canon" / "testburg.json").write_text('{"id": "town.testburg"}\n')
|
||||||
|
assert main(argv + ["--check"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_detects_missing(tmp_path):
|
||||||
|
lore = _lore(tmp_path)
|
||||||
|
world, server = tmp_path / "world", tmp_path / "server"
|
||||||
|
argv = ["--lore", str(lore), "--world", str(world), "--server", str(server)]
|
||||||
|
assert main(argv) == 0
|
||||||
|
(world / "npcs" / "tess.json").unlink()
|
||||||
|
assert main(argv + ["--check"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_bible_fails_build(tmp_path):
|
||||||
|
lore = tmp_path / "lore"
|
||||||
|
lore.mkdir()
|
||||||
|
(lore / "bad.md").write_text(
|
||||||
|
"```yaml\nid: town.a\ntype: town\nstatus: canon\nsecrecy: 0\n"
|
||||||
|
"related: [does.not-exist]\nbody: x\n```\n")
|
||||||
|
argv = ["--lore", str(lore), "--world", str(tmp_path / "w"),
|
||||||
|
"--server", str(tmp_path / "s")]
|
||||||
|
assert main(argv) == 1
|
||||||
Reference in New Issue
Block a user