diff --git a/.claude/skills/world-building/references/schema.md b/.claude/skills/world-building/references/schema.md index 276c63c..4d58b5b 100644 --- a/.claude/skills/world-building/references/schema.md +++ b/.claude/skills/world-building/references/schema.md @@ -142,7 +142,7 @@ build failure. A guarded fisherman who warms up and, if trusted, names who really owns the boats. ```yaml -id: person.old-teague +id: person.specimen-teague type: person status: canon secrecy: 0 @@ -153,37 +153,37 @@ body: > ``` ```yaml -id: fact.foreign-coin-on-the-docks +id: fact.specimen-foreign-coin type: fact status: canon secrecy: 2 -related: [place.specimen-wharf, person.old-teague] +related: [place.specimen-wharf, person.specimen-teague] body: > The dock hands are paid in pass-country coin, not town mint. Someone outside the charter is buying the wharf's silence, a little at a time. ``` ```yaml -id: npc.old-teague +id: npc.specimen-teague type: person status: canon start_disposition: cold -related: [person.old-teague, fact.foreign-coin-on-the-docks] +related: [person.specimen-teague, fact.specimen-foreign-coin] body: > - Same man as person.old-teague; his interactive layer. Curt with strangers, not + Same man as person.specimen-teague; his interactive layer. Curt with strangers, not from malice but from a lifetime of watching talkers end up face-down in the river. Warms slowly, and when he does the dryness turns to something almost fond. knows: - - {fact: fact.foreign-coin-on-the-docks, gate: warm} + - {fact: fact.specimen-foreign-coin, gate: warm} disposition_notes: > Starts cold. Buying his catch, not his story, is what moves him. At warm he names the coin; he never speculates aloud about who mints it. ``` -Note: `person.old-teague` (secrecy 0, public) and `npc.old-teague` (the layer, +Note: `person.specimen-teague` (secrecy 0, public) and `npc.specimen-teague` (the layer, persona server-only) are two entries. The fact is secrecy 2, gated at `warm`. ## Hand-authored JSON diff --git a/client/scripts/content/content_db.gd b/client/scripts/content/content_db.gd index afa2e07..6752e4c 100644 --- a/client/scripts/content/content_db.gd +++ b/client/scripts/content/content_db.gd @@ -39,7 +39,8 @@ func _load_dir(dir_path: String) -> Dictionary: var out: Dictionary = {} var dir := DirAccess.open(dir_path) if dir == null: - push_error("content dir not found: %s" % dir_path) + # A missing content dir means "no content of this kind authored yet", + # not an error — e.g. content/world/topics/ before any gated content exists. return out for file in dir.get_files(): if not file.ends_with(".json"): diff --git a/client/tests/unit/test_content_db.gd b/client/tests/unit/test_content_db.gd index 19eed5a..a1b3c42 100644 --- a/client/tests/unit/test_content_db.gd +++ b/client/tests/unit/test_content_db.gd @@ -52,17 +52,25 @@ func test_null_quest_ref_resolves(): assert_eq(db.unresolved_refs(o), []) -func test_loads_canon_and_topics(): - assert_true(db.has_canon("town.duncarrow")) - assert_true(db.has_canon("place.the-white-antlers")) - assert_true(db.has_topic("rumor.travelers-go-missing")) +func test_loads_canon(): + assert_true(db.has_canon("faction.the-seven")) + assert_true(db.has_canon("person.the-warden")) + assert_true(db.has_canon("rule.disposition-ladder")) -func test_topic_skeleton_has_no_body(): - var t: Dictionary = db.topic("secret.crell-runs-slave-trade") - assert_false(t.has("body")) # bodies are server-only +func test_no_loaded_topic_has_a_body(): + # Bodies are server-only (secrecy≥1 knowledge routes to content/server/topics, + # never the client). Structural, so it stays a real guard even when the + # topic set is empty (as it is now — the last authored topics were purged) + # and automatically starts testing something the moment a topic exists. + # One assertion, always executed (not per-loop-item), so an empty topic + # set still exercises the check instead of asserting zero times. + var leaked: Array = [] + for id in db.topics: + if db.topics[id].has("body"): + leaked.append(id) + assert_eq(leaked, [], "topic(s) leaked a body to the client") func test_legacy_npcs_still_load(): assert_true(db.has_npc("fenn")) - assert_true(db.has_npc("npc.mera-fenn")) diff --git a/content/lore/canon-roadmap.md b/content/lore/canon-roadmap.md index acf8ebf..2e9d168 100644 --- a/content/lore/canon-roadmap.md +++ b/content/lore/canon-roadmap.md @@ -25,6 +25,14 @@ order the Margreave is being written. Source of truth is `content/lore/*.md`; example. Not game canon. The Duncarrow scaffolding (Crell chain, Mera Fenn) is deleted. +## Lesson from the Duncarrow purge + +Generated content under `content/world/` has a client-side consumer: +`client/scripts/content/content_db.gd` (loaded via `client/scripts/harness/npc_harness.gd`) +and its GUT tests in `client/tests/unit/test_content_db.gd`. Deleting generated +content — a whole namespace of topics/npcs/canon — is a **client change**, not +just a content change; check `client/` before purging. + ## Pending (not yet authored) - Per-town knowledge that grounds the cosmology: possessed NPCs, blood-harvest diff --git a/tools/content_build/tests/test_real_content.py b/tools/content_build/tests/test_real_content.py new file mode 100644 index 0000000..a2efe07 --- /dev/null +++ b/tools/content_build/tests/test_real_content.py @@ -0,0 +1,16 @@ +"""Smoke gate: the real Margreave bible builds clean. Local mirror of the CI +content-build workflow's `--check` step — asserts freshness/validity only, +never specific entities (that's what made the old Duncarrow-era test brittle).""" + +from pathlib import Path + +from content_build.__main__ import check + +REPO_ROOT = Path(__file__).parents[3] + + +def test_real_bible_check_is_clean(): + lore = REPO_ROOT / "content" / "lore" + world = REPO_ROOT / "content" / "world" + server = REPO_ROOT / "content" / "server" # absent today — check() must tolerate that + assert check(lore, world, server) == 0