Files
interactive-story/planning/DECISIONS/013-authoritative-narrative-state-document.md
JesseMarkowitzandClaude Opus 5 b7005e6fdd M5: genre-neutral authoritative narrative state, with review corrections
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
2026-09-05 07:01:50 -04:00

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.