- --check now detects orphaned/stale build artifacts in build-owned dirs (client canon/ + topics/ fully owned; client npcs/ shared with legacy hand-authored files, only npc.* ids are treated as build-owned) - resolve() enforces the schema rule that canon entities must be secrecy 0 - resolve() and ladder_rungs() guard against malformed value TYPES (non-int secrecy, non-list related/rungs) raising BuildError instead of crashing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""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 json
|
|
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 _orphans(root, expected_relpaths: set, is_client: bool) -> list[str]:
|
|
"""Build-owned .json files on disk that the build no longer produces.
|
|
Client npcs/ is shared with legacy hand-authored files (bare ids), so there
|
|
only generated npc.* files are treated as build-owned."""
|
|
owned = ["canon", "topics", "npcs"] if is_client else ["npcs", "topics"]
|
|
out: list[str] = []
|
|
for sub in owned:
|
|
d = Path(root) / sub
|
|
if not d.is_dir():
|
|
continue
|
|
for f in sorted(d.glob("*.json")):
|
|
relpath = f"{sub}/{f.name}"
|
|
if relpath in expected_relpaths:
|
|
continue
|
|
if is_client and sub == "npcs":
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
except (OSError, ValueError):
|
|
continue
|
|
if not str(data.get("id", "")).startswith("npc."):
|
|
continue # legacy hand-authored file — not build-owned
|
|
out.append(f"orphan {f}")
|
|
return out
|
|
|
|
|
|
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)
|
|
stale += _orphans(world_dir, set(client_files.keys()), is_client=True)
|
|
stale += _orphans(server_dir, set(server_files.keys()), is_client=False)
|
|
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())
|