93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
"""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))
|