Replaces AI-DnD's RPG relative-delta world state with the genre-neutral typed
narrative state of ADR 010: explicit, absolute, allowlisted events proposed by
the model, validated by the application, applied to one authoritative document,
and snapshotted per position so restore stays a row read.
This commit includes the corrective pass that followed the independent review
in planning/reports/M5-IMPLEMENTATION-REPORT.md. The invariant it exists to
hold is:
visible active transcript position == stored head == authoritative state
Narrator editing (D10, STORY-BRANCH-SEMANTICS §§14-15)
A narrator edit no longer rewrites a row. It returns to the state before the
turn, takes the reader's exact text as the accepted narration, re-derives the
state that text implies, and becomes a new active continuation — while the
original narration keeps its words, its live flag and its whole future as
retained history. At the tip the correction is another take; with story below
it, it forks. No new history machinery: this is the existing fork/take/head
path with the reader's text in place of a generated reply. The §14A refusal
is therefore gone for narrator turns, and remains only for player input.
Pre-M5 positions
Migration 88 backfills the empty narrative document onto every action written
before M5, and a missing snapshot now restores the empty document instead of
leaving the previous position's state standing. Restoring to an old Save
Point no longer leaves a later position's entities and facts on screen.
Narrator context
Replayed history carries prose only; the machine-readable block is no longer
reconstructed into past turns, where it contradicted the authoritative state
in the same prompt. A fact withdrawn by a manual correction is now named as
no longer true, with the reader's reason, rather than silently dropped.
Also
- state_changes joins the action-list bulk read, removing one query per row.
- Extraction takes only the application's own protocol payload: an ordinary
```json or ```python block in a story survives, and a mangled proposal
still does not reach the reader.
Planning: ADR 013 records the authoritative document shape; §§14-15/14A, D10,
C04 and BUILD-MILESTONES are updated to describe what exists. Debt is recorded
against M8 (scenario editor UX) and M9 (export of the audit trail).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWU4gTfLYY6Qq9U7aa9Qw2
116 lines
5.1 KiB
Markdown
116 lines
5.1 KiB
Markdown
# ADR 013 — The Authoritative Narrative State Document
|
|
|
|
**Status:** Accepted; implemented in M5
|
|
**Date:** 2026-09-04
|
|
|
|
## Context
|
|
|
|
[ADR 010](010-explicit-typed-narrative-state-events.md) settled the **protocol**:
|
|
the model proposes change as explicit, typed, absolute events drawn from a fixed
|
|
allowlist, never as relative deltas. It did not settle what those events write
|
|
into.
|
|
|
|
That shape turned out to be load-bearing. The prompt renders it, the browser's
|
|
state panel groups it, export carries it, migration has to produce it for
|
|
positions that predate it, and every Undo, Redo, take switch and Save Point
|
|
restore reads a copy of it. The M5 review recommended recording it as a decision
|
|
rather than leaving it an implementation detail of `narrative/model.py`. This ADR
|
|
records what was built; it does not extend it.
|
|
|
|
## Decision
|
|
|
|
### The document
|
|
|
|
One JSON document is the campaign's authoritative account of its own story. It
|
|
is genre-neutral: nothing in it names a stat, a level, a currency or a class.
|
|
|
|
```text
|
|
entities who and what exists — people, places, things, groups.
|
|
Each carries a name, a type, aliases, a description, a status,
|
|
free-form attributes, conditions, and a current location.
|
|
facts what has been established, as subject/predicate/object.
|
|
Each carries an id, a status, and where it came from.
|
|
relationships how entities stand to one another, directionally.
|
|
possessions which entity holds which item.
|
|
threads open story threads, with a title and a status.
|
|
scene where the story is now, and a short summary of the moment.
|
|
```
|
|
|
|
Two fields appear throughout and are the reason the document can be trusted:
|
|
|
|
- **authority** — who established this: the story, or the reader's own
|
|
correction. A reader's correction outranks the narration, and the prompt says
|
|
so in words.
|
|
- **provenance** — the branch and depth the change was made at, so a fact can be
|
|
traced to the moment it entered the story.
|
|
|
|
Nothing is deleted. A fact a correction takes back is marked `invalidated`, with
|
|
the reason and the position, because a record that vanished would audit nothing.
|
|
|
|
### The pipeline
|
|
|
|
```text
|
|
model output
|
|
-> extraction the protocol block is separated from the prose
|
|
-> validation envelope, allowlist, schema, references, canon
|
|
-> validated events recorded in `state_events`, with their provenance
|
|
-> the document applied to produce the new authoritative state
|
|
-> per-position snapshot stored on the node the turn produced
|
|
```
|
|
|
|
Each stage has one job, and the order is deliberate: the allowlist is checked
|
|
before any field is read, so an unknown event type is rejected before its
|
|
contents are touched.
|
|
|
|
### Where it lives
|
|
|
|
- `adventures.narrative_state` — the current authoritative document. This is
|
|
what the narrator is told and what the reader is shown.
|
|
- `actions.narrative_state_after` — the whole document as it stood after that
|
|
position played, on **every** node.
|
|
- `state_events` / `state_proposals` — the audit: what was proposed, what was
|
|
accepted, what was refused and why.
|
|
|
|
### What each is for
|
|
|
|
**Event history is audit and provenance, not a source of truth.** Restoring a
|
|
position never replays it. This was proven by renaming the table away
|
|
mid-campaign: Undo and Redo continued to work.
|
|
|
|
**Snapshots are what make restore bounded.** Arriving at a position is a single
|
|
row read whose cost does not grow with the length of the story (ADR 012 §10.4).
|
|
A position without a snapshot is a position the head cannot be restored to, so
|
|
every node has one — including nodes written before M5, which migration 88
|
|
backfills with the empty document. A missing snapshot restores the empty
|
|
document rather than leaving the previous position's state standing.
|
|
|
|
**The document is the authority; the model only proposes.** Every event, whether
|
|
it came from the model or from a reader's correction, passes the same
|
|
validation. The model cannot write a field directly, cannot invent an event
|
|
type, and cannot refer to an entity that does not exist in this campaign.
|
|
|
|
## The invariant
|
|
|
|
```text
|
|
visible active transcript position == stored head == authoritative state
|
|
```
|
|
|
|
Everything above exists to hold this. The M5 review found two ways it had been
|
|
broken — a narrator edit that rewound live state while the head stayed at the
|
|
tip, and a restore to a migrated position that left a later position's state
|
|
standing — and both were fixed by making the rule absolute rather than by adding
|
|
a special case.
|
|
|
|
## Consequences
|
|
|
|
- The document is larger than the RPG dict it replaced, so both it and the
|
|
per-node snapshots are stored compressed.
|
|
- Genre lives in the campaign's canon and in the words the story uses, never in
|
|
the schema. A science-fiction campaign and a fantasy one produce the same
|
|
shapes.
|
|
- A reader's correction is durable and outranks narration, and the prompt states
|
|
both what holds and what has been withdrawn.
|
|
- Export carries the document, the canon and every snapshot. It does **not**
|
|
yet carry `state_events` / `state_proposals`, so an imported campaign keeps a
|
|
correction's effect but not its audit trail. Deferred to M9.
|