Planning: close M3 and record active-head architecture
M3's review recommended planning changes and, following the M2 pattern, reported rather than applied them. This applies them, and adds the ADR the review asked for. ADR 012 records the architecture rather than the requirement. ADR 005 already says that going backward must preserve abandoned history and that the user sees Undo/Redo/Retry rather than branch management; it names a movable active head as the direction and stops. What M3 settled is the shape: the head is stored rather than derived, every read of the story is capped at it in one place, one mechanism moves it, the state of a position comes off the node rather than from a replay, the first write below a moved-back head is the divergence, and whether Redo exists is decided by the lineage rather than by a flag that could be stale. The last of those is the property worth keeping — a flag can be wrong and make the story wrong; a lineage cannot. Two semantics are ratified in STORY-BRANCH-SEMANTICS.md, both of them reversals or narrowings that a reader would otherwise take for bugs. Undo now crosses fork points and continues to the campaign opening, because refusing at the fork was a consequence of deleting rows the parent line was also reading, and nothing is deleted any more. And the system refuses to switch which take is live while a later story is off screen, because doing it quietly would leave retained history continuing from words the story no longer says. A new §14A covers editing in place. §14-15 describe the finished behaviour — the edit becomes authoritative, the state it implies is re-evaluated, a new continuation is created, the original is retained — and that requirement is intact and explicitly not weakened here. It is also not built, because re-evaluating state from prose a user typed needs M5's extraction pass. §14A says what exists in the meantime and why refusing is the minimum that holds the invariant rather than the destination. TECHNICAL-DESIGN.md gains §8.7 and §9.1, recording the implemented model and the bundle behaviour as fact in the way §5.2 records M1 and M2. §10.4 gains a constraint that is easy to lose: the snapshot half of the hybrid state model is a requirement, not an optimization. Head movement is a row lookup plus a restore, which is why Undo, Redo and Save Point restore cost the same at any distance into a campaign; a state model recoverable only by replaying from the opening would make all three proportional to campaign length, on exactly the long campaigns this product is for. DATA-MODEL.md records the head as stored on the campaign rather than derived from its newest turn — two campaigns holding identical turns can be read at different places, and nothing about the turns can tell them apart — and the branch disposition as implemented: the depth a divergent write left the branch at, deliberately advisory, and carried through export because every row of an abandoned line is exported either way. BUILD-MILESTONES.md marks M3 complete and states the one condition still open. M4 is told a Save Point is a durable pointer and that restoring one is head movement with a bounds check, not a restore system: a second mover is the specific failure to avoid, because the two paths would silently disagree about what restore means. M5 gets three constraints — keep state efficiently recoverable, move the test instrumentation rather than the assertions when the world-state protocol goes, and finish the narrator edit §14A defers. V1-ACCEPTANCE-TESTS.md clarifies ownership without lowering a bar. D10 keeps all three pass conditions and is explicitly recorded as *not* satisfied at the end of M3; what changed is that the document now says which milestone delivers which condition. D03's result is recorded as a full pass rather than the partial the text allowed for, I07 gains the pre-M3 bundle clause, and L01 gains the note that resolves its apparent conflict with A05 — a failed turn does advance the head by one, onto the player's retained input, and that is A05 working rather than L01 failing. README.md described a different application: a hosted demo, guest accounts, cloud providers, Postgres, a Render blueprint, an analytics dashboard, a QuickJS scripting engine, and 549 tests. M2 removed all of that and the README was never updated — a gap M2's own debt table missed. It now describes what this fork is, including the endpoint policy and the TLS behaviour, and the numbers in it are the current ones. M3's report is included here as its own evidence record: no separate baseline report was produced, so it carries the raw counts and runtime observations as well as the review, and §W records this closeout. SPECIFICATION.md and SECURITY-THREAT-MODEL.md are unchanged. M3 altered no product requirement and touched no path in the threat model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QF5TcoB86QADgjHz1GZe8u
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f082b61d8
commit
c8755c21c2
@@ -3,25 +3,27 @@
|
|||||||
[](https://github.com/parththakkar106/AI-DnD/actions/workflows/ci.yml)
|
[](https://github.com/parththakkar106/AI-DnD/actions/workflows/ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
An AI Dungeon-style interactive storytelling app that runs entirely on your own machine, with
|
An interactive storytelling app that runs entirely on your own machine, with your own model.
|
||||||
your own AI model. Create scenarios, play open-ended adventures where an LLM narrates the
|
Create scenarios and play open-ended adventures where a local LLM narrates the world, keeps
|
||||||
world, and extend the engine with **JavaScript scripts compatible with real AI Dungeon
|
track of what is true, and remembers what happened.
|
||||||
scripting**.
|
|
||||||
|
|
||||||
> ### ▶️ Try it live: **[parththakkar106.github.io/AI-DnD](https://parththakkar106.github.io/AI-DnD/)**
|
This is the **Adventure Storyteller** fork of [AI-DnD](https://github.com/parththakkar106/AI-DnD).
|
||||||
> The project page loads instantly and launches the hosted demo in one tap. Play a scenario as
|
It is deliberately narrower than its upstream: single-user, local-only, and pointed at a model
|
||||||
> a guest: no sign-up and no API key needed. The demo runs on a free tier that sleeps, so the
|
you run yourself. The hosted deployment, the accounts and sessions, the cloud provider support,
|
||||||
> first load after it's been idle takes about 30 to 60 seconds to wake up.
|
the Postgres path, and the JavaScript scripting engine have all been removed rather than
|
||||||
|
disabled. What is left is a storyteller you can run offline.
|
||||||
|
|
||||||
|
> **Local-only, by design.** The app talks to one place — an Ollama-compatible endpoint on this
|
||||||
|
> machine or on a machine you control on your own network — and it refuses to be pointed at a
|
||||||
|
> public address. There is no telemetry, no account, no cloud inference, and nothing is fetched
|
||||||
|
> at runtime from the Internet.
|
||||||
>
|
>
|
||||||
> For the internals, read the **[design notes](https://parththakkar106.github.io/AI-DnD/guide.html)**.
|
> For the internals, read the **[design notes](docs/GUIDE.md)**. They walk through the context
|
||||||
> They walk through the context budgeting, the world-state referee, and the memory bank, and
|
> budgeting, the world-state referee, and the memory bank, and state the reasoning behind each
|
||||||
> state the reasoning behind each one ([Markdown version](docs/GUIDE.md)).
|
> one. Some sections still describe upstream subsystems this fork has removed.
|
||||||
|
|
||||||
Built with FastAPI and SQLAlchemy on the backend and React (Vite) on the frontend, running on
|
Built with FastAPI and SQLAlchemy on the backend and React (Vite) on the frontend, storing
|
||||||
SQLite locally and Postgres in the cloud. It works with **any OpenAI-compatible endpoint**:
|
everything in one SQLite file.
|
||||||
Ollama and LM Studio locally, or OpenRouter, OpenAI, Groq, or vLLM in the cloud. Endpoint, key,
|
|
||||||
and model are all runtime settings, and OpenRouter's free-tier models make the whole experience
|
|
||||||
cost nothing.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ isn't the live one starts a new branch.*
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **The full play loop.** Do / Say / Story / Continue actions, streamed AI responses (SSE),
|
- **The full play loop.** Do / Say / Story / Continue actions, streamed AI responses (SSE),
|
||||||
retry, undo, and edit. Reasoning models are supported: "thinking" streams into a collapsible
|
retry, undo, redo, and edit. Reasoning models are supported: "thinking" streams into a collapsible
|
||||||
💭 panel with its own token budget.
|
💭 panel with its own token budget.
|
||||||
- **A branching story tree.** The story is a tree, not a list. Any turn can hold more than one
|
- **A branching story tree.** The story is a tree, not a list. Any turn can hold more than one
|
||||||
**take**, and `‹ 2/4 ›` steps between them. Stepping is free: the story below simply empties,
|
**take**, and `‹ 2/4 ›` steps between them. Stepping is free: the story below simply empties,
|
||||||
@@ -55,30 +57,34 @@ isn't the live one starts a new branch.*
|
|||||||
- **Insights: total prompt transparency.** Every turn stores the exact prompt sent to the
|
- **Insights: total prompt transparency.** Every turn stores the exact prompt sent to the
|
||||||
model. Open 🔍 on any AI action to see each context component, its token cost, and why it was
|
model. Open 🔍 on any AI action to see each context component, its token cost, and why it was
|
||||||
included.
|
included.
|
||||||
- **JavaScript scripting, AI Dungeon-compatible.** `onInput` / `onModelContext` / `onOutput`
|
|
||||||
modifiers share `state` and a `worldEntries` API, and run in an embedded quickjs sandbox
|
|
||||||
(`backend/app/scripting/`). Real AI Dungeon scripts import and run as is. An in-app CodeMirror
|
|
||||||
editor is included.
|
|
||||||
- **Auto-summarization and Memory Bank.** The modern AI Dungeon memory system: AI-generated
|
- **Auto-summarization and Memory Bank.** The modern AI Dungeon memory system: AI-generated
|
||||||
memories every few actions, a running story summary, and embedding-based retrieval that
|
memories every few actions, a running story summary, and embedding-based retrieval that
|
||||||
pulls old-but-relevant facts back into context, with similarity scores visible in Insights
|
pulls old-but-relevant facts back into context, with similarity scores visible in Insights
|
||||||
(`backend/app/memorybank.py`).
|
(`backend/app/memorybank.py`).
|
||||||
- **Undo and retry that actually roll back state.** Undo and retry roll back the world state
|
- **Undo, Redo, and retry that roll back state and delete nothing.** Undo moves where the story
|
||||||
and script state to a per-node snapshot, not just the text, and prune the memories that
|
is being read; it removes no accepted turn, so Redo can walk forward into the turns it stepped
|
||||||
covered the removed turns. Nothing a retry replaces is discarded: the old attempt stays as
|
over. Both restore the world state from a per-node snapshot rather than just the text, and a
|
||||||
another take of that turn, one keystroke and one click from becoming a branch of its own.
|
memory derived from a turn now behind the head stops being retrieved without being deleted or
|
||||||
- **Import and export.** AI Dungeon-compatible formats for scripts and scenarios; JSON for
|
re-embedded. Writing a new turn below a moved-back head is the moment the story forks: the
|
||||||
everything else. An adventure exports as `ai-dnd-adventure-v2`, which carries the whole tree:
|
displaced future stays on the line it was written for, and ordinary Redo stops offering it.
|
||||||
every branch, every take, and the fork points, since those were chosen rather than computed.
|
Nothing a retry replaces is discarded either — the old attempt stays as another take of that
|
||||||
Files saved in the old single-line format still import.
|
turn, one keystroke and one click from becoming a branch of its own.
|
||||||
- **Optional accounts for hosted deployments.** By default the app is single-user with zero
|
- **Import and export.** AI Dungeon-compatible scenario format; JSON for everything else. An adventure exports as `ai-dnd-adventure-v2`, which carries the whole tree:
|
||||||
auth friction. Set `AIDND_MULTI_USER=1` and visitors play instantly as guests (signed
|
every branch, every take, the fork points, which branches the story has left behind, and the
|
||||||
session cookie), can register (email and password) at any point to keep their data, and each
|
position it is being read at — all of them chosen rather than computed, which is the rule for
|
||||||
user gets isolated data plus their own encrypted-at-rest API key. A server-funded **shared
|
what a bundle carries. A campaign exported after two Undos imports still undone, with its
|
||||||
demo key** with a daily turn cap lets people try it without bringing a key
|
retained future intact, instead of silently reopening at its newest turn. Files that predate
|
||||||
(`backend/app/auth.py`). Each new guest is also given a copy of a short pre-played
|
the head position, and files saved in the old single-line format, still import.
|
||||||
adventure, so the first screen shows real turns and their world-state changes without
|
- **Single user, no accounts.** There is no sign-up, no login, no session and no API key
|
||||||
spending a demo turn (`backend/app/starter.py`).
|
anywhere in the product. The storyteller API binds to loopback and is unauthenticated by
|
||||||
|
design, because the only person who can reach it is the person running it. A new install
|
||||||
|
starts with a short pre-played adventure, so the first screen shows real turns and their
|
||||||
|
world-state changes rather than an empty page (`backend/app/starter.py`).
|
||||||
|
- **A refusal you can rely on.** The inference endpoint is checked against an address
|
||||||
|
allowlist when you save it and again before every request, so a public endpoint is refused
|
||||||
|
even if the setting is edited in the database directly. TLS verification is never traded
|
||||||
|
against reachability: a privately issued certificate is verified against your machine's own
|
||||||
|
trust store, and there is no bypass switch.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -86,10 +92,10 @@ isn't the live one starts a new branch.*
|
|||||||
|---|---|
|
|---|---|
|
||||||
|  |  |
|
|  |  |
|
||||||
| **Insights**: the exact prompt for the next turn, broken into components with token counts and the trigger word that pulled each story card in. | **Authoring**: stats with ranges, per-turn caps, cooldowns, and word-labeled bands; NPCs the AI addresses by id. |
|
| **Insights**: the exact prompt for the next turn, broken into components with token counts and the trigger word that pulled each story card in. | **Authoring**: stats with ranges, per-turn caps, cooldowns, and word-labeled bands; NPCs the AI addresses by id. |
|
||||||
|  |  |
|
|  |  |
|
||||||
| **Scripting**: the three AI Dungeon hooks with shared persistent `state`, run in a quickjs sandbox. | **Home**: continue a story in progress or start from a scenario. |
|
| **Home**: continue a story in progress or start from a scenario. | **The tree**: one lane per line, from the moment it left its parent to the moment it ends. The horizontal axis is the story's own clock, so a short branch reads as short. |
|
||||||
|  |  |
|
|  | |
|
||||||
| **The tree**: one lane per line, from the moment it left its parent to the moment it ends. The horizontal axis is the story's own clock, so a short branch reads as short. | **Branches**: every line the story has taken, and the three things you can do to one. A line the one you're reading was forked from can't be deleted, and says so. |
|
| **Branches**: every line the story has taken, and the three things you can do to one. A line the one you're reading was forked from can't be deleted, and says so. | |
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -121,58 +127,62 @@ Open http://localhost:5173.
|
|||||||
|
|
||||||
## Connect a model
|
## Connect a model
|
||||||
|
|
||||||
Open **Settings** in the app and point it at any OpenAI-compatible endpoint:
|
Open **Settings** in the app and point it at a local Ollama-compatible endpoint:
|
||||||
|
|
||||||
| Provider | Endpoint URL | Notes |
|
| Where the model runs | Endpoint URL | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Ollama (local) | `http://localhost:11434/v1` | free, private; also serves embedding models for the Memory Bank (e.g. `nomic-embed-text`) |
|
| Ollama, same machine | `http://localhost:11434/v1` | the default, and the simplest thing that works |
|
||||||
| LM Studio (local) | `http://localhost:1234/v1` | free, private |
|
| Ollama, a machine on your own network | `http://<host>:11434/v1` or `https://<host>/v1` | see below |
|
||||||
| OpenRouter | `https://openrouter.ai/api/v1` | `:free` models cost nothing (no embeddings on the free tier) |
|
| LM Studio, same machine | `http://localhost:1234/v1` | |
|
||||||
| OpenAI / Groq / vLLM / … | provider's `/v1` URL | anything speaking `/v1/chat/completions` |
|
|
||||||
| Claude Code CLI (local) | `http://127.0.0.1:8787/v1` | your Claude subscription instead of an API key; see [Playing against Claude locally](#playing-against-claude-locally) |
|
|
||||||
|
|
||||||
Model name, API key, generation parameters, and (optionally) summary and embedding models for
|
Model name, generation parameters, and (optionally) summary and embedding models for the
|
||||||
the Memory Bank are all configured there too. No config files and no rebuild are needed.
|
Memory Bank are configured there too. No config files and no rebuild are needed. There is no
|
||||||
|
API key field, because there is nothing to authenticate to.
|
||||||
|
|
||||||
### Playing against Claude locally
|
### What the endpoint policy allows
|
||||||
|
|
||||||
`backend/tools/claude_shim.py` serves an OpenAI-compatible endpoint backed by the
|
The address is checked when you save it and again before every request. Only loopback and
|
||||||
`claude` command line tool, so you can play the demos against a real model without an
|
private-network addresses are accepted; every public address is refused, by address rather than
|
||||||
API key. Each request spawns one `claude --print` process, which suits the turn engine:
|
by hostname, so a name that resolves outward is refused too. A well-known cloud inference host
|
||||||
the app assembles the whole prompt every turn and expects a stateless endpoint.
|
is named in the error message only so the refusal says *why*.
|
||||||
|
|
||||||
|
Running the model on a second machine you control is supported and expected — that machine
|
||||||
|
does the inference while the storyteller itself stays bound to loopback on yours. If that
|
||||||
|
machine serves HTTPS with a certificate from a CA you installed, it works: certificates are
|
||||||
|
verified against your operating system's trust store as well as the bundled one. Verification
|
||||||
|
itself is never relaxed, and there is no option to turn it off.
|
||||||
|
|
||||||
|
### Playing against a local shim
|
||||||
|
|
||||||
|
`backend/tools/claude_shim.py` serves an OpenAI-compatible endpoint on `127.0.0.1:8787`
|
||||||
|
backed by a command-line tool, which is useful for testing the turn engine against a stronger
|
||||||
|
model. Each request spawns one process, which suits the engine: the app assembles the whole
|
||||||
|
prompt every turn and expects a stateless endpoint.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd backend
|
cd backend
|
||||||
.venv/Scripts/python.exe tools/claude_shim.py # listens on 127.0.0.1:8787
|
.venv/bin/python tools/claude_shim.py # listens on 127.0.0.1:8787
|
||||||
```
|
```
|
||||||
|
|
||||||
In Settings, choose the OpenAI-compatible provider, set the base URL to
|
Set the base URL to `http://127.0.0.1:8787/v1` and pick a model the tool offers. Set the
|
||||||
`http://127.0.0.1:8787/v1`, put any non-empty string in the API key field, and pick
|
reasoning budget to `0` or `-1`: a positive budget sends a `reasoning.max_tokens` field that
|
||||||
`sonnet`. The shim ignores the key and authenticates as you, through the CLI. Set the
|
some models reject. Embeddings are not served — leave the embedding model blank, or point the
|
||||||
reasoning budget to `0` or `-1`: a positive budget sends a `reasoning.max_tokens` field
|
Memory Bank at an endpoint that serves one.
|
||||||
that Claude 5 models reject.
|
|
||||||
|
|
||||||
Embeddings are not served. Leave the embedding model blank, or point the Memory Bank at
|
The shim has no authentication and spends whatever quota backs it, so run it on loopback and
|
||||||
a real endpoint.
|
leave it there.
|
||||||
|
|
||||||
Run it against a local backend only. The endpoint has no authentication, and anything
|
|
||||||
reaching it spends your Claude quota. `app/netguard.py` blocks localhost endpoints when
|
|
||||||
`AIDND_MULTI_USER` is set, so a deployed instance cannot be pointed at it.
|
|
||||||
|
|
||||||
## How a turn works
|
## How a turn works
|
||||||
|
|
||||||
```
|
```
|
||||||
player input
|
player input
|
||||||
→ onInput script modifier
|
|
||||||
→ assemble context: [narrator prompt] + [world state + stat guide] + [AI instructions]
|
→ assemble context: [narrator prompt] + [world state + stat guide] + [AI instructions]
|
||||||
+ [plot essentials] + [story summary] + [retrieved memories]
|
+ [plot essentials] + [story summary] + [retrieved memories]
|
||||||
+ [triggered story cards] + [history along this branch, token-budgeted]
|
+ [triggered story cards] + [history along this branch, token-budgeted]
|
||||||
+ [author's note] + [player action]
|
+ [author's note] + [player action]
|
||||||
→ onModelContext script modifier
|
|
||||||
→ snapshot context (Insights)
|
→ snapshot context (Insights)
|
||||||
→ provider adapter → AI (streamed)
|
→ provider adapter → AI (streamed)
|
||||||
→ extract + referee the world-state delta block, strip it from the prose
|
→ extract + referee the world-state delta block, strip it from the prose
|
||||||
→ onOutput script modifier
|
|
||||||
→ store & render
|
→ store & render
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -180,18 +190,17 @@ player input
|
|||||||
|
|
||||||
```
|
```
|
||||||
frontend/ React + Vite SPA ──HTTP/SSE──► backend/ FastAPI
|
frontend/ React + Vite SPA ──HTTP/SSE──► backend/ FastAPI
|
||||||
├─ routers/ auth, scenarios, adventures, story cards, scripts, chat, settings, analytics, debug
|
├─ routers/ scenarios, adventures, story cards, chat, settings, debug
|
||||||
├─ models.py SQLAlchemy: User, Scenario, Adventure, Branch, Action, StoryCard, Script, Settings, Memory
|
├─ models.py SQLAlchemy: Scenario, Adventure, Branch, Action, StoryCard, Settings, Memory
|
||||||
├─ migrations.py hand-rolled, versioned via PRAGMA user_version (64 and counting)
|
├─ migrations.py hand-rolled, versioned via PRAGMA user_version (79 and counting)
|
||||||
├─ auth.py guest/registered users, sessions, shared demo key
|
├─ endpoints.py the inference-endpoint address policy
|
||||||
├─ security.py password hashing, cookie signing, API-key encryption
|
├─ tlstrust.py one TLS context: the OS trust store unioned with certifi's
|
||||||
├─ tree.py forking, promotion, and where a node is placed
|
├─ tree.py forking, promotion, and where a node is placed
|
||||||
|
├─ head.py the active head: where the story is read, and what moving it costs
|
||||||
├─ attempts.py the takes of one turn, grouped by parent
|
├─ attempts.py the takes of one turn, grouped by parent
|
||||||
├─ context/ prompt assembly under a token budget + lineage/history windowing
|
├─ context/ prompt assembly under a token budget + lineage/history windowing
|
||||||
├─ worldstate/ the stat engine: clamps, cooldowns, bands, milestones
|
├─ worldstate/ the stat engine: clamps, cooldowns, bands, milestones
|
||||||
├─ scripting/ quickjs sandbox + AI Dungeon API surface
|
|
||||||
├─ memorybank.py auto-summarization + embedding retrieval
|
├─ memorybank.py auto-summarization + embedding retrieval
|
||||||
├─ analytics.py buffered visit counters + the owner's dashboard query
|
|
||||||
├─ bundle.py the export/import formats, v2 (tree) and a v1 reader
|
├─ bundle.py the export/import formats, v2 (tree) and a v1 reader
|
||||||
├─ providers/ OpenAI-compatible adapter, streaming
|
├─ providers/ OpenAI-compatible adapter, streaming
|
||||||
└─ data.db SQLite (path overridable via AIDND_DB_PATH)
|
└─ data.db SQLite (path overridable via AIDND_DB_PATH)
|
||||||
@@ -202,9 +211,10 @@ development, Vite proxies `/api` to FastAPI.
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
549 backend tests: unit tests plus full HTTP integration through the real quickjs scripting
|
638 backend tests: unit tests plus full HTTP integration through the real turn engine, with
|
||||||
engine, with the LLM provider mocked. CI runs them on every push, alongside the frontend
|
the model provider mocked. They run with no route to the Internet, which is a requirement
|
||||||
lint/build and a Docker image build.
|
rather than a convenience — an offline claim proved on a machine that has been online once
|
||||||
|
proves nothing.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd backend && pip install -r requirements.txt -r requirements-dev.txt
|
cd backend && pip install -r requirements.txt -r requirements-dev.txt
|
||||||
@@ -233,57 +243,19 @@ most interesting engineering in the repo.
|
|||||||
the number of SQL clauses is bounded by the context window rather than by the number of
|
the number of SQL clauses is bounded by the context window rather than by the number of
|
||||||
forks.
|
forks.
|
||||||
|
|
||||||
## Visit analytics
|
|
||||||
|
|
||||||
The hosted demo keeps its own analytics: an owner-only dashboard at `/analytics` shows
|
|
||||||
traffic, which shared scenarios get played, turns and demo-key spend, errors, and a funnel
|
|
||||||
from *visited* to *played a turn* to *signed up*. It is visible only to the emails listed in
|
|
||||||
`AIDND_ANALYTICS_EMAILS`, and the route returns 404 for everyone else.
|
|
||||||
|
|
||||||
This is built into the app rather than added with a third-party script, for reasons specific
|
|
||||||
to this project: the CSP allows only `script-src 'self'`, ad blockers block the popular
|
|
||||||
trackers, and none of those trackers can see the measurement that matters here, a turn. Counts
|
|
||||||
are aggregated in memory and flushed as UPSERTs, so a visit is a write and never a read, and
|
|
||||||
every dashboard query is a `GROUP BY` that returns tens of rows regardless of traffic volume.
|
|
||||||
That matters: see the egress note above for what reading rows per request costs on this stack.
|
|
||||||
|
|
||||||
## Deploy (Render)
|
|
||||||
|
|
||||||
The repo ships a [`render.yaml`](render.yaml) blueprint: one Docker web service that serves
|
|
||||||
the SPA and API same-origin, backed by external [Neon](https://neon.tech) Postgres. The free
|
|
||||||
Render tier has no persistent disk, so the database lives off-box.
|
|
||||||
|
|
||||||
1. Create a **Neon** project and copy its pooled connection string.
|
|
||||||
2. In Render, choose **New → Blueprint** and point it at this repo. Render reads
|
|
||||||
`render.yaml`.
|
|
||||||
3. Fill in the secrets it prompts for (`sync: false` vars): `AIDND_DATABASE_URL` (the Neon
|
|
||||||
string); `AIDND_DEMO_API_KEY` and `AIDND_DEMO_MODELS` to offer a no-signup demo; and
|
|
||||||
`AIDND_ANALYTICS_EMAILS` (your own account's email) to see the Visitors dashboard.
|
|
||||||
`AIDND_SECRET_KEY` is generated automatically and stays stable across deploys.
|
|
||||||
4. Deploy. Pushes to `main` auto-deploy after this. The health check is `/api/health`.
|
|
||||||
|
|
||||||
On the free tier the service sleeps after about 15 minutes idle, and the first request after
|
|
||||||
that takes about 30 to 60 seconds to wake it. Point any keep-warm pinger at `/api/health`,
|
|
||||||
which deliberately doesn't touch the database: waking the database around the clock costs far
|
|
||||||
more than the cold start saves.
|
|
||||||
|
|
||||||
If you put another proxy or CDN in front of Render, set `AIDND_TRUSTED_PROXY_HOPS` to the
|
|
||||||
number of proxies in the chain. It defaults to 1. The rate limiter reads the client IP that
|
|
||||||
many entries from the right of `X-Forwarded-For`, because the trusted edge appends the real
|
|
||||||
one last. Leave it at 1 behind two proxies and the limiter reads an entry the caller
|
|
||||||
supplied, so anyone can rotate the header for a fresh rate-limit bucket per request.
|
|
||||||
|
|
||||||
## Repo notes
|
## Repo notes
|
||||||
|
|
||||||
- `plan/` holds the phased implementation plan this project was built from, kept as a build
|
- `planning/` is this fork's own package: the product specification, the architecture
|
||||||
log. All fourteen phases are complete. The later files (11, 12, 14) also serve as design
|
decisions, the milestone plan, the acceptance contract, and a review report for every
|
||||||
notes for the state-revert, world-state, and story-tree work.
|
milestone shipped. Start at [`planning/README.md`](planning/README.md).
|
||||||
[`plan/STATUS.md`](plan/STATUS.md) is the running thread: what shipped, what was measured,
|
- `plan/` holds the *upstream* project's phased implementation plan, kept as a build log. The
|
||||||
and what is owed next.
|
later files (11, 12, 14) still serve as design notes for the state-revert, world-state, and
|
||||||
- [`docs/GUIDE.md`](docs/GUIDE.md) holds design notes: how each subsystem works and why it was
|
story-tree work this fork inherited.
|
||||||
built that way, with the measurements behind the decisions. It is also rendered as a
|
- [`docs/GUIDE.md`](docs/GUIDE.md) holds upstream's design notes: how each subsystem works and
|
||||||
[reading page](https://parththakkar106.github.io/AI-DnD/guide.html).
|
why it was built that way, with the measurements behind the decisions. Sections covering
|
||||||
- `backend/.env.example` lists the few environment variables the backend reads.
|
scripting, accounts and hosted deployment describe subsystems this fork removed.
|
||||||
|
- `backend/.env.example` lists the two environment variables the backend reads. Everything
|
||||||
|
about the model is a runtime setting on the Settings page instead.
|
||||||
- [`docs/self-review.md`](docs/self-review.md) records a full-codebase self-review pass and
|
- [`docs/self-review.md`](docs/self-review.md) records a full-codebase self-review pass and
|
||||||
what came out of it. All correctness findings are resolved.
|
what came out of it. All correctness findings are resolved.
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,47 @@ Must cover:
|
|||||||
|
|
||||||
History operations are non-destructive, Redo works, divergence preserves old futures, and export/import reopens at the exact active head.
|
History operations are non-destructive, Redo works, divergence preserves old futures, and export/import reopens at the exact active head.
|
||||||
|
|
||||||
|
## Status: COMPLETE
|
||||||
|
|
||||||
|
Accepted 2026-09-03. Evidence: `planning/reports/M3-IMPLEMENTATION-REPORT.md`,
|
||||||
|
which is M3's primary evidence record — no separate baseline report was produced,
|
||||||
|
so that document carries the raw counts and runtime observations as well as the
|
||||||
|
review. The architecture is recorded in **ADR 012**.
|
||||||
|
|
||||||
|
**Capabilities M3 delivered, which later milestones inherit rather than build:**
|
||||||
|
|
||||||
|
- **Undo that deletes zero accepted turns** — measured directly on row identity:
|
||||||
|
15 rows before five Undos, 15 after,
|
||||||
|
- **Redo**, round-tripping exactly (head 14 → 4 → 14) in transcript and in state,
|
||||||
|
- a **single head-movement mechanism** every position change goes through, and a
|
||||||
|
**single capped lineage** that bounds every read of the story,
|
||||||
|
- **divergence decided by the lineage** rather than by a flag: the first write
|
||||||
|
below a moved-back head forks, the displaced future keeps its rows, and
|
||||||
|
ordinary Redo stops offering it with nothing to invalidate,
|
||||||
|
- **branch-scoped memory isolation preserved for free** — a memory past the head
|
||||||
|
is unretrievable and becomes eligible again on Redo, with no pruning and no
|
||||||
|
re-embedding,
|
||||||
|
- **an export that carries the reader's position**, so a campaign exported after
|
||||||
|
two Undos imports still undone, with pre-M3 bundles opening at their tip,
|
||||||
|
- **Undo across fork points** to the campaign opening, and refusal to change what
|
||||||
|
a turn says while story descends from it off screen — both ratified in
|
||||||
|
`STORY-BRANCH-SEMANTICS.md` (§5, §10, §14A).
|
||||||
|
|
||||||
|
**Outstanding closeout condition:** the required **browser smoke test has not
|
||||||
|
been performed** — no session in which M3 was implemented or reviewed had a
|
||||||
|
browser available. The equivalent sequence was driven end-to-end against the
|
||||||
|
running application with real inference and a process restart, and every
|
||||||
|
server-side behaviour it covers passes; the DOM-level behaviour of the Redo
|
||||||
|
button, its disabled states and its keyboard shortcut remain unverified by
|
||||||
|
observation. This does not block M4, which touches none of that wiring, but it
|
||||||
|
remains an open M3 item until a human runs it.
|
||||||
|
|
||||||
|
**Debt carried forward, none of it blocking M4:** full narrator-edit state
|
||||||
|
re-evaluation is deferred to M5 (`STORY-BRANCH-SEMANTICS.md` §14A records the
|
||||||
|
interim refusal); `POST /adventures/import` returns every branch's rows rather
|
||||||
|
than a head-capped window (inherited, harmless in the UI); `ActionPage` is
|
||||||
|
constructed in two places. Full table in the implementation report §S.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# M4 — Named Save Points / Checkpoints
|
# M4 — Named Save Points / Checkpoints
|
||||||
@@ -283,6 +324,32 @@ Add durable user-facing Save Points on top of the active-head model.
|
|||||||
|
|
||||||
The user can create a named Save Point, continue, restart, restore it, and continue differently without losing later history.
|
The user can create a named Save Point, continue, restart, restore it, and continue differently without losing later history.
|
||||||
|
|
||||||
|
## Note from M3 — reuse the head machinery, do not build a second one
|
||||||
|
|
||||||
|
A Save Point is **a durable named pointer to a recoverable story position**, and
|
||||||
|
nothing more. M3 made that position a stored coordinate and made moving to one a
|
||||||
|
row lookup plus a state restore, so restoring a Save Point is head movement with
|
||||||
|
a bounds check — not a restore system of its own.
|
||||||
|
|
||||||
|
Concretely, M4 should:
|
||||||
|
|
||||||
|
- store the coordinate, the name, and the metadata around them, and no copy of
|
||||||
|
any story;
|
||||||
|
- restore by calling M3's head-movement mechanism, so that state, transcript,
|
||||||
|
context and memory eligibility all move together exactly as they do for Undo
|
||||||
|
and Redo, and so that later history is retained rather than deleted (D13 is
|
||||||
|
already satisfied by the mechanism);
|
||||||
|
- let the existing fork-on-first-write-below-the-head rule handle divergence
|
||||||
|
after a restore, rather than forking at restore time;
|
||||||
|
- validate that a Save Point's coordinate is still on the lineage being read
|
||||||
|
before moving to it.
|
||||||
|
|
||||||
|
A second restore path is the specific failure to avoid. The Phase 0B spike put
|
||||||
|
the fork check in the write path and left Retry and add-take on the old one, and
|
||||||
|
M3's cost was reconciling them; a parallel checkpoint mover would recreate that
|
||||||
|
divergence in a place where the two paths would silently disagree about what
|
||||||
|
"restore" means. See ADR 012.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# M5 — Genre-Neutral Authoritative Narrative State
|
# M5 — Genre-Neutral Authoritative Narrative State
|
||||||
@@ -369,6 +436,41 @@ dropped.
|
|||||||
|
|
||||||
Evidence: `planning/reports/M2-IMPLEMENTATION-REPORT.md` §K.2, §Q.
|
Evidence: `planning/reports/M2-IMPLEMENTATION-REPORT.md` §K.2, §Q.
|
||||||
|
|
||||||
|
## Note from M3 — three constraints this milestone must satisfy
|
||||||
|
|
||||||
|
**1. Keep state efficiently recoverable at a retained position.**
|
||||||
|
M3's head movement is a row lookup plus a state restore, which is why Undo, Redo
|
||||||
|
and — from M4 — Save Point restore all cost the same regardless of how far into a
|
||||||
|
campaign the position is. A state model recoverable only by replaying events from
|
||||||
|
the campaign opening would make every one of those operations proportional to
|
||||||
|
campaign length, on exactly the long campaigns this product exists for.
|
||||||
|
|
||||||
|
`TECHNICAL-DESIGN.md` §10.4 already selects a hybrid of validated events plus
|
||||||
|
snapshots/cache. M3 turns the snapshot half from a preference into a requirement:
|
||||||
|
keep per-node snapshots, or an equivalent cache with the same property, while
|
||||||
|
adding the typed event model. See ADR 012.
|
||||||
|
|
||||||
|
**2. Move the instrumentation, keep the assertions.**
|
||||||
|
The M2 note above applies with more force after M3: `test_head_cursor.py` adds
|
||||||
|
roughly a dozen more tests that express position and rollback through the
|
||||||
|
inherited gold counter. What they measure is *positional* — that the state
|
||||||
|
belonging to a story position is restored when the head moves to it, in either
|
||||||
|
direction, and that an abandoned line's state does not survive a divergence.
|
||||||
|
Those properties must still hold over whatever carries state after M5. The file's
|
||||||
|
own docstring says so.
|
||||||
|
|
||||||
|
**3. Complete the narrator edit.**
|
||||||
|
`STORY-BRANCH-SEMANTICS.md` §14-15 requires that a narrator edit become
|
||||||
|
authoritative and that the state it implies be re-evaluated. That requirement is
|
||||||
|
intact and unimplemented: re-evaluating state from prose a user typed needs this
|
||||||
|
milestone's extraction pass. M3 shipped the safe interim behavior only — an
|
||||||
|
in-place edit is refused when story descends from the turn and is off screen
|
||||||
|
(§14A), so retained history cannot be made to disagree with itself unseen.
|
||||||
|
M5 is where §14-15 is finished: return to the state before the edited narration,
|
||||||
|
treat the edited text as accepted output, re-evaluate the implied state, create a
|
||||||
|
new continuation, and retain the original. The refusal in §14A is then replaced
|
||||||
|
by that behavior rather than kept alongside it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# M6 — Branch-Safe Context, Summaries, and Long-Term Story Memory
|
# M6 — Branch-Safe Context, Summaries, and Long-Term Story Memory
|
||||||
|
|||||||
+37
-1
@@ -74,6 +74,14 @@ story_profile:
|
|||||||
tense: optional string
|
tense: optional string
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The active head is **stored on the campaign, not derived** from its newest turn.
|
||||||
|
This is the concept M3 implemented; the current implementation carries it as a
|
||||||
|
branch reference plus a depth on that branch rather than as a turn id, which is
|
||||||
|
an equivalent coordinate and is what the export format records. What matters
|
||||||
|
conceptually is that the position is a decision the campaign remembers: two
|
||||||
|
campaigns holding identical turns can be being read at different places, and
|
||||||
|
nothing about the turns themselves can tell them apart.
|
||||||
|
|
||||||
Campaigns also store durable narrator rules and model configuration.
|
Campaigns also store durable narrator rules and model configuration.
|
||||||
|
|
||||||
Potential model roles:
|
Potential model roles:
|
||||||
@@ -109,7 +117,26 @@ Rules:
|
|||||||
- Redo moves the active head forward while the prior continuation remains selected,
|
- Redo moves the active head forward while the prior continuation remains selected,
|
||||||
- a new write below the retained tip creates a new continuation and leaves the old future retained/disposable.
|
- a new write below the retained tip creates a new continuation and leaves the old future retained/disposable.
|
||||||
|
|
||||||
Detailed behavior will be defined separately in `STORY-BRANCH-SEMANTICS.md`.
|
`disposition` above is conceptual. As implemented in M3 it is stored as **the
|
||||||
|
fact that produced it** rather than as a word: a branch records the depth a
|
||||||
|
divergent write left it at, and when. No value means active; a value means the
|
||||||
|
story past that depth is retained history no active head is reading. The
|
||||||
|
shallowest departure wins if a branch is left more than once.
|
||||||
|
|
||||||
|
Two properties of that representation are deliberate and worth carrying in this
|
||||||
|
document:
|
||||||
|
|
||||||
|
- **Nothing reads it to decide behavior.** Whether Redo is available, what the
|
||||||
|
transcript shows, and which continuation a write belongs to are all decided by
|
||||||
|
the lineage. A stale or hand-edited disposition therefore cannot make the story
|
||||||
|
wrong; it can only mislead a cleanup or recovery feature about what is
|
||||||
|
abandoned.
|
||||||
|
- **It survives export and import.** Every row of an abandoned line is exported
|
||||||
|
either way, so the disposition is the only thing distinguishing it from an
|
||||||
|
active one in a restored campaign.
|
||||||
|
|
||||||
|
Detailed behavior will be defined separately in `STORY-BRANCH-SEMANTICS.md`;
|
||||||
|
the architecture is recorded in ADR 012.
|
||||||
|
|
||||||
## 6. Turn
|
## 6. Turn
|
||||||
|
|
||||||
@@ -655,6 +682,15 @@ A campaign export should be capable of preserving:
|
|||||||
|
|
||||||
The physical container format remains an implementation choice, but the export must preserve the exact active branch **and active head position**, even when the head is behind a retained tip after Undo. A ZIP containing a database plus manifest remains a strong candidate.
|
The physical container format remains an implementation choice, but the export must preserve the exact active branch **and active head position**, even when the head is behind a retained tip after Undo. A ZIP containing a database plus manifest remains a strong candidate.
|
||||||
|
|
||||||
|
As implemented in M3, the export carries the active branch, the active head
|
||||||
|
position on it, and each branch's disposition, alongside the whole retained turn
|
||||||
|
graph. The governing rule for this package is that an export carries what was
|
||||||
|
*chosen* and recomputes what is *derived* — and the active head moved from the
|
||||||
|
second category to the first, because once Undo stops deleting, two campaigns
|
||||||
|
with identical turns can be being read at different positions and no import can
|
||||||
|
tell which. An export written before the field existed is opened at its retained
|
||||||
|
tip, which is the position such a file recorded.
|
||||||
|
|
||||||
## 30. Deletion vs Archival
|
## 30. Deletion vs Archival
|
||||||
|
|
||||||
The system must distinguish:
|
The system must distinguish:
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# ADR 012 — Active-Head Non-Destructive History
|
||||||
|
|
||||||
|
**Status:** Accepted; implemented in M3
|
||||||
|
**Date:** 2026-09-03
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Where the story is being read and how much story is retained are **two separate
|
||||||
|
facts**, stored separately and answered by different code.
|
||||||
|
|
||||||
|
- The **active head** is a stored position — a branch and a depth on it. It is
|
||||||
|
where the story currently ends as far as the reader, the narrator prompt, and
|
||||||
|
every feature built on them are concerned.
|
||||||
|
- The **retained tip** is the deepest node still kept on the same lineage. It may
|
||||||
|
be ahead of the head.
|
||||||
|
|
||||||
|
Undo and Redo move the active head. They delete nothing, restore nothing from a
|
||||||
|
log, and recompute nothing. Every ordinary read of the story is bounded by the
|
||||||
|
head; retained story beyond it stays live in the database, reachable by Redo,
|
||||||
|
and available to a divergence.
|
||||||
|
|
||||||
|
Concretely, and as implemented:
|
||||||
|
|
||||||
|
1. The active head is persisted on the campaign, not derived from the newest
|
||||||
|
row. It is a decision, and no read may re-derive it.
|
||||||
|
2. Lineage resolution caps every entry at the head, in one place, so the
|
||||||
|
transcript, the assembled context, take/parent resolution and memory
|
||||||
|
retrieval narrow together and cannot disagree.
|
||||||
|
3. Reading past the head is possible through one narrow, named exception, and
|
||||||
|
only two callers may use it: Redo, and the check that decides whether a write
|
||||||
|
must fork.
|
||||||
|
4. The state belonging to a position is recorded on the node that produced it, so
|
||||||
|
moving the head is a row lookup plus a restore — the same cost at any
|
||||||
|
distance, in either direction.
|
||||||
|
5. Undo alone never forks. The **first write below a moved-back head** is the
|
||||||
|
divergence: it creates a new continuation, and the displaced future stays
|
||||||
|
where it was written, on the line it was written on.
|
||||||
|
6. Whether an ordinary Redo exists is decided by the lineage, not by a flag. After
|
||||||
|
a divergence the displaced future is no longer on the lineage, so there is
|
||||||
|
nothing ahead to walk into and no state to invalidate.
|
||||||
|
7. A branch the story has left records the depth it was left at and when, as
|
||||||
|
metadata that **nothing reads to decide behavior**. It exists so a divergence
|
||||||
|
is observable and so later cleanup and recovery features have something to
|
||||||
|
select on.
|
||||||
|
8. Derived work — memories, summary coverage — is anchored to the node it came
|
||||||
|
from and is therefore filtered by the same capped lineage. Undo prunes
|
||||||
|
nothing; Redo re-derives nothing.
|
||||||
|
9. Export carries the active head, because it is a chosen position rather than a
|
||||||
|
fact about the newest row. Import honors it. A file that predates the field is
|
||||||
|
opened at its tip, which is the position such a file recorded.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR 005 states the **product requirement**: returning to an earlier point
|
||||||
|
preserves abandoned future history rather than erasing it, and the user sees
|
||||||
|
Undo/Redo/Retry/Save Point rather than branch management. It names a movable
|
||||||
|
active head as the implementation direction and stops there.
|
||||||
|
|
||||||
|
This ADR records the **architecture selected to implement it**, as built and
|
||||||
|
demonstrated in M3. It does not restate or revise ADR 005.
|
||||||
|
|
||||||
|
The production base shipped a destructive Undo: it deleted the trailing turns,
|
||||||
|
pruned the memories covering them, and let the tip fall back to whatever
|
||||||
|
survived. That made the head a derived value, made Redo impossible, and — as
|
||||||
|
Phase 0B found — let an export silently reopen an undone campaign at its newest
|
||||||
|
retained turn.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
- **Keep the head derived and mark rows inactive.** Rejected: every read would
|
||||||
|
need its own filter, and the filters would drift. Capping the lineage once is
|
||||||
|
what makes the whole application agree about where the story ends.
|
||||||
|
- **Rebuild state by replaying events from the opening.** Rejected: it makes the
|
||||||
|
cost of Undo proportional to campaign length, and long campaigns are the case
|
||||||
|
this product exists for.
|
||||||
|
- **Fork on Undo rather than on the first write below the head.** Rejected:
|
||||||
|
moving the head is not a decision to abandon anything — the user may be
|
||||||
|
reading, or about to Redo — and forking on every Undo fills the branch table
|
||||||
|
with branches nobody chose. Redo could not survive it.
|
||||||
|
- **Decide Redo from a stored flag.** Rejected: a flag can be stale or
|
||||||
|
hand-edited, and a wrong value would produce a wrong story. Deriving it from
|
||||||
|
the lineage cannot.
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
The head is the smallest thing that can move. Making it a stored position rather
|
||||||
|
than a derived one turns Undo from an operation that destroys accepted story
|
||||||
|
into one that changes a coordinate, and everything else — Redo, divergence
|
||||||
|
preserving the old future, memory isolation, an export that reopens where the
|
||||||
|
user left it — follows from that single change rather than needing machinery of
|
||||||
|
its own.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Undo deletes zero accepted rows.** This is the invariant the architecture
|
||||||
|
exists to hold, and it is asserted directly on row identity.
|
||||||
|
- **Retained history accumulates.** v1 requires no automatic cleanup; the
|
||||||
|
disposition metadata is what a later cleanup or recovery feature will select
|
||||||
|
on.
|
||||||
|
- **Any operation that changes what the story says at a position must ask
|
||||||
|
whether story descends from that position and is off screen.** Switching the
|
||||||
|
selected take and editing a turn's text in place both must refuse in that
|
||||||
|
situation rather than act silently. See `STORY-BRANCH-SEMANTICS.md` §10 and
|
||||||
|
§14A.
|
||||||
|
- **Undo crosses fork points**, because a forked story includes the story it was
|
||||||
|
forked out of and nothing is being deleted. The floor is the campaign opening.
|
||||||
|
- **Save Points must reuse this mechanism.** A named save point is a durable
|
||||||
|
coordinate; restoring one is head movement with a bounds check. Introducing a
|
||||||
|
second restore path would reintroduce exactly the divergence this ADR removes.
|
||||||
|
- **The narrative-state model must keep state efficiently recoverable at a
|
||||||
|
position** — a per-node snapshot or an equivalent cache — or Undo, Redo and
|
||||||
|
Save Point restore all become proportional to campaign length. This is a
|
||||||
|
constraint on ADR 010's engine, not a reversal of it.
|
||||||
|
- **Every feature that reads story must read it through the capped lineage.**
|
||||||
|
Anything that queries rows directly will see retained history the story is not
|
||||||
|
telling.
|
||||||
+46
-10
@@ -1,7 +1,7 @@
|
|||||||
# Adventure Storyteller Planning Package
|
# Adventure Storyteller Planning Package
|
||||||
|
|
||||||
**Status:** Phase 0 complete; architecture selected; **Milestones M1 and M2 implemented and accepted (2026-09-02)**.
|
**Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted (M3: 2026-09-03)**.
|
||||||
**Production coding:** Underway, milestone by milestone. M1 and M2 are done; M3 is the next milestone to brief.
|
**Production coding:** Underway, milestone by milestone. M1, M2 and M3 are done; M4 is the next milestone to brief.
|
||||||
|
|
||||||
This package contains the current product requirements, final Phase 0 architecture decisions, detailed subsystem designs, acceptance tests, research evidence, and the production milestone plan for the local-only interactive-story project.
|
This package contains the current product requirements, final Phase 0 architecture decisions, detailed subsystem designs, acceptance tests, research evidence, and the production milestone plan for the local-only interactive-story project.
|
||||||
|
|
||||||
@@ -168,8 +168,13 @@ Milestone M2 COMPLETE (2026-09-02)
|
|||||||
policy
|
policy
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Milestone M3 NEXT — brief not yet prepared
|
Milestone M3 COMPLETE (2026-09-03)
|
||||||
non-destructive undo/redo
|
non-destructive undo/redo, see planning/reports/M3-*.md and ADR 012
|
||||||
|
active-head export one open condition: the browser smoke test
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Milestone M4 NEXT — brief not yet prepared
|
||||||
|
named Save Points
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Implement and review milestone-by-milestone
|
Implement and review milestone-by-milestone
|
||||||
@@ -179,12 +184,43 @@ Implement and review milestone-by-milestone
|
|||||||
|
|
||||||
**One milestone at a time. Do not begin a milestone before its brief exists.**
|
**One milestone at a time. Do not begin a milestone before its brief exists.**
|
||||||
|
|
||||||
M1 and M2 are complete and accepted; the evidence is in `reports/M1-*.md` and
|
M1, M2 and M3 are complete and accepted; the evidence is in `reports/M1-*.md`,
|
||||||
`reports/M2-*.md`. **No M3 brief has been prepared.** The current action is to
|
`reports/M2-*.md` and `reports/M3-IMPLEMENTATION-REPORT.md` — the last of which
|
||||||
write one, informed by the post-M2 corrections below and by
|
is M3's primary evidence record as well as its review, since no separate M3
|
||||||
`reports/M2-IMPLEMENTATION-REPORT.md` §Q, which records that M3's chokepoints
|
baseline report was produced. **No M4 brief has been prepared.** The current
|
||||||
were left untouched or simplified by M2 and that the Phase 0B undo/redo spike
|
action is to write one, informed by the post-M3 corrections below, by the note
|
||||||
still applies.
|
`BUILD-MILESTONES.md` now attaches to M4, and by **ADR 012**, which records the
|
||||||
|
head-movement mechanism M4 must reuse rather than reimplement.
|
||||||
|
|
||||||
|
One M3 condition remains open and is not a blocker for M4: the required
|
||||||
|
**browser smoke test has not been performed**, because no session in which M3
|
||||||
|
was implemented or reviewed had a browser available. See
|
||||||
|
`reports/M3-IMPLEMENTATION-REPORT.md` §M and §W.4.
|
||||||
|
|
||||||
|
### Post-M3 corrections applied (2026-09-03)
|
||||||
|
|
||||||
|
M3's review recommended planning changes and, following the M2 pattern, reported
|
||||||
|
rather than applied them. All are now applied, together with the closeout work
|
||||||
|
the milestone itself required:
|
||||||
|
|
||||||
|
| Document | Correction |
|
||||||
|
| --- | --- |
|
||||||
|
| `DECISIONS/012-active-head-non-destructive-history.md` | **New ADR.** The architecture selected to implement ADR 005: head stored not derived, one capped read path, one movement mechanism, state from the node, divergence on first write below the head, Redo decided by the lineage, advisory disposition metadata, and the head as an exported decision. |
|
||||||
|
| `STORY-BRANCH-SEMANTICS.md` §5 | Undo crosses fork points and continues to the campaign opening. The old refusal was a consequence of destructive deletion, not a product decision. |
|
||||||
|
| `STORY-BRANCH-SEMANTICS.md` §10 | Switching which take is live is refused while a later story is off screen, with the two resolutions the user has. |
|
||||||
|
| `STORY-BRANCH-SEMANTICS.md` §14A | **New.** In-place editing before §14-15 exist: refuse when story descends from the turn and is not on screen. States explicitly that the full narrator-edit requirement stands and is completed in M5. |
|
||||||
|
| `TECHNICAL-DESIGN.md` §8.7, §9.1 | **New.** The implemented active-head model and bundle behaviour, recorded as fact. |
|
||||||
|
| `TECHNICAL-DESIGN.md` §10.4 | Constraint from M3: the snapshot half of the hybrid state model is a requirement, or head movement becomes proportional to campaign length. |
|
||||||
|
| `DATA-MODEL.md` §4, §5, §29 | The head as campaign-stored rather than derived; the branch disposition as implemented and deliberately advisory; the export as carrying a chosen position. |
|
||||||
|
| `BUILD-MILESTONES.md` M3 | Marked COMPLETE, with inherited capabilities, the open browser condition, and carried debt. |
|
||||||
|
| `BUILD-MILESTONES.md` M4 | Note: a Save Point is a durable pointer; restore by reusing M3's head movement rather than building a second restore path. |
|
||||||
|
| `BUILD-MILESTONES.md` M5 | Note: keep state efficiently recoverable at a position; move the test instrumentation rather than the assertions; complete the narrator edit. |
|
||||||
|
| `V1-ACCEPTANCE-TESTS.md` D03, D10, I07, L01 | D03's result recorded as a full pass; **D10's milestone ownership stated without weakening any pass condition**; I07's pre-M3 bundle clause added; the apparent L01/A05 conflict resolved. |
|
||||||
|
| `README.md` | Corrected to describe the current local-only single-user application. The scripting, accounts, analytics, hosted-demo, cloud-provider, Postgres and Render material described subsystems M2 removed. |
|
||||||
|
|
||||||
|
`SPECIFICATION.md` and `SECURITY-THREAT-MODEL.md` were deliberately **not**
|
||||||
|
changed. M3 altered no product requirement and touched no path in the threat
|
||||||
|
model.
|
||||||
|
|
||||||
### Post-M2 corrections applied (2026-09-03)
|
### Post-M2 corrections applied (2026-09-03)
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,29 @@ If the selected base architecture makes unlimited Undo substantially harder or u
|
|||||||
|
|
||||||
Phase 0B demonstrated repeated non-destructive Undo well beyond the minimum five-step requirement. The selected head-cursor design should therefore support Undo across retained active-lineage history up to the root unless a later implementation defect forces a documented exception.
|
Phase 0B demonstrated repeated non-destructive Undo well beyond the minimum five-step requirement. The selected head-cursor design should therefore support Undo across retained active-lineage history up to the root unless a later implementation defect forces a documented exception.
|
||||||
|
|
||||||
|
### Undo crosses fork points (settled in M3)
|
||||||
|
|
||||||
|
Undo continues backward through story a branch **inherited** from the line it
|
||||||
|
forked from, up to the campaign opening. It does not stop at the fork.
|
||||||
|
|
||||||
|
This reverses the behavior of the pre-M3 base, and the reversal follows from the
|
||||||
|
history model rather than from a change of mind about the product. Undo used to
|
||||||
|
delete the turns it stepped over, and the turns before a fork belong to the
|
||||||
|
parent line's story as well, so refusing at the fork was the only way to stop
|
||||||
|
one line's Undo from destroying story another line was still telling. Undo now
|
||||||
|
moves the reading position and deletes nothing, so there is nothing to protect
|
||||||
|
the parent from: a forked story includes the story it was forked out of, and
|
||||||
|
walking back through it is a reader moving backward, not a branch reaching into
|
||||||
|
another branch's history.
|
||||||
|
|
||||||
|
The only floor is the campaign opening. There is no pre-campaign position to
|
||||||
|
reach, and Undo at the opening reports that there is nothing to undo.
|
||||||
|
|
||||||
|
One consequence is worth stating plainly for anyone reading a transcript: a
|
||||||
|
single Undo on a forked story can step back over a turn that was originally
|
||||||
|
written on the line it forked from. Nothing about that turn changes; it simply
|
||||||
|
stops being part of what is currently being told.
|
||||||
|
|
||||||
## 6. State Restoration on Undo
|
## 6. State Restoration on Undo
|
||||||
|
|
||||||
Undo must restore more than visible transcript text.
|
Undo must restore more than visible transcript text.
|
||||||
@@ -242,6 +265,28 @@ User action
|
|||||||
|
|
||||||
Inactive takes should remain retained initially.
|
Inactive takes should remain retained initially.
|
||||||
|
|
||||||
|
### Selecting a take while a later story is off screen (settled in M3)
|
||||||
|
|
||||||
|
Selecting a different take is a change to what the story says at a position that
|
||||||
|
already has a story after it. While that later story is on screen, the choice is
|
||||||
|
plainly visible and the user can see what they are changing.
|
||||||
|
|
||||||
|
It is not, once the later story has been moved out of view — undone and not yet
|
||||||
|
redone, or left behind by a new continuation. Silently switching the take
|
||||||
|
underneath it would leave retained history continuing from words the story no
|
||||||
|
longer says, and the user would have no way to see that it had happened.
|
||||||
|
|
||||||
|
The system must therefore refuse to switch the selected take in that situation
|
||||||
|
and say why, rather than switching it quietly. The user resolves it by deciding
|
||||||
|
what they mean:
|
||||||
|
|
||||||
|
- **Redo**, bringing the later story back into view, and then choose freely; or
|
||||||
|
- **play the turn again from here**, which starts a new continuation and keeps
|
||||||
|
the old one as retained history.
|
||||||
|
|
||||||
|
The same rule and the same two resolutions apply to editing a turn's text in
|
||||||
|
place; see §14A.
|
||||||
|
|
||||||
## 11. Retry vs Branch
|
## 11. Retry vs Branch
|
||||||
|
|
||||||
Retry should not be presented to the user as “creating a branch.”
|
Retry should not be presented to the user as “creating a branch.”
|
||||||
@@ -332,6 +377,38 @@ Therefore the system must:
|
|||||||
|
|
||||||
The system must not simply replace visible text while leaving stale state behind.
|
The system must not simply replace visible text while leaving stale state behind.
|
||||||
|
|
||||||
|
## 14A. Editing In Place, Before §14-15 Are Implemented
|
||||||
|
|
||||||
|
§14 and §15 describe the finished behavior: a narrator edit becomes
|
||||||
|
authoritative, the state it implies is re-evaluated, a new continuation is
|
||||||
|
created, and the original narration and its future are retained. That
|
||||||
|
requirement stands in full and is **not** weakened by this section.
|
||||||
|
|
||||||
|
It is not yet built. Re-evaluating the state implied by prose a user typed
|
||||||
|
requires the authoritative narrative-state extraction that the genre-neutral
|
||||||
|
state milestone introduces, so the finished behavior is completed there. What
|
||||||
|
exists in the meantime is a plain correction: it changes the words of one turn
|
||||||
|
and re-evaluates nothing.
|
||||||
|
|
||||||
|
That correction is safe while everything descending from the turn is on screen,
|
||||||
|
because the user can see what their change has to stay consistent with. It is
|
||||||
|
not safe when a continuation descends from the turn and is **off screen** —
|
||||||
|
undone and not yet redone, or left behind by a divergence — because the edit
|
||||||
|
would then silently change the words that retained story was written from, and
|
||||||
|
nothing on screen would show it. Retained history is not permitted to be made to
|
||||||
|
disagree with itself in a way the user cannot see.
|
||||||
|
|
||||||
|
Until §14-15 are implemented, the system must therefore **refuse** an in-place
|
||||||
|
edit of a turn that has story descending from it which is not currently being
|
||||||
|
shown, and say why. The user resolves it the same two ways as §10:
|
||||||
|
|
||||||
|
- **Redo**, bringing the later story back into view; or
|
||||||
|
- **play the turn again from here**, which is the §13 shape — return to the
|
||||||
|
parent position, continue differently, and keep the old line as retained
|
||||||
|
history.
|
||||||
|
|
||||||
|
Refusing is the minimum that keeps the invariant. It is not the destination.
|
||||||
|
|
||||||
## 16. Manual State / Canon Correction
|
## 16. Manual State / Canon Correction
|
||||||
|
|
||||||
The user should be able to correct authoritative story state without rewriting prose.
|
The user should be able to correct authoritative story state without rewriting prose.
|
||||||
|
|||||||
@@ -361,6 +361,58 @@ Abandoned history must:
|
|||||||
- stop influencing current state/context/memory/summary,
|
- stop influencing current state/context/memory/summary,
|
||||||
- remain available for future recovery/cleanup features.
|
- remain available for future recovery/cleanup features.
|
||||||
|
|
||||||
|
### 8.7 As implemented in M3
|
||||||
|
|
||||||
|
M3 built this model. The following is fact rather than direction, and ADR 012
|
||||||
|
records it as the architectural decision. Sections 8.1-8.6 stand; this says how
|
||||||
|
they were realised.
|
||||||
|
|
||||||
|
**The head is stored, not derived.** A campaign carries a branch and a depth,
|
||||||
|
and that pair is the active head. No read may recompute it from the newest row —
|
||||||
|
that was the pre-M3 behavior, and it is what made Redo impossible and made an
|
||||||
|
export reopen an undone campaign at its tip.
|
||||||
|
|
||||||
|
**Lineage reads are capped at the head, in one place.** The path abstraction that
|
||||||
|
already resolved a branch's ancestry now also limits every entry to the head, so
|
||||||
|
the transcript, the assembled narrator context, take/parent resolution and memory
|
||||||
|
retrieval narrow together. There is exactly one way to read past the head — a
|
||||||
|
named, uncapped view of the same lineage — and only two callers may use it: Redo,
|
||||||
|
and the check that decides whether a write must fork. Any new feature that reads
|
||||||
|
story rows directly, rather than through the capped lineage, will see retained
|
||||||
|
history the story is not telling.
|
||||||
|
|
||||||
|
**Head movement is one mechanism.** Undo, Redo, and anything later that restores
|
||||||
|
a position resolve a target depth and then call a single move operation, which
|
||||||
|
sets the coordinate and restores the state recorded at it. Undo and Redo differ
|
||||||
|
only in which way they resolve the target. Both step over a whole turn — a
|
||||||
|
player's action and the reply to it — so the head never rests between the two
|
||||||
|
halves of one turn.
|
||||||
|
|
||||||
|
**State comes from the node, not from a replay.** Each node records the state it
|
||||||
|
left behind, so moving the head is a row lookup plus a restore: the same cost at
|
||||||
|
any distance, in either direction, and identical whether the position is reached
|
||||||
|
from in front of it or from behind. This is the property §10.4's hybrid storage
|
||||||
|
must preserve.
|
||||||
|
|
||||||
|
**Divergence is a property of the lineage, not a flag.** The first write below a
|
||||||
|
moved-back head forks; Undo alone never does. After the fork, the displaced
|
||||||
|
future is no longer on the lineage being read, so ordinary Redo finds nothing
|
||||||
|
ahead and reports that it has nowhere to go. Nothing has to be invalidated,
|
||||||
|
cleared, or kept in step.
|
||||||
|
|
||||||
|
**A branch the story leaves records the depth and time it was left**, as metadata
|
||||||
|
nothing reads to decide behavior (§8.6's "implementation-appropriate metadata").
|
||||||
|
It makes a divergence observable and gives later cleanup and recovery features
|
||||||
|
something to select on; because no decision depends on it, a stale or hand-edited
|
||||||
|
value cannot make the story wrong.
|
||||||
|
|
||||||
|
**Operations that change what the story says at a position must ask whether
|
||||||
|
story descends from that position and is off screen.** Switching which take is
|
||||||
|
live, and editing a turn's text in place, both refuse in that situation rather
|
||||||
|
than act silently, because retained history must not be made to disagree with
|
||||||
|
itself in a way the user cannot see. See `STORY-BRANCH-SEMANTICS.md` §10 and
|
||||||
|
§14A.
|
||||||
|
|
||||||
## 9. Export / Import and Head Position
|
## 9. Export / Import and Head Position
|
||||||
|
|
||||||
AI-DnD's current export carries branch information but reconstructs the imported head at the branch tip.
|
AI-DnD's current export carries branch information but reconstructs the imported head at the branch tip.
|
||||||
@@ -383,6 +435,31 @@ For compatibility with earlier bundles, import may fall back to the retained tip
|
|||||||
|
|
||||||
Export/import regression tests must include an undone campaign and verify the imported story reopens at the exact exported head rather than silently redoing later turns.
|
Export/import regression tests must include an undone campaign and verify the imported story reopens at the exact exported head rather than silently redoing later turns.
|
||||||
|
|
||||||
|
### 9.1 As implemented in M3
|
||||||
|
|
||||||
|
The bundle carries the active head depth beside the active branch, and the import
|
||||||
|
honors it. This moved the head depth across the format's own rule about what a
|
||||||
|
bundle carries: a bundle carries what was *chosen* and recomputes what is
|
||||||
|
*derived*, and before M3 the head depth was genuinely derived — the newest row was
|
||||||
|
the only place a story could be read. It is a decision now, because the same tree
|
||||||
|
exports identically whether the user undid three turns or none, so the file has to
|
||||||
|
say.
|
||||||
|
|
||||||
|
A file that does not state a head is opened at the tip of its active branch. That
|
||||||
|
is a fallback only in form: such a file was written when the head could not be
|
||||||
|
anywhere else, so deriving the tip reproduces the position it actually recorded.
|
||||||
|
Pre-tree bundles take the same path. No format version bump was required, because
|
||||||
|
an absent field is unambiguous.
|
||||||
|
|
||||||
|
The head depth is validated before any row is written — a depth past the branch's
|
||||||
|
own retained story is a file disagreeing with itself and is refused, while a depth
|
||||||
|
*behind* it is the feature.
|
||||||
|
|
||||||
|
The bundle also carries which branches the story has left, and at what depth.
|
||||||
|
Every row of an abandoned line is exported either way, so without that metadata a
|
||||||
|
restored campaign could not distinguish abandoned history from active history —
|
||||||
|
which is precisely what a later cleanup or recovery feature has to select on.
|
||||||
|
|
||||||
## 10. Authoritative Narrative State
|
## 10. Authoritative Narrative State
|
||||||
|
|
||||||
### 10.1 Do not retain the RPG state protocol as the product model
|
### 10.1 Do not retain the RPG state protocol as the product model
|
||||||
@@ -457,6 +534,17 @@ Selected direction:
|
|||||||
|
|
||||||
Events provide audit/reconstruction value. Snapshots/cache make normal reads, Undo/Redo, and context construction fast.
|
Events provide audit/reconstruction value. Snapshots/cache make normal reads, Undo/Redo, and context construction fast.
|
||||||
|
|
||||||
|
**Constraint added by M3 (see ADR 012).** The snapshot half is not an
|
||||||
|
optimization to be traded away. M3's head movement is a row lookup plus a
|
||||||
|
restore, which is why Undo, Redo and — later — Save Point restore cost the same
|
||||||
|
at any distance into a campaign's history. A state model that could only be
|
||||||
|
reconstructed by replaying events from the campaign opening would make every one
|
||||||
|
of those operations proportional to campaign length, on exactly the long
|
||||||
|
campaigns this product exists for. Whatever M5 introduces must keep the
|
||||||
|
authoritative state at a retained position efficiently recoverable — a per-node
|
||||||
|
snapshot, or an equivalent cache with the same property — while adding the typed
|
||||||
|
event model.
|
||||||
|
|
||||||
## 11. Context and Memory
|
## 11. Context and Memory
|
||||||
|
|
||||||
Retain AI-DnD's useful lineage-aware memory foundation, but align it with the product authority model.
|
Retain AI-DnD's useful lineage-aware memory foundation, but align it with the product authority model.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Adventure Storyteller — V1 Acceptance Tests
|
# Adventure Storyteller — V1 Acceptance Tests
|
||||||
|
|
||||||
**Status:** v1.1 planning/release contract — updated after Phase 0B, and after M2 for
|
**Status:** v1.2 planning/release contract — updated after Phase 0B, after M2 for
|
||||||
the security contract (H10 strengthened, H12 added)
|
the security contract (H10 strengthened, H12 added), and after M3 for history
|
||||||
|
ownership and results (D03, D10, I07, L01)
|
||||||
**Purpose:** Define black-box acceptance tests for finalist evaluation during Phase 0B and for the eventual v1 release.
|
**Purpose:** Define black-box acceptance tests for finalist evaluation during Phase 0B and for the eventual v1 release.
|
||||||
|
|
||||||
## 1. Test Philosophy
|
## 1. Test Philosophy
|
||||||
@@ -575,6 +576,17 @@ All retained turns can be traversed backward safely.
|
|||||||
### Partial
|
### Partial
|
||||||
System supports at least five but has a documented technical limit.
|
System supports at least five but has a documented technical limit.
|
||||||
|
|
||||||
|
### Result (M3)
|
||||||
|
**Pass, not partial.** Undo traverses to the campaign opening and then reports
|
||||||
|
that there is nothing to undo. No technical limit applies: each step is one
|
||||||
|
indexed query regardless of story length, because the position is a stored
|
||||||
|
coordinate rather than a replay. The floor is the campaign opening — there is no
|
||||||
|
pre-campaign position to reach.
|
||||||
|
|
||||||
|
Note also that Undo continues backward through story a branch inherited from the
|
||||||
|
line it forked from; it does not stop at a fork. See
|
||||||
|
`STORY-BRANCH-SEMANTICS.md` §5.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## D04 — Redo
|
## D04 — Redo
|
||||||
@@ -689,6 +701,26 @@ Mara wears a green cloak.
|
|||||||
- downstream state is re-evaluated,
|
- downstream state is re-evaluated,
|
||||||
- old version/future remains retained/disposable.
|
- old version/future remains retained/disposable.
|
||||||
|
|
||||||
|
### Milestone ownership
|
||||||
|
|
||||||
|
All three pass conditions stand for v1. They are delivered across three
|
||||||
|
milestones, and this note records which is which rather than reducing the
|
||||||
|
requirement:
|
||||||
|
|
||||||
|
- **M3 — safe history behavior.** Replaying a narrator turn with different text
|
||||||
|
forks, keeps the original take and its future, and starts the new continuation
|
||||||
|
from the correct earlier state. In-place editing of a turn is **refused** while
|
||||||
|
story descends from it off screen, so retained history cannot be made to
|
||||||
|
contradict itself unseen (`STORY-BRANCH-SEMANTICS.md` §14A). Delivered.
|
||||||
|
- **M5 — authoritative state re-evaluation.** The second pass condition. Making a
|
||||||
|
hand-typed narrator correction authoritative and re-evaluating the state it
|
||||||
|
implies requires the narrative-state extraction pass, so it is completed there,
|
||||||
|
and the §14A refusal is replaced by it. Outstanding.
|
||||||
|
- **Later browser UX work — the finished editing workflow.** How the user reaches
|
||||||
|
and confirms the operation. Outstanding.
|
||||||
|
|
||||||
|
D10 is therefore **not** satisfied at the end of M3, and is not scored as such.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## D11 — Named Checkpoint
|
## D11 — Named Checkpoint
|
||||||
@@ -1358,6 +1390,21 @@ No external API credentials are embedded in campaign export.
|
|||||||
- import does not silently Redo to the newest retained turn,
|
- import does not silently Redo to the newest retained turn,
|
||||||
- Redo/recovery behavior remains coherent after import.
|
- Redo/recovery behavior remains coherent after import.
|
||||||
|
|
||||||
|
### Also required — an export written before the head was carried
|
||||||
|
|
||||||
|
Import an export produced by a build that recorded no active head, and confirm it
|
||||||
|
opens at the retained tip of its active branch.
|
||||||
|
|
||||||
|
This is compatibility, not a degraded path, and the distinction matters when
|
||||||
|
reading a result: such a file was written when the head could not be anywhere but
|
||||||
|
the tip, so opening it there reproduces the position it actually recorded. An
|
||||||
|
import that refused it, or that guessed some other position for it, would be the
|
||||||
|
failure.
|
||||||
|
|
||||||
|
An export whose stated head lies beyond the story it contains is a file
|
||||||
|
disagreeing with itself and must be refused rather than opened at a guessed
|
||||||
|
position.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# J. Genre Independence
|
# J. Genre Independence
|
||||||
@@ -1490,6 +1537,20 @@ No condition exists where:
|
|||||||
- branch head advances incorrectly,
|
- branch head advances incorrectly,
|
||||||
- previous story becomes inaccessible.
|
- previous story becomes inaccessible.
|
||||||
|
|
||||||
|
### Note on the head, and on A05
|
||||||
|
|
||||||
|
"The head advances incorrectly" must be read together with A05, or the two appear
|
||||||
|
to contradict each other. A failed turn **does** move the active head forward by
|
||||||
|
one, onto the player's submitted text, because A05 deliberately retains that text
|
||||||
|
so the player can try again. That is correct behavior, not a half-advanced head.
|
||||||
|
|
||||||
|
What this test forbids is the head moving past a turn that did not happen: an
|
||||||
|
accepted narration with state written only partway, or a position that implies a
|
||||||
|
reply the story never received. Assert on the accepted narration and the
|
||||||
|
authoritative state, not on whether the head moved at all. One Undo from that
|
||||||
|
position steps back over the stranded input and leaves the story on a complete
|
||||||
|
turn.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L02 — State Reconstruction
|
## L02 — State Reconstruction
|
||||||
|
|||||||
+43
-2
@@ -1,8 +1,49 @@
|
|||||||
# Planning Package Version
|
# Planning Package Version
|
||||||
|
|
||||||
**Package:** Adventure Storyteller Planning Package v2.2
|
**Package:** Adventure Storyteller Planning Package v2.3
|
||||||
**Revision date:** 2026-09-03
|
**Revision date:** 2026-09-03
|
||||||
**Status:** Phase 0 complete; architecture selected; **Milestones M1 and M2 implemented and accepted**; M3 not yet briefed.
|
**Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted**; M4 is next to brief.
|
||||||
|
|
||||||
|
## v2.3 — Post-M3 Closeout (2026-09-03)
|
||||||
|
|
||||||
|
M3 replaced destructive Undo with a stored active head. Its review is
|
||||||
|
`reports/M3-IMPLEMENTATION-REPORT.md`, which is also M3's primary evidence
|
||||||
|
record — no separate baseline report was produced — and whose §W records this
|
||||||
|
closeout.
|
||||||
|
|
||||||
|
In summary:
|
||||||
|
|
||||||
|
- the architecture is recorded as **ADR 012 — Active-Head Non-Destructive
|
||||||
|
History**: the head is stored rather than derived, every read of the story is
|
||||||
|
capped at it in one place, one mechanism moves it, state comes from the node
|
||||||
|
rather than from a replay, the first write below a moved-back head is the
|
||||||
|
divergence, and Redo is decided by the lineage rather than by a flag. ADR 005
|
||||||
|
is unchanged: it states the product requirement, and ADR 012 states the
|
||||||
|
architecture chosen to implement it,
|
||||||
|
- two history semantics are **ratified** in `STORY-BRANCH-SEMANTICS.md`: Undo
|
||||||
|
crosses fork points to the campaign opening (§5), and the system refuses to
|
||||||
|
switch which take is live while a later story is off screen (§10),
|
||||||
|
- a **new §14A** records the interim in-place-editing rule — refuse when story
|
||||||
|
descends from the turn and is not on screen — and states explicitly that
|
||||||
|
§14-15's full narrator-edit requirement stands and is completed in M5,
|
||||||
|
- `TECHNICAL-DESIGN.md` gains **§8.7** and **§9.1** recording the implemented
|
||||||
|
model and bundle behaviour as fact, and a constraint on §10.4: the snapshot
|
||||||
|
half of the hybrid state model is a requirement, because head movement must
|
||||||
|
not become proportional to campaign length,
|
||||||
|
- `DATA-MODEL.md` records the head as campaign-stored, the branch disposition as
|
||||||
|
implemented and deliberately advisory, and the export as carrying a chosen
|
||||||
|
position rather than a derived one,
|
||||||
|
- `BUILD-MILESTONES.md` marks **M3 complete**, states the one outstanding
|
||||||
|
condition (the browser smoke test), tells **M4** to reuse M3's head movement
|
||||||
|
rather than build a second restore path, and gives **M5** three constraints,
|
||||||
|
- `V1-ACCEPTANCE-TESTS.md` records D03's full pass, states **D10's milestone
|
||||||
|
ownership without weakening any pass condition**, adds I07's pre-M3 bundle
|
||||||
|
clause, and resolves the apparent L01/A05 conflict,
|
||||||
|
- `README.md` is corrected to describe the current local-only single-user
|
||||||
|
application rather than the upstream hosted one.
|
||||||
|
|
||||||
|
`SPECIFICATION.md` and `SECURITY-THREAT-MODEL.md` are unchanged: M3 altered no
|
||||||
|
product requirement and touched no path in the threat model.
|
||||||
|
|
||||||
## v2.2 — Post-M2 Closeout (2026-09-03)
|
## v2.2 — Post-M2 Closeout (2026-09-03)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user