# Canon Log — living contract The canon log is the compact structured state code maintains and injects into every AI call (charter §11). This is the **contract** the Godot client and the FastAPI proxy both bind to. The schemas are the single source of truth; this doc is the human-readable companion. - Schemas (authoritative): [`/docs/schemas/canon-log.schema.json`](schemas/canon-log.schema.json), [`/docs/schemas/origin.schema.json`](schemas/origin.schema.json) - Design rationale: [`superpowers/specs/2026-07-09-canon-log-schema-design.md`](superpowers/specs/2026-07-09-canon-log-schema-design.md) ## The three layers | Layer | Owner | Where | Mutable? | |---|---|---|---| | World content | authored | `/content/world` (id-referenced) | no — static per playthrough | | Origin seed | authored | `/content/origins` | no — an initial-state overlay | | **Canon log** | **code (client)** | runtime + save | **yes — evolves every turn** | The client **constructs** the canon log at new-game (origin + world + character creation) and **maintains** it turn to turn. The proxy never mutates it — it **validates** every posted log and rejects an invalid one before any prompt work. ## Canon log shape Full field rules live in `canon-log.schema.json`. Summary: ```json { "schema_version": 1, "player": { "name": "…", "race_id": "human|elf|dwarf|beastfolk", "calling_id": "sellsword|reaver|cutpurse|trapper|hedge_mage|bonesetter|bloodsworn", "luck_descriptor": "…" }, "location": { "id": "…", "name": "…" }, "party": [ { "id": "…", "name": "…", "disposition": -100..100 } ], "recent_events": ["… (≤5, rolling)"], "established_facts": ["… (durable, uncapped)"], "active_quests": [ { "id": "…", "name": "…", "status": "active|complete|failed", "objective": "…" } ], "humiliations": [ { "id": "…", "text": "…", "weight": 1..10, "turn": 0.. } ] } ``` **The §7 boundary (enforced structurally).** The schema sets `additionalProperties: false` on the root and on `player`, and `player` admits only `name`, `race_id`, `calling_id`, `luck_descriptor`. `race_id` and `calling_id` are there so the Narrator and NPCs can *describe* the player (a "beastfolk cutpurse" reads different scene text than a "dwarf bonesetter") — they are identity for prose, not mechanics. **Numeric Luck, stats, HP/MP, and inventory cannot be represented in the canon log** — the mechanical sheet stays in `GameState` and never reaches the log, and the AI never sees a number it could use to calculate Luck. Only `luck_descriptor` (e.g. "Fortune spits on you") crosses the boundary. All string ids match `^[a-z0-9_]+$`. ## Origin seed shape Full rules in `origin.schema.json`. An origin seeds the initial log: ```json { "schema_version": 1, "id": "…", "display_name": "…", "description": "…", "start_location_id": "…", "situation": ["…"], "opening_facts": ["…"], "disposition_overrides": { "": -100..100 }, "inventory_grants": [ { "item_id": "…", "qty": 1.. } ], "start_quest_id": "…|null", "build_constraints": { "allowed_callings": ["sellsword",…], "luck_modifier": 0 } } ``` Every id an origin references must resolve in world content — the proxy exposes `content.unresolved_refs(origin, world)` for that check; `humiliations` are never seeded (earned in play). ## API contract The client posts game state to a role endpoint; the body is: ```json { "canon_log": { …a canon log… } } ``` Endpoints (charter §4): `POST /dm/narrate`, `/dm/adjudicate`, `/dm/improvise`, `/npc/speak`, `/party/banter`. Each validates `canon_log` and, on success, returns the role's result. (Prompt routing is stubbed for now — a valid log returns `{"detail": "not implemented"}`.) ### Error shape — ONE envelope for every 422 Whether the log fails the JSON Schema **or** the request body is malformed at the pydantic layer (missing/mistyped `canon_log`), the proxy returns the **same** shape, so the client parses one thing: ```json HTTP 422 { "detail": { "canon_log_errors": ["", "", …] } } ``` Client rule: on 422, read `detail.canon_log_errors` (a list of strings). It is never the raw pydantic error list. A degraded-DM fallback (charter §13) should fire on any non-200 rather than surfacing these strings to the player — they are for logs and development. ## Per-role injection The canon log is the shared base of every call; roles add their own extras (the log never grows per-role fields): | Role | Log + … | |---|---| | Narrator | log only | | NPC | log + persona, `knowledge[]`, `available_moves[]` (§6) | | Improviser | log + the whitelisted event-change set (§7) | | Banter | log, with `humiliations` as primary source (§9) | | Adjudicator | log + the current legal action set |