"""Route each canon entry's fields into the client tree (content/world) and the server-only tree (content/server), per the schema's role split. build_trees is pure (returns dicts); write_trees does the I/O; check_secrecy is the after-routing defense-in-depth lint.""" import json from pathlib import Path from .errors import BuildError from .model import Entry from .resolve import World from .rungs import LADDER_ID def dump_json(obj) -> str: return json.dumps(obj, indent=2, sort_keys=True) + "\n" def _client_relpath(e: Entry) -> str: if e.is_npc_layer: return f"npcs/{e.slug}.json" if e.is_knowledge: return f"topics/{e.slug}.json" return f"canon/{e.slug}.json" def _client_record(e: Entry) -> dict: if e.is_npc_layer: return { "id": e.id, "type": e.type, "start_disposition": e.start_disposition, "related": e.related, "knows": [{"fact_id": k.fact_id, "gate": k.gate} for k in e.knows], } if e.is_knowledge: return {"id": e.id, "type": e.type, "related": e.related} rec = {"id": e.id, "type": e.type, "related": e.related} if e.body is not None: rec["body"] = e.body if e.id == LADDER_ID and e.rungs: rec["rungs"] = e.rungs return rec def _server_entry(e: Entry): """Return (relpath, record) for the server tree, or None for canon entities (their bodies are public and ship client-side).""" if e.is_npc_layer: rec = {"id": e.id, "persona": e.body} if e.disposition_notes is not None: rec["disposition_notes"] = e.disposition_notes return f"npcs/{e.slug}.json", rec if e.is_knowledge: return f"topics/{e.slug}.json", {"id": e.id, "body": e.body, "secrecy": e.secrecy} return None def build_trees(world: World) -> tuple[dict, dict]: client: dict[str, dict] = {} server: dict[str, dict] = {} for e in world.entries: if e.status != "canon": continue client[_client_relpath(e)] = _client_record(e) se = _server_entry(e) if se is not None: relpath, rec = se server[relpath] = rec return client, server def check_secrecy(client_files: dict, world: World) -> None: for relpath, rec in client_files.items(): if "body" in rec and world.by_id[rec["id"]].secrecy >= 3: e = world.by_id[rec["id"]] raise BuildError( f"secrecy {e.secrecy} body routed to client artifact {relpath}", source=e.source, entry_id=e.id) def write_trees(client_files: dict, server_files: dict, world_dir: Path, server_dir: Path) -> None: for relpath, obj in client_files.items(): p = Path(world_dir) / relpath p.parent.mkdir(parents=True, exist_ok=True) p.write_text(dump_json(obj)) for relpath, obj in server_files.items(): p = Path(server_dir) / relpath p.parent.mkdir(parents=True, exist_ok=True) p.write_text(dump_json(obj))