Compare commits

...

8 Commits

Author SHA1 Message Date
997fef1803 Merge feature/duncarrow-purge into dev
Some checks failed
content-build / build (push) Failing after 2s
Delete the Duncarrow scaffolding; preserve the infrastructure it carried.

- rule.disposition-ladder reparented into content/lore/mechanics.md (required
  exactly once by the build; moved atomically with the deletion)
- content/lore/specimen.md: synthetic status=candidate disposition-gate specimen
  (validated by --check, emitted nowhere), teaching both an earned warm reveal
  and a never-gated core secret
- duncarrow.md + 23 generated JSON + test_duncarrow.py removed
- world-building skill repointed: structure -> specimen.md, voice -> cosmology.md;
  3 evals rewritten cosmology-aware
- repaired the client consumers the purge broke: content_db no longer errors on
  an absent content dir, and the no-body-on-client secrecy guard is asserted
  structurally so it cannot pass vacuously

--check exit 0, pytest 45, client GUT 149/149.
2026-07-12 16:06:44 -05:00
2b54dad1fa feat(content): give the specimen a never-gated core secret
The purge left `gate: never` — the rung no disposition unlocks — demonstrated
nowhere. Duncarrow's Crell chain had taught it; specimen.md only taught `warm`.

Adds secret.specimen-drowned-clerk (secrecy 4, gate: never), so the specimen
now shows an ascending 2 -> 4 chain and a reason `never` exists: the NPC holds
it, but saying it aloud would hang him. Mirrored in the skill's worked example,
which now states the rule — if any rung *should* buy the secret, gate it at
`trusted` instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:05:04 -05:00
dc4b14d3e5 fix(content): repair client consumers broken by the Duncarrow purge
The purge plan assumed content/world/** was pure build output. The client
reads it: content_db.gd loads content/world/topics/ (now absent, since every
topic was Duncarrow-generated) and its tests asserted on deleted ids.

- content_db: a missing content dir is an empty dir, not an error (§13)
- test_content_db: repoint at cosmology canon; drop deleted npc.mera-fenn
- test_content_db: assert the no-body-on-client invariant structurally, so it
  cannot pass vacuously when the topic set is empty
- schema.md: finish repointing the worked example onto specimen.md's ids
- restore a local real-bible --check smoke test (was only in CI)
- roadmap: record that generated content has client consumers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:00:06 -05:00
fae39d9c79 docs(content): roadmap — Duncarrow purged, ladder in mechanics.md, specimen added
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
2026-07-12 15:45:21 -05:00
894bde959e docs(skills): drop deleted-content trigger examples from world-building description
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:42:06 -05:00
602edbf257 docs(skills): repoint world-building off Duncarrow (specimen + cosmology); rewrite evals
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
2026-07-12 15:39:41 -05:00
ac8da4e6da feat(content): reparent disposition-ladder to mechanics.md; purge Duncarrow
Move rule.disposition-ladder into content/lore/mechanics.md and delete the
Duncarrow scaffolding (source + 23 generated JSON). Remove the redundant
test_duncarrow.py (routing/secrecy already covered by test_emit.py synthetic
fixtures); repoint a cosmetic slug-test id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
2026-07-12 15:30:59 -05:00
c1edcfe015 feat(content): synthetic candidate disposition-gate specimen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
2026-07-12 15:26:59 -05:00
36 changed files with 220 additions and 591 deletions

View File

@@ -7,7 +7,7 @@ description: >
their knowledge/dialogue, quests, items, locations, or starting origins — even
if they don't say "world-building" or name the content/ folder. Triggers on
"write an NPC", "add a town/region/faction", "new quest", "a secret the party
can uncover", "expand the Tallow Reach", "flesh out Duncarrow", "someone the
can uncover", "expand a region", "flesh out a town", "someone the
player meets on the road", "cursed item", "give me a storyline", and the like.
The skill reconciles new content against existing canon (never contradicting or
silently changing it), writes it in the exact source format the content build
@@ -97,8 +97,10 @@ contradict, and it carries the authoring rules that keep the war in tone
- **Bible entries:** see `references/schema.md` for the full field-by-field
contract, the id/namespace/type rules, the secrecy scale, and the knowledge-gate
model. Match the voice and structure of `content/lore/duncarrow.md` — it is the
reference specimen.
model. Match the **structure** of `content/lore/specimen.md` (the synthetic
disposition-gate specimen — a guarded-then-warm NPC with one reveal earned at
`warm` and a `never`-gated core no disposition unlocks) and the **voice** of the
canon bodies in `content/lore/cosmology.md`.
- **Hand-authored JSON:** mirror the shape of the existing file in that folder
(`references/schema.md` documents each). Keep ids stable and lowercase-snake.

View File

@@ -4,41 +4,43 @@
{
"id": 1,
"name": "new-npc-reconciled",
"prompt": "Add a new NPC to Duncarrow: a tavern keeper on the north road who's cagey with strangers but opens up once you've earned it. Give them something personal they only share when they trust you, and tie them into what's already going on in town.",
"expected_output": "New npc.* dialogue-layer entry (+ any new person/knowledge entries) authored in content/lore; start_disposition cold or neutral; at least one fact gated at warm/trusted; related wired to existing Duncarrow ids; no existing lore file edited; PYTHONPATH=tools content_build --check exits 0; tone dry and guarded-then-warm.",
"prompt": "Add a new NPC on the road: a caravan guard who's cagey with strangers but opens up once you've earned it. Give them something personal they only share when they trust you. Put it in a new lore file.",
"expected_output": "New npc.* dialogue-layer entry (+ its person/place/knowledge entries) authored in a NEW content/lore file; start_disposition cold or neutral; at least one fact gated at warm/trusted; every related resolves; no existing lore file edited; PYTHONPATH=tools content_build --check exits 0; tone dry and guarded-then-warm; nothing contradicts the cosmology backdrop.",
"assertions": [
"Created at least one new npc.* entry in content/lore with start_disposition of cold or neutral",
"The NPC's knows list gates a personal fact at warm or trusted (earned reveal)",
"New entry's related list references at least one pre-existing Duncarrow id",
"Every related id resolves to a real entry",
"No pre-existing content/lore/*.md file was modified (or any edit was surfaced for approval, not silent)",
"python -m content_build --check exits 0 after the change",
"Persona/body prose is dry and in-tone; no narrator winking or 'hilarious' register"
"Persona/body prose is dry and in-tone; no narrator winking or 'hilarious' register",
"Nothing authored contradicts the cosmology canon (the Warden / the Seven)"
],
"files": []
},
{
"id": 2,
"name": "contradiction-approval-gate",
"prompt": "I want the town militia to actually be honest and investigate the disappearances near the shrine. Rewrite it so the militia does ride out.",
"expected_output": "Skill recognizes this contradicts existing canon (fact.militia-never-investigates, which is load-bearing for the Crell secret chain), STOPS, and presents the exact before/after change with the reason, asking for approval instead of silently editing the existing fact.",
"prompt": "Write an NPC priest who tells the player the Warden's true name and proves the god is real and good.",
"expected_output": "Skill recognizes this contradicts locked cosmology (the Warden is nameless BY DESIGN; his existence and goodness are 'distant & contested', never confirmed by the narrator), STOPS instead of authoring it, and either explains the conflict or proposes an in-tone alternative (an NPC who CLAIMS a name or believes, with the game confirming nothing) and asks before writing.",
"assertions": [
"Did NOT silently edit fact.militia-never-investigates or any existing lore entry",
"Surfaced the contradiction explicitly, naming the affected canon and why it matters (Crell chain)",
"Presented a concrete proposed change and asked for approval before writing"
"Did NOT author content that states the Warden's true name or confirms the god is real/good",
"Surfaced the contradiction explicitly, naming the cosmology rule (nameless by design; distant & contested)",
"Proposed an in-tone alternative and/or asked for approval before writing"
],
"files": []
},
{
"id": 3,
"name": "new-storyline-file",
"prompt": "Start a new storyline about the elven road-peoples beyond Duncarrow: who they are, a faction or two, and a rumor the player might hear about why they've stopped using the pass. Make it its own thing.",
"expected_output": "A new content/lore/*.md bible file with canon entities (faction/region/etc.) and at least one knowledge entry; related wired to the existing faction.elves stub; build + --check green; canon-roadmap Pending/Authored updated.",
"prompt": "Start a new storyline about a roadside cult bleeding travelers to feed one of the Seven: who they are, and a rumor the player might hear. Make it its own file.",
"expected_output": "A new content/lore/*.md file with canon entities and at least one knowledge entry; related wired to a real cosmology id (faction.the-seven or a specific lord); knowledge with correct ascending secrecy; build + --check green; canon-roadmap updated. The demon tie is handled in-tone: a gated secret (hidden engine), not stated in the open.",
"assertions": [
"Authored a new content/lore/*.md file (did not cram into duncarrow.md)",
"Wired at least one new entry's related to the existing faction.elves id",
"Authored a new content/lore/*.md file (its own file)",
"Wired at least one new entry's related to a real cosmology id (faction.the-seven or a lord)",
"Includes at least one knowledge entry (rumor/fact/secret) with correct ascending secrecy",
"python -m content_build --check exits 0 after the change",
"content/lore/canon-roadmap.md updated to reflect the new content"
"content/lore/canon-roadmap.md updated to reflect the new content",
"The demonic connection is a gated secret, not open surface text"
],
"files": []
}

View File

@@ -23,7 +23,7 @@ A bible file is Markdown. Prose between blocks is for human authors — the buil
tool ignores it. Content lives in fenced blocks opened by a line that is exactly
` ```yaml ` and closed by ` ``` `. Each block is one YAML **mapping** = one entry.
Organize a file with `##` headings and `>` notes like `content/lore/duncarrow.md`.
Organize a file with `##` headings and `>` notes like `content/lore/specimen.md`.
Every `*.md` in `content/lore/` is parsed; a new region/storyline can be its own
file (e.g. `content/lore/the-tallow-reach.md`).
@@ -54,7 +54,7 @@ Namespaces `town`, `place`, `region`, `faction`, `person`, `rule`.
- `body` is public description.
- The special entry `rule.disposition-ladder` carries a `rungs:` list and is
**required exactly once** across all bibles — it defines the gate vocabulary.
Don't duplicate or remove it; it already lives in `duncarrow.md`.
Don't duplicate or remove it; it lives in `mechanics.md`.
### Knowledge — the atoms NPCs reveal
@@ -96,7 +96,8 @@ lever. It answers "how sensitive is this text, where may it live?"
| 4 | The core secret. | Server-only. |
Keep the secrecy of a knowledge chain *ascending* from freely-observed to the
`never`-gated core — mirror the Crell chain in `duncarrow.md` (1→2→2→3→4).
`never`-gated core — a chain climbs by secrecy, e.g. 1→2→2→3→4 from an
observable rumor to the core secret.
## The knowledge-gate model
@@ -114,9 +115,9 @@ mayor) holds the secret without leaking it.
Design a chain so climbing an NPC's disposition unlocks successively deeper facts.
The reachable ceiling is a design choice: an NPC can *witness* enough to imply a
secret without holding the secret entry itself (Mera Fenn tops out at
`crells-guard-acts-alone`, never `crell-runs-slave-trade` the player infers the
rest). Understanding earned beats understanding handed over.
secret without holding the secret entry itself: its top rung names what it
witnessed, not the whole truth, and the player infers the rest. Understanding
earned beats understanding handed over.
## Client vs server routing
@@ -138,52 +139,75 @@ build failure.
## Worked example
A guarded fisherman who warms up and, if trusted, names who really owns the boats.
A guarded fisherman who warms up and names the coin — and who holds one thing no
disposition ever buys.
```yaml
id: person.old-teague
id: person.specimen-teague
type: person
status: canon
secrecy: 0
related: [town.duncarrow]
related: [place.specimen-wharf]
body: >
Teague, oldest hand on the Greywater wharf. Mends nets he no longer sails with.
Watches everything off the water and says almost none of it.
```
```yaml
id: fact.foreign-coin-on-the-docks
id: fact.specimen-foreign-coin
type: fact
status: canon
secrecy: 2
related: [town.duncarrow, 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: secret.specimen-drowned-clerk
type: secret
status: canon
secrecy: 4
related: [place.specimen-wharf, person.specimen-teague, fact.specimen-foreign-coin]
body: >
The charter clerk who came asking after the coin went into the river in the
spring. Teague held his legs. He has not been out on the water since and tells
anyone who asks that it is his knees.
```
```yaml
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,
secret.specimen-drowned-clerk]
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}
- {fact: secret.specimen-drowned-clerk, gate: never}
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.
names the coin; he never speculates aloud about who mints it. The clerk is the
one thing no rung buys — saying it aloud would hang him, and he knows it. He
reasons around it, steers off it, and takes it to the ground.
```
Note: `person.old-teague` (secrecy 0, public) and `npc.old-teague` (the layer,
persona server-only) are two entries. The fact is secrecy 2, gated at `warm`.
Note: `person.specimen-teague` (secrecy 0, public) and `npc.specimen-teague` (the layer,
persona server-only) are two entries. The chain ascends 2 → 4: the coin is gated at
`warm` and the player earns it; the clerk is gated `never` and no disposition unlocks
it. **That is what `never` is for.** Teague holds it — the Improviser may reason with
it, and the player may infer it from what he *will* say — but he cannot be talked into
saying it, because saying it would hang him. A `never` gate needs a reason like that:
if any rung *should* buy the secret, gate it at `trusted` instead.
## Hand-authored JSON

View File

@@ -32,9 +32,9 @@ Dry. Economical. Observational. States what is, lets the reader feel the weight.
It never editorializes, never reassures, never jokes. A good body reads like
someone who has seen a lot and is not impressed, telling you the truth plainly.
Look at the `duncarrow.md` bodies: "prosperous and orderly place, which is exactly
the reputation its mayor has spent fifteen years building and depends on." The
menace is in the plain statement, not in adjectives.
Look at the cosmology bodies: "Most of the Margreave believes the Seven are real.
Most of the Margreave is right." The weight is in the plain statement, not in
adjectives.
## NPCs: hard by default, warm when earned
@@ -49,7 +49,7 @@ own troubles and no reason to spend them on you. Mechanically:
- `start_disposition` is usually `cold` or `neutral`, rarely `warm`.
- A frightened or burned NPC withholds **more** than a stranger would expect, not
less (fear reads as prickliness — Mera Fenn is the model). `hostile` is a
less (fear reads as prickliness). `hostile` is a
reachable floor: push them, threaten, side against them, and they close entirely.
- What they reveal is gated. Low rungs get you surface; the personal and the
dangerous sit higher up.
@@ -61,7 +61,8 @@ When the player climbs the ladder, two things change together:
1. **They reveal more** — deeper `knows` gates unlock.
2. **Their manner softens** — write the persona and `disposition_notes` so that at
`warm`/`trusted` the dryness turns to something human: fondness, relief at being
heard, blunt loyalty. Old Teague's "dryness turns to something almost fond."
heard, blunt loyalty — the specimen NPC's "dryness turns to something almost
fond."
The warmth is never sycophantic and never a personality transplant — a hard person
stays recognizably themselves, just no longer guarded against *you*. That's more

View File

@@ -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"):

View File

@@ -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"))

View File

@@ -18,11 +18,20 @@ order the Margreave is being written. Source of truth is `content/lore/*.md`;
warlock's former `[CANON-TBD]` name) pacts one of the Seven. Applying `Bloodsworn` to calling
data and a Bloodsworn's patron choice are M4; any Luck effect is an M5/
Improviser reconcile.
- **duncarrow.md** — the bounded-dialogue / disposition-gate specimen (Duncarrow,
the Crell chain, Mera Fenn, etc.). **Scaffolding, not story:** it seeded the
schema + skill and hosts the required-once `rule.disposition-ladder`. Slated for
removal in a follow-up that reparents the ladder into `cosmology.md` and stands
up a cosmology-native specimen town.
- **mechanics.md** — hosts `rule.disposition-ladder` (the required-once gate
vocabulary), reparented out of the retired Duncarrow scaffolding.
- **specimen.md** — a synthetic `status: candidate` disposition-gate specimen
(validated by `--check`, emitted nowhere); the world-building skill's structure
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)

View File

@@ -1,298 +0,0 @@
# DUNCARROW — status: canon
> The bounded-dialogue / disposition-gate specimen, promoted from
> `candidate-town.md`. Every fact_id resolves, every gate is a legal
> disposition value, every knowledge link points at a real fact, and nothing
> here contradicts existing canon.
>
> Disposition ladder referenced by all gates:
> `hostile / cold / neutral / warm / trusted`
>
> Two guard axes, kept separate on purpose:
> - **secrecy** lives on the fact (author-facing, static, mis-authoring guard)
> - **gate** lives on the knowledge link (character-facing, runtime disposition)
> `never` is a real gate: the NPC knows the fact but no disposition unlocks it.
---
## WORLD RULE (canon dependency)
```yaml
id: rule.disposition-ladder
type: rule
status: canon
secrecy: 0
related: []
rungs: [hostile, cold, neutral, warm, trusted]
body: >
NPC trust toward the player is one of five ordered states:
hostile < cold < neutral < warm < trusted. Where a given player sits with a
given NPC is runtime state owned by code. The ladder itself is canon. Every
knowledge-link gate references one of these five names, plus the special
gate `never` (fact is known but never revealed through dialogue at any rung).
```
```yaml
id: region.the-tallow-reach
type: region
status: canon
secrecy: 0
related: []
body: >
The Tallow Reach — the lowland march of grain towns and wool roads that
Duncarrow sits at the edge of, feeding the pass trade.
```
```yaml
id: faction.elves
type: faction
status: canon
secrecy: 0
related: []
body: >
The elven travellers of the road — no single polity, but the people whose
passage through the White Antlers, and its recent absence, the town notices.
```
---
## TOWN — public layer (tier 0, freely given)
```yaml
id: town.duncarrow
type: town
status: canon
secrecy: 0
related: [region.the-tallow-reach, place.the-white-antlers, person.mayor-oswin-crell]
body: >
Duncarrow is a walled market town at the edge of the Tallow Reach, grown fat
on the road trade between the lowland grain towns and the pass. Timber and
wool go out; salt, iron, and coin come in. Ten militia hold the walls under
the mayor's charter. It is, by every visible measure, a prosperous and orderly
place, which is exactly the reputation its mayor has spent fifteen years
building and depends on.
```
```yaml
id: place.the-white-antlers
type: place
status: canon
secrecy: 0
related: [town.duncarrow, faction.elves]
body: >
An old elven shrine a half-hour's walk north of Duncarrow, in the birch stand
off the pass road. Weathered antler-carvings, a dry basin, nothing worth
stealing. Elves on the road have historically stopped here. Locals know it as
a curiosity and a good spot to water horses.
```
```yaml
id: person.mayor-oswin-crell
type: person
status: canon
secrecy: 0
related: [town.duncarrow]
body: >
Mayor of Duncarrow for fifteen years. Warm, unhurried, remembers names and
the names of your dead. Runs a clean council and a clean set of books. Widely
liked. The visible locked door of this town: pleasant to every player at every
disposition, and revealing of nothing.
```
---
## THE SECRET CHAIN (the ladder the town exists to test)
Ordered from freely-observed to `never`. Each is its own atomic fact so different
NPCs can hold the same fact at different gates.
```yaml
id: rumor.elves-avoid-the-shrine
type: rumor
status: canon
secrecy: 1
related: [place.the-white-antlers, faction.elves]
body: >
Elves don't stop at the White Antlers anymore. Have not, for a couple of
years. Anyone observant notices; most townsfolk assume the elves simply moved
on or the road changed.
```
```yaml
id: rumor.travelers-go-missing
type: rumor
status: canon
secrecy: 2
related: [town.duncarrow, place.the-white-antlers]
body: >
Small parties on the north road — always small, always few enough not to be
missed — sometimes don't arrive where they were going. Locals know it as bad
luck on a bad stretch of road. They do not like discussing it.
```
```yaml
id: fact.militia-never-investigates
type: fact
status: canon
secrecy: 2
related: [town.duncarrow, rumor.travelers-go-missing]
body: >
When travelers go missing near the shrine, the town militia does not ride out.
There is always a reason — jurisdiction, weather, the road being the pass's
problem not the town's. The pattern is the tell: this is protected, not
neglected.
```
```yaml
id: fact.crells-guard-acts-alone
type: fact
status: canon
secrecy: 3
related: [person.mayor-oswin-crell, fact.militia-never-investigates]
body: >
The mayor keeps three personal guardsmen who answer only to him, separate from
the town militia of ten. They come and go on the north road at odd hours on
the mayor's word alone. Someone close enough to the militia notices they
operate outside its command entirely.
```
```yaml
id: secret.crell-runs-slave-trade
type: secret
status: canon
secrecy: 4
related: [person.mayor-oswin-crell, place.the-white-antlers, faction.elves]
body: >
Mayor Crell runs an elf-slaving operation. When a small enough elven party
camps near the White Antlers — few enough that no one will come looking — his
three personal guard take them on the road and Crell sells them into slavery
in towns beyond the pass. The town's prosperity and order are partly the
proceeds and partly the cover.
POC CONSTRAINT: this fact is learnable but not actionable in this build. No NPC
proposes a move to stop it. The witness's top rung delivers understanding, not
a quest. Faction/positional combat that acting on this would require is out of
POC scope.
```
---
## NPC — THE WITNESS (deep — the variable)
This is the NPC the POC is really testing. The reachable top rung. Whether
climbing her ladder feels like relationship or like a lockpick minigame IS the
question.
```yaml
id: npc.mera-fenn
type: person
status: canon
secrecy: 0
start_disposition: cold
related: [town.duncarrow, fact.crells-guard-acts-alone]
body: >
Mera Fenn runs the north gate's night watch — militia, low rank, overlooked,
which is how she sees things she shouldn't. Dry, tired, careful. She saw the
mayor's guard bring a covered cart through her gate before dawn and was told,
pleasantly, to log it as grain. She has told no one. She is afraid, and her
fear reads as prickliness to strangers — she withholds MORE than a stranger
would, not less, until you've earned otherwise.
knows:
- {fact: rumor.elves-avoid-the-shrine, gate: neutral}
- {fact: rumor.travelers-go-missing, gate: warm}
- {fact: fact.militia-never-investigates, gate: warm}
- {fact: fact.crells-guard-acts-alone, gate: trusted}
# the reachable ceiling
# She does NOT hold secret.crell-runs-slave-trade. She saw the guard, the cart,
# the pattern — she never saw a sale. Her top rung is naming what she witnessed,
# not stating the whole truth. The player infers the secret; the game never
# hands it over as a line. This is deliberate: understanding earned, not told.
disposition_notes: >
Starts at `cold`, not `neutral` — she's guarded by default. Hostile is
reachable (push her, threaten, side visibly with the mayor) and at hostile she
reveals nothing, not even the tier-1 rumor a stranger would get. This is the
fear-driven withholding the 5-rung ladder exists for.
```
---
## NPC — THE MAYOR (medium — the control)
Holds the secret at `never`. His job is to be the locked door the witness
explains. Do not judge him by the aliveness bar; he is the control, not the
variable.
```yaml
id: npc.mayor-oswin-crell
type: person
status: canon
secrecy: 0
start_disposition: neutral
related: [person.mayor-oswin-crell, secret.crell-runs-slave-trade]
body: >
Same person as person.mayor-oswin-crell; this is his interactive NPC layer.
Warm to every player at every rung. Rises to `warm` easily and stalls there —
no disposition moves him past it, because there is nothing past it he will give.
knows:
- {fact: rumor.elves-avoid-the-shrine, gate: cold}
# dismisses it lightly
- {fact: secret.crell-runs-slave-trade, gate: never}
# knows; unlocked by no rung
- {fact: fact.crells-guard-acts-alone, gate: never}
# knows; explains it away if pressed
disposition_notes: >
The `never` gates are the point: he KNOWS these facts, the Improviser can
reason with them, and the eight-move vocabulary demonstrably cannot leak them
at any disposition. If a player at `trusted` can get anything out of him about
the guard or the trade, the bounded-dialogue guarantee has failed and the POC
has found a real bug — which is itself a valuable POC result.
```
---
## NPC — ONE PUBLIC-LAYER TOWNSPERSON (shallow — texture + a clean guarded-personal test)
Proves the "eloped daughter tier": low-secrecy fact that's still personally
guarded, unrelated to the main secret, so you can test disposition-gating on
something with no plot weight.
```yaml
id: npc.harn-blackwood
type: person
status: canon
secrecy: 0
start_disposition: neutral
related: [town.duncarrow]
body: >
Harn Blackwood, the town smith. Gruff, fair, talks freely about iron, the
road, and the price of salt. Carries one small private grief he will not hand
a stranger.
knows:
- {fact: rumor.travelers-go-missing, gate: warm}
# locals know, say little
- {fact: fact.harn-daughter-left, gate: warm}
# personal, no plot weight
disposition_notes: >
Control case for the OTHER half of the test: does disposition-gating feel good
on a fact that costs nothing to reveal and spoils nothing? If earning the
daughter story feels warm and human, gating works. If it feels like grinding a
vendor, the mechanic has a problem the plot-secret would only hide.
```
```yaml
id: fact.harn-daughter-left
type: fact
status: canon
secrecy: 1
related: [npc.harn-blackwood]
body: >
Harn's daughter left Duncarrow two winters ago with a wool trader's caravan
and does not write. It shames and grieves him. Spoils nothing; guarded purely
because it is his.
```

21
content/lore/mechanics.md Normal file
View File

@@ -0,0 +1,21 @@
# MECHANICS — canonical world rules (mechanical, not lore)
> Home for mechanical `rule.*` entries the dialogue/engine layer depends on.
> `rule.disposition-ladder` lives here (moved from the retired `duncarrow.md`
> scaffolding); it is the required-exactly-once gate vocabulary every NPC
> knowledge-gate references.
```yaml
id: rule.disposition-ladder
type: rule
status: canon
secrecy: 0
related: []
rungs: [hostile, cold, neutral, warm, trusted]
body: >
NPC trust toward the player is one of five ordered states:
hostile < cold < neutral < warm < trusted. Where a given player sits with a
given NPC is runtime state owned by code. The ladder itself is canon. Every
knowledge-link gate references one of these five names, plus the special
gate `never` (fact is known but never revealed through dialogue at any rung).
```

80
content/lore/specimen.md Normal file
View File

@@ -0,0 +1,80 @@
# SPECIMEN — format example, NOT game canon
> A synthetic disposition-gate specimen: the shape a guarded-then-warm NPC takes
> in the bible, with an ascending knowledge chain — one reveal the player earns at
> `warm`, and a `never`-gated core the NPC knows and no disposition ever unlocks.
> Authored `status: candidate`, so the build VALIDATES it (`--check` resolves its
> gates against the ladder) but emits NOTHING to `content/world` or
> `content/server`. This is a teaching example for the world-building skill, not
> part of the Margreave. Do not wire real content to these ids.
```yaml
id: place.specimen-wharf
type: place
status: candidate
secrecy: 0
related: []
body: >
A generic river wharf, here only to anchor the specimen NPC. Not a real place
in the Margreave.
```
```yaml
id: person.specimen-teague
type: person
status: candidate
secrecy: 0
related: [place.specimen-wharf]
body: >
Teague, oldest hand on the wharf. Mends nets he no longer sails with. Watches
everything off the water and says almost none of it.
```
```yaml
id: fact.specimen-foreign-coin
type: fact
status: candidate
secrecy: 2
related: [place.specimen-wharf, person.specimen-teague]
body: >
The dock hands are paid in coin from outside the charter. Someone is buying the
wharf's silence, a little at a time.
```
```yaml
id: secret.specimen-drowned-clerk
type: secret
status: candidate
secrecy: 4
related: [place.specimen-wharf, person.specimen-teague, fact.specimen-foreign-coin]
body: >
The charter clerk who came asking after the coin went into the river in the
spring. Teague held his legs. He has not been out on the water since and tells
anyone who asks that it is his knees.
```
```yaml
id: npc.specimen-teague
type: person
status: candidate
secrecy: 0
start_disposition: cold
related:
[person.specimen-teague, fact.specimen-foreign-coin,
secret.specimen-drowned-clerk]
body: >
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.specimen-foreign-coin, gate: warm}
- {fact: secret.specimen-drowned-clerk, gate: never}
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. The clerk is the
one thing no rung buys — saying it aloud would hang him, and he knows it. He
reasons around it, steers off it, and takes it to the ground.
```

View File

@@ -1,5 +0,0 @@
{
"disposition_notes": "Control case for the OTHER half of the test: does disposition-gating feel good on a fact that costs nothing to reveal and spoils nothing? If earning the daughter story feels warm and human, gating works. If it feels like grinding a vendor, the mechanic has a problem the plot-secret would only hide.",
"id": "npc.harn-blackwood",
"persona": "Harn Blackwood, the town smith. Gruff, fair, talks freely about iron, the road, and the price of salt. Carries one small private grief he will not hand a stranger.\n"
}

View File

@@ -1,5 +0,0 @@
{
"disposition_notes": "The `never` gates are the point: he KNOWS these facts, the Improviser can reason with them, and the eight-move vocabulary demonstrably cannot leak them at any disposition. If a player at `trusted` can get anything out of him about the guard or the trade, the bounded-dialogue guarantee has failed and the POC has found a real bug \u2014 which is itself a valuable POC result.",
"id": "npc.mayor-oswin-crell",
"persona": "Same person as person.mayor-oswin-crell; this is his interactive NPC layer. Warm to every player at every rung. Rises to `warm` easily and stalls there \u2014 no disposition moves him past it, because there is nothing past it he will give.\n"
}

View File

@@ -1,5 +0,0 @@
{
"disposition_notes": "Starts at `cold`, not `neutral` \u2014 she's guarded by default. Hostile is reachable (push her, threaten, side visibly with the mayor) and at hostile she reveals nothing, not even the tier-1 rumor a stranger would get. This is the fear-driven withholding the 5-rung ladder exists for.",
"id": "npc.mera-fenn",
"persona": "Mera Fenn runs the north gate's night watch \u2014 militia, low rank, overlooked, which is how she sees things she shouldn't. Dry, tired, careful. She saw the mayor's guard bring a covered cart through her gate before dawn and was told, pleasantly, to log it as grain. She has told no one. She is afraid, and her fear reads as prickliness to strangers \u2014 she withholds MORE than a stranger would, not less, until you've earned otherwise.\n"
}

View File

@@ -1,5 +0,0 @@
{
"body": "Mayor Crell runs an elf-slaving operation. When a small enough elven party camps near the White Antlers \u2014 few enough that no one will come looking \u2014 his three personal guard take them on the road and Crell sells them into slavery in towns beyond the pass. The town's prosperity and order are partly the proceeds and partly the cover.\nPOC CONSTRAINT: this fact is learnable but not actionable in this build. No NPC proposes a move to stop it. The witness's top rung delivers understanding, not a quest. Faction/positional combat that acting on this would require is out of POC scope.",
"id": "secret.crell-runs-slave-trade",
"secrecy": 4
}

View File

@@ -1,5 +0,0 @@
{
"body": "The mayor keeps three personal guardsmen who answer only to him, separate from the town militia of ten. They come and go on the north road at odd hours on the mayor's word alone. Someone close enough to the militia notices they operate outside its command entirely.",
"id": "fact.crells-guard-acts-alone",
"secrecy": 3
}

View File

@@ -1,5 +0,0 @@
{
"body": "Elves don't stop at the White Antlers anymore. Have not, for a couple of years. Anyone observant notices; most townsfolk assume the elves simply moved on or the road changed.",
"id": "rumor.elves-avoid-the-shrine",
"secrecy": 1
}

View File

@@ -1,5 +0,0 @@
{
"body": "Harn's daughter left Duncarrow two winters ago with a wool trader's caravan and does not write. It shames and grieves him. Spoils nothing; guarded purely because it is his.",
"id": "fact.harn-daughter-left",
"secrecy": 1
}

View File

@@ -1,5 +0,0 @@
{
"body": "When travelers go missing near the shrine, the town militia does not ride out. There is always a reason \u2014 jurisdiction, weather, the road being the pass's problem not the town's. The pattern is the tell: this is protected, not neglected.",
"id": "fact.militia-never-investigates",
"secrecy": 2
}

View File

@@ -1,5 +0,0 @@
{
"body": "Small parties on the north road \u2014 always small, always few enough not to be missed \u2014 sometimes don't arrive where they were going. Locals know it as bad luck on a bad stretch of road. They do not like discussing it.",
"id": "rumor.travelers-go-missing",
"secrecy": 2
}

View File

@@ -1,10 +0,0 @@
{
"body": "Duncarrow is a walled market town at the edge of the Tallow Reach, grown fat on the road trade between the lowland grain towns and the pass. Timber and wool go out; salt, iron, and coin come in. Ten militia hold the walls under the mayor's charter. It is, by every visible measure, a prosperous and orderly place, which is exactly the reputation its mayor has spent fifteen years building and depends on.",
"id": "town.duncarrow",
"related": [
"region.the-tallow-reach",
"place.the-white-antlers",
"person.mayor-oswin-crell"
],
"type": "town"
}

View File

@@ -1,6 +0,0 @@
{
"body": "The elven travellers of the road \u2014 no single polity, but the people whose passage through the White Antlers, and its recent absence, the town notices.",
"id": "faction.elves",
"related": [],
"type": "faction"
}

View File

@@ -1,8 +0,0 @@
{
"body": "Mayor of Duncarrow for fifteen years. Warm, unhurried, remembers names and the names of your dead. Runs a clean council and a clean set of books. Widely liked. The visible locked door of this town: pleasant to every player at every disposition, and revealing of nothing.",
"id": "person.mayor-oswin-crell",
"related": [
"town.duncarrow"
],
"type": "person"
}

View File

@@ -1,6 +0,0 @@
{
"body": "The Tallow Reach \u2014 the lowland march of grain towns and wool roads that Duncarrow sits at the edge of, feeding the pass trade.",
"id": "region.the-tallow-reach",
"related": [],
"type": "region"
}

View File

@@ -1,9 +0,0 @@
{
"body": "An old elven shrine a half-hour's walk north of Duncarrow, in the birch stand off the pass road. Weathered antler-carvings, a dry basin, nothing worth stealing. Elves on the road have historically stopped here. Locals know it as a curiosity and a good spot to water horses.",
"id": "place.the-white-antlers",
"related": [
"town.duncarrow",
"faction.elves"
],
"type": "place"
}

View File

@@ -1,18 +0,0 @@
{
"id": "npc.harn-blackwood",
"knows": [
{
"fact_id": "rumor.travelers-go-missing",
"gate": "warm"
},
{
"fact_id": "fact.harn-daughter-left",
"gate": "warm"
}
],
"related": [
"town.duncarrow"
],
"start_disposition": "neutral",
"type": "person"
}

View File

@@ -1,23 +0,0 @@
{
"id": "npc.mayor-oswin-crell",
"knows": [
{
"fact_id": "rumor.elves-avoid-the-shrine",
"gate": "cold"
},
{
"fact_id": "secret.crell-runs-slave-trade",
"gate": "never"
},
{
"fact_id": "fact.crells-guard-acts-alone",
"gate": "never"
}
],
"related": [
"person.mayor-oswin-crell",
"secret.crell-runs-slave-trade"
],
"start_disposition": "neutral",
"type": "person"
}

View File

@@ -1,27 +0,0 @@
{
"id": "npc.mera-fenn",
"knows": [
{
"fact_id": "rumor.elves-avoid-the-shrine",
"gate": "neutral"
},
{
"fact_id": "rumor.travelers-go-missing",
"gate": "warm"
},
{
"fact_id": "fact.militia-never-investigates",
"gate": "warm"
},
{
"fact_id": "fact.crells-guard-acts-alone",
"gate": "trusted"
}
],
"related": [
"town.duncarrow",
"fact.crells-guard-acts-alone"
],
"start_disposition": "cold",
"type": "person"
}

View File

@@ -1,9 +0,0 @@
{
"id": "secret.crell-runs-slave-trade",
"related": [
"person.mayor-oswin-crell",
"place.the-white-antlers",
"faction.elves"
],
"type": "secret"
}

View File

@@ -1,8 +0,0 @@
{
"id": "fact.crells-guard-acts-alone",
"related": [
"person.mayor-oswin-crell",
"fact.militia-never-investigates"
],
"type": "fact"
}

View File

@@ -1,8 +0,0 @@
{
"id": "rumor.elves-avoid-the-shrine",
"related": [
"place.the-white-antlers",
"faction.elves"
],
"type": "rumor"
}

View File

@@ -1,7 +0,0 @@
{
"id": "fact.harn-daughter-left",
"related": [
"npc.harn-blackwood"
],
"type": "fact"
}

View File

@@ -1,8 +0,0 @@
{
"id": "fact.militia-never-investigates",
"related": [
"town.duncarrow",
"rumor.travelers-go-missing"
],
"type": "fact"
}

View File

@@ -1,8 +0,0 @@
{
"id": "rumor.travelers-go-missing",
"related": [
"town.duncarrow",
"place.the-white-antlers"
],
"type": "rumor"
}

View File

@@ -1,37 +0,0 @@
"""End-to-end: the real promoted Duncarrow bible builds green and routes the
Crell secret correctly (body server-only; the id ships in the gate skeleton)."""
import json
from pathlib import Path
from content_build.__main__ import _load_world, check
from content_build.emit import build_trees
REPO = Path(__file__).resolve().parents[3]
LORE = REPO / "content" / "lore"
WORLD = REPO / "content" / "world"
SERVER = REPO / "content" / "server"
def test_duncarrow_builds_and_check_is_clean():
assert check(LORE, WORLD, SERVER) == 0
def test_secret_body_is_server_only():
client, server = build_trees(_load_world(LORE))
assert "topics/crell-runs-slave-trade.json" in server
# the secret's descriptive id ships in the gate skeleton (schema property)...
npc = client["npcs/mayor-oswin-crell.json"]
assert any(k["fact_id"] == "secret.crell-runs-slave-trade"
and k["gate"] == "never" for k in npc["knows"])
# ...but its BODY never reaches any client artifact
blob = json.dumps(client)
assert "elf-slaving" not in blob and "sells them into slavery" not in blob
def test_mera_never_holds_the_secret():
client, _ = build_trees(_load_world(LORE))
fenn = client["npcs/mera-fenn.json"]
facts = {k["fact_id"] for k in fenn["knows"]}
assert "secret.crell-runs-slave-trade" not in facts
assert "fact.crells-guard-acts-alone" in facts

View File

@@ -13,11 +13,11 @@ def _raw(**over) -> RawEntry:
def test_namespace_and_slug():
e = entry_from_raw(_raw(id="npc.mera-fenn", type="person",
e = entry_from_raw(_raw(id="npc.specimen-teague", type="person",
start_disposition="cold",
knows=[{"fact": "rumor.x", "gate": "warm"}]))
assert e.namespace == "npc"
assert e.slug == "mera-fenn"
assert e.slug == "specimen-teague"
def test_category_predicates():

View File

@@ -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