feat(content): POC deserter origin + world fixtures with id-resolution check

This commit is contained in:
2026-07-09 12:37:29 -05:00
parent e80a4071f6
commit 4aa65c9d4c
9 changed files with 127 additions and 0 deletions

50
api/app/content.py Normal file
View File

@@ -0,0 +1,50 @@
"""Load authored world content and cross-check origin references.
Language-neutral content integrity: every id an origin references (start
location, start quest, granted items, seeded npc dispositions) must resolve in
world content. New-game construction (client-side, Plan B) relies on this
holding; catching it here fails a broken origin at authoring time, not three
scenes into play.
"""
import json
from pathlib import Path
def _load_ids(dir_path: Path) -> set[str]:
ids: set[str] = set()
for f in dir_path.glob("*.json"):
with open(f) as fh:
ids.add(json.load(fh)["id"])
return ids
def load_world(content_root: Path) -> dict[str, set[str]]:
world = content_root / "world"
return {
"locations": _load_ids(world / "locations"),
"npcs": _load_ids(world / "npcs"),
"quests": _load_ids(world / "quests"),
"items": _load_ids(world / "items"),
}
def load_origin(path: Path) -> dict:
with open(path) as f:
return json.load(f)
def unresolved_refs(origin: dict, world: dict[str, set[str]]) -> list[str]:
missing: list[str] = []
if origin["start_location_id"] not in world["locations"]:
missing.append(f"location:{origin['start_location_id']}")
quest = origin.get("start_quest_id")
if quest is not None and quest not in world["quests"]:
missing.append(f"quest:{quest}")
for grant in origin["inventory_grants"]:
if grant["item_id"] not in world["items"]:
missing.append(f"item:{grant['item_id']}")
for npc_id in origin["disposition_overrides"]:
if npc_id not in world["npcs"]:
missing.append(f"npc:{npc_id}")
return missing

View File

@@ -0,0 +1,40 @@
import copy
import json
from pathlib import Path
from app.canon_log import validate_origin
from app.content import load_world, load_origin, unresolved_refs
REPO_ROOT = Path(__file__).resolve().parents[2]
CONTENT_ROOT = REPO_ROOT / "content"
DESERTER = CONTENT_ROOT / "origins" / "deserter.json"
def test_deserter_origin_matches_schema():
assert validate_origin(load_origin(DESERTER)) == []
def test_deserter_ids_all_resolve():
world = load_world(CONTENT_ROOT)
assert unresolved_refs(load_origin(DESERTER), world) == []
def test_broken_location_ref_is_detected():
world = load_world(CONTENT_ROOT)
origin = copy.deepcopy(load_origin(DESERTER))
origin["start_location_id"] = "nowhere"
assert "location:nowhere" in unresolved_refs(origin, world)
def test_broken_item_ref_is_detected():
world = load_world(CONTENT_ROOT)
origin = copy.deepcopy(load_origin(DESERTER))
origin["inventory_grants"].append({"item_id": "ghost_blade", "qty": 1})
assert "item:ghost_blade" in unresolved_refs(origin, world)
def test_null_quest_ref_resolves():
world = load_world(CONTENT_ROOT)
origin = copy.deepcopy(load_origin(DESERTER))
origin["start_quest_id"] = None
assert unresolved_refs(origin, world) == []