M4: add durable named Save Points
A Save Point is a name for a story position, and restoring one is head movement. That is the whole architecture, and it is what ADR 012 and BUILD-MILESTONES' note on M4 asked for: M3 made the head a stored (branch, depth) and made arriving at one a row lookup plus a state restore, so a Save Point needs no restore machinery of its own. What the user gets: - Name the moment they are reading, keep playing, restart the app, and come back to it. Restoring moves the story back and deletes nothing: the later turns stay, Redo still walks forward into them, and writing something different is what starts a new line while the old one is kept. - Rename, delete, and a list, in a Save Points panel beside the branch panel, with a Save Point button next to Undo and Redo. Both confirmations say what is *not* destroyed, because that is the part the screen cannot show. - Save Points survive export and import. What was deliberately not built: - No second restore path. `head.move_to_node` is the only new movement: its depth half is M3's `head.move_to` unchanged, and its branch half is the single assignment `switch_branch` already makes. No head field is written in the checkpoint router, nothing reconstructs state, nothing prunes a memory, nothing copies or deletes a turn, and restore never forks — the first write below the restored head does, through `fork_if_behind_head`. - No automatic cleanup. A Save Point behind the head, or naming a line the story left, is doing its job (STORY-BRANCH-SEMANTICS §19). The one removal is a cascade: deleting a branch takes its Save Points, as it takes its memories, because the story they named went with it. - No new ADR. ADR 012 already decides the architecture, and a table is not a decision. The one call the planning package did not already make: restore moves the branch half of the head only when the coordinate is off the path being read. Doing it unconditionally would quietly hand back an abandoned continuation whenever a Save Point in a shared prefix was restored; never doing it would make a Save Point on a departed line unrestorable, which contradicts §19. TECHNICAL-DESIGN §8.8 records it. Schema: a `checkpoints` table holding a name, an optional note and a (branch, depth) coordinate — no copy of any story. `create_all` builds it as it did `memories` and `branches`; migration 80 adds the index. No backfill, because nobody had named a position before M4. The coordinate is deliberately not an action id: one coordinate holds every attempt at a turn and exactly one is live, so a coordinate follows a retry where a row id would pin a take the story no longer tells. Tests: 680 pass (638 before). 42 new in tests/test_save_points.py covering D11-D14, I04, L03, E-series lineage and memory isolation after restore and divergence, the edge cases, and an M3-database migration. One pre-existing fixture in test_tree_migration.py needed `checkpoints` added to its drop list — SQLite refuses to drop a table another table references. Not verified: the browser. No session has had a usable one, so the Save Point panel's DOM behaviour is unobserved — as M3's Redo control still is. The twenty-step sequence was driven over HTTP against a live server with a real process restart instead, and all seventeen checks pass. M4 is implemented, not accepted: no review has been written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWU4gTfLYY6Qq9U7aa9Qw2
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c8e91f644
commit
e08d49c3eb
@@ -1,6 +1,6 @@
|
||||
# Adventure Storyteller — Production Build Milestones
|
||||
|
||||
**Status:** In implementation. M1, M2 and M3 complete and accepted (M1 and M2: 2026-09-02; M3: 2026-09-03); M4 — Named Save Points / Checkpoints — next
|
||||
**Status:** In implementation. M1, M2 and M3 complete and accepted (M1 and M2: 2026-09-02; M3: 2026-09-03); M4 — Named Save Points / Checkpoints — implemented 2026-09-03, awaiting review
|
||||
**Base:** AI-DnD `d72f7c1bda0f34fccd84afb7a25c34eb01c901de`
|
||||
|
||||
## 1. Purpose
|
||||
@@ -351,6 +351,62 @@ 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.
|
||||
|
||||
## Status: IMPLEMENTED — awaiting review
|
||||
|
||||
Implementation landed 2026-09-03. **Not accepted**: the milestone report has not
|
||||
been written and no reviewer has read the change. The Definition of Done above is
|
||||
met by the code and the tests below; whether it is met by the *product* is what
|
||||
the review is for.
|
||||
|
||||
**What M4 delivered:**
|
||||
|
||||
- **`checkpoints`**, a table holding a name, an optional note, and a
|
||||
`(branch, depth)` coordinate — and no copy of any story. `create_all` builds
|
||||
it, as it did `memories` and `branches`; migration 80 adds the index. No
|
||||
backfill: nobody had named a position before M4, and inventing one would be
|
||||
inventing the decision.
|
||||
- **Create / list / rename / delete / restore** under
|
||||
`/api/adventures/{id}/checkpoints`, campaign-scoped, with a Save Point from
|
||||
another campaign a 404 rather than a restore of the wrong story.
|
||||
- **Create at the active head, not the retained tip**, so a Save Point made after
|
||||
two Undos names the undone position.
|
||||
- **Restore that delegates**, and is the whole of the milestone's architecture:
|
||||
resolve the coordinate, refuse it if it names no live turn, then
|
||||
`head.move_to_node` — one function whose depth half is M3's `head.move_to`
|
||||
unchanged, and whose branch half is the single assignment `switch_branch`
|
||||
makes. Restore forks nothing.
|
||||
- **A browser Save Point panel** — create form, list, Restore, Rename, Delete,
|
||||
with both confirmations saying what is *not* destroyed — plus a Save Point
|
||||
button beside Undo and Redo, where it belongs. No branch explorer, no
|
||||
discarded-history browser, no merge UI.
|
||||
- **Export/import of Save Points** with no format version bump, and a pre-M4
|
||||
bundle importing with none.
|
||||
|
||||
**The one architectural decision M4 had to make**, which ADR 012 does not settle:
|
||||
a Save Point can name a position on a line the story has since left, so restore
|
||||
moves the branch half of the head as well — but *only* when the coordinate is not
|
||||
on the path being read. Doing it unconditionally would quietly hand back an
|
||||
abandoned continuation whenever a Save Point in a shared prefix was restored. No
|
||||
new ADR: this is ADR 012's mechanism applied to both halves of a coordinate ADR
|
||||
012 already defines, not a new architecture. `TECHNICAL-DESIGN.md` §8.8 records
|
||||
it.
|
||||
|
||||
**Tests:** 42 in `backend/tests/test_save_points.py`, covering D11-D14, I04, L03,
|
||||
E-series lineage and memory isolation after restore and divergence, the edge
|
||||
cases in the brief, and the M3-database migration.
|
||||
|
||||
**Outstanding condition, carried from M3 and not resolved here:** the **browser
|
||||
smoke test has still not been performed**, for M3 or for M4. No session has had a
|
||||
usable browser. The M4 sequence was driven end-to-end over HTTP against a live
|
||||
server with a real process restart, and every server-side behaviour it covers
|
||||
passes; the DOM-level behaviour of the Save Point panel, its buttons and its
|
||||
confirmations remains unverified by observation.
|
||||
|
||||
**Debt M4 carries forward:** none newly discovered in the head model. The Save
|
||||
Point panel has no frontend test, because the project still has no frontend test
|
||||
runner at all (M8). `POST /adventures/import` still returns every branch's rows
|
||||
rather than a head-capped window (inherited, M3).
|
||||
|
||||
---
|
||||
|
||||
# M5 — Genre-Neutral Authoritative Narrative State
|
||||
|
||||
+39
-1
@@ -210,6 +210,40 @@ checkpoint:
|
||||
|
||||
A checkpoint is a named pointer to a recoverable story position. It should normally remain tied to the turn where it was created. Restoring it moves the campaign active head; it does not delete later retained history. A new branch is created on the first divergent write after restore, not merely because the checkpoint was opened.
|
||||
|
||||
As implemented in M4, the pointer is a **coordinate rather than a turn id**:
|
||||
`(branch_id, depth)`, which is the same pair §4 records as the campaign's active
|
||||
head and which `head.node_at` resolves. This is the equivalence §4 already draws
|
||||
between a turn reference and a branch-plus-depth, applied to the same position
|
||||
from the other end, and it is not a shortcut — it is the more correct pointer of
|
||||
the two for this data model:
|
||||
|
||||
- **A coordinate follows a retry; a row id does not.** One coordinate holds
|
||||
every attempt at a turn and exactly one of them is live (§7). A Save Point
|
||||
names the turn, so it must land on whichever take the story currently tells.
|
||||
Pinning the row would leave the pointer on a superseded attempt the reader
|
||||
cannot see.
|
||||
- **The user-facing term is Save Point**; `checkpoint` remains the internal name
|
||||
(`BROWSER-UX-SPEC.md` §23).
|
||||
|
||||
The row carries the name, an optional note, the coordinate, and its timestamps.
|
||||
It carries **no** copy of the transcript, the state, the prompt, a memory, a
|
||||
summary, or a branch's contents. Everything a restore produces comes from the
|
||||
retained history the coordinate points into.
|
||||
|
||||
Two consequences worth recording here:
|
||||
|
||||
- **Restore reuses the campaign's one head-movement mechanism.** It resolves the
|
||||
coordinate and moves the head; nothing is reconstructed and nothing is
|
||||
deleted. The branch half of the head moves only when the coordinate is not on
|
||||
the path being read, which is what makes a Save Point on a departed line
|
||||
restorable at all — and what keeps a Save Point in a shared prefix from
|
||||
dragging the reader off the line they chose. See `TECHNICAL-DESIGN.md` §8.8.
|
||||
- **A checkpoint is durable against everything but its own deletion and its
|
||||
branch's.** No pass removes one for going stale, sitting behind the head, or
|
||||
naming a line the story left (`STORY-BRANCH-SEMANTICS.md` §19). Deleting a
|
||||
branch removes its checkpoints by cascade, as it removes its memories, because
|
||||
the story they named is gone.
|
||||
|
||||
## 9. Narrative Entity
|
||||
|
||||
An entity is a persistent thing or concept in the fictional world.
|
||||
@@ -684,7 +718,11 @@ The physical container format remains an implementation choice, but the export m
|
||||
|
||||
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
|
||||
graph. **M4 added the checkpoints**, by the same rule: a position someone chose
|
||||
to name cannot be recomputed from the turns, because nothing about a turn records
|
||||
that it was bookmarked. The head and the checkpoints stay independent on import —
|
||||
a campaign opens where its head says, never at a checkpoint merely because one is
|
||||
in the file. 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
|
||||
|
||||
+16
-13
@@ -4,8 +4,9 @@
|
||||
|
||||
**Current state:** Phase 0 complete; AI-DnD forked as the production base;
|
||||
milestones **M1, M2 and M3 implemented and accepted** (M3: 2026-09-03).
|
||||
**Next:** **M4 — named Save Points.** Its brief has not been written yet, and
|
||||
writing it is the current action.
|
||||
**M4 — named Save Points — is implemented (2026-09-03) and awaiting review.**
|
||||
Its implementation report has not been written, and writing it is the current
|
||||
action. Do not begin M5.
|
||||
|
||||
**Package version:** see `VERSION.md`, which records what each revision changed
|
||||
and why.
|
||||
@@ -132,7 +133,9 @@ that is the one the next milestone's planning has to consult:
|
||||
|
||||
Completed earlier milestones are in `archive/milestone-reports/`. When M4's
|
||||
report lands, M3's moves there too: a milestone report is useful during the
|
||||
immediate next milestone and historical afterwards.
|
||||
immediate next milestone and historical afterwards. **M3's report has not moved
|
||||
yet**, because M4's does not exist — the rotation belongs to M4's closeout, not
|
||||
to its implementation.
|
||||
|
||||
## The decision this package rests on
|
||||
|
||||
@@ -234,8 +237,8 @@ Milestone M3 COMPLETE (2026-09-03)
|
||||
active-head export and ADR 012
|
||||
|
|
||||
v
|
||||
Milestone M4 NEXT — brief not yet prepared
|
||||
named Save Points
|
||||
Milestone M4 IMPLEMENTED 2026-09-03 —
|
||||
named Save Points awaiting review; no report yet
|
||||
|
|
||||
v
|
||||
M5-M11, one at a time see BUILD-MILESTONES.md
|
||||
@@ -245,15 +248,15 @@ M5-M11, one at a time see BUILD-MILESTONES.md
|
||||
|
||||
**One milestone at a time. Do not begin a milestone before its brief exists.**
|
||||
|
||||
**No M4 brief has been prepared.** Writing one is the current action, informed
|
||||
by the post-M3 corrections below, by the note `BUILD-MILESTONES.md` attaches to
|
||||
M4, and by **ADR 012**, which records the head-movement mechanism M4 must reuse
|
||||
rather than reimplement.
|
||||
**M4 is implemented and unreviewed.** Its implementation report is the current
|
||||
action; M5 does not begin before that report is written and accepted.
|
||||
|
||||
One M3 condition remains open and does not block 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.
|
||||
Two conditions remain open. The **browser smoke test has still not been
|
||||
performed** — now for M3 and for M4 — because no session so far has had a usable
|
||||
browser. See `reports/M3-IMPLEMENTATION-REPORT.md` §M and §W.4, and the M4 status
|
||||
block in `BUILD-MILESTONES.md`. And **no M4 review exists**: the status block was
|
||||
written by the implementation and records what it built, which is not the same as
|
||||
a reviewer having read it.
|
||||
|
||||
## What each milestone closeout corrected
|
||||
|
||||
|
||||
@@ -413,6 +413,49 @@ 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.
|
||||
|
||||
### 8.8 Save Points, as implemented in M4
|
||||
|
||||
M4 added durable named Save Points and built nothing in §8 that was not already
|
||||
there. This records what the milestone establishes as fact.
|
||||
|
||||
**A Save Point is a name and a coordinate.** The stored row holds the name, an
|
||||
optional note, and `(branch, depth)` — the same pair §8.7 calls the head. It
|
||||
holds no transcript, no state, no summary, no memory, and no branch contents.
|
||||
`DATA-MODEL.md` §8 describes the pointer as naming a turn; the coordinate is
|
||||
that turn's address, and `DATA-MODEL.md` §8's implementation note records why
|
||||
this project uses the address rather than a row id: one coordinate can hold
|
||||
several attempts at a turn, and a retry replaces the live one. "Turn 42 of this
|
||||
line" survives a retry; a row id would pin a take the story no longer tells.
|
||||
|
||||
**Restore is head movement, and nothing else.** It resolves the coordinate,
|
||||
refuses it if it no longer names a live turn, and then moves the head — the
|
||||
depth through §8.7's single move operation, unchanged. The transcript, the
|
||||
assembled context, the state and memory eligibility all arrive together because
|
||||
they already read through the one capped lineage. There is no second restore
|
||||
path, no state reconstruction, no memory pruning and no separate redo stack:
|
||||
D13 is satisfied by the mechanism rather than by code written to satisfy it.
|
||||
|
||||
**A Save Point may name a position on a line the story has left.** Save Points
|
||||
survive divergence, so this is reachable in ordinary use, and the depth half of
|
||||
the head cannot reach a branch the current path does not contain. Restore
|
||||
therefore moves the branch half as well when, and only when, the coordinate is
|
||||
not on the path being read — the same single assignment a branch switch makes.
|
||||
The distinction matters in the other direction too: a Save Point in a shared
|
||||
prefix must *not* drag the reader onto the ancestor, because which continuation
|
||||
follows that turn is exactly what the reader has already chosen.
|
||||
|
||||
**Restore never forks.** Moving the head is not a decision to abandon anything.
|
||||
The first write below the restored head forks, through §8.7's existing check,
|
||||
and the displaced future stays retained — so Redo still walks the original
|
||||
continuation until the user writes something different, and stops offering it
|
||||
once they have.
|
||||
|
||||
**Nothing removes a Save Point but the user.** There is no cleanup pass, and none
|
||||
is wanted: a Save Point pointing behind the head, or into a line the story left,
|
||||
is doing its job. The one exception is referential and not a policy — deleting a
|
||||
branch takes its Save Points with it, by the same cascade that takes its
|
||||
memories, because the story they named went with it.
|
||||
|
||||
## 9. Export / Import and Head Position
|
||||
|
||||
AI-DnD's current export carries branch information but reconstructs the imported head at the branch tip.
|
||||
@@ -460,6 +503,30 @@ Every row of an abandoned line is exported either way, so without that metadata
|
||||
restored campaign could not distinguish abandoned history from active history —
|
||||
which is precisely what a later cleanup or recovery feature has to select on.
|
||||
|
||||
### 9.2 Save Points in the bundle, as implemented in M4
|
||||
|
||||
Save Points are exported and imported with the campaign, which is `I04`. They
|
||||
fall on the "chosen" side of §9.1's rule without argument: a position someone
|
||||
named is not recoverable from the rows, since nothing about a turn records that
|
||||
a player once bookmarked it.
|
||||
|
||||
No format version bump. A bundle written before M4 has no `checkpoints` key and
|
||||
imports with none, which is what such a campaign had — the same unambiguous
|
||||
absence §9.1 relies on for the head depth, and the same treatment the persona
|
||||
block and the branch disposition received.
|
||||
|
||||
The head and the Save Points are independent, deliberately. An import opens the
|
||||
campaign where `headDepth` says, never at a Save Point merely because the file
|
||||
carries one: the bundle records where the story was being read and, separately,
|
||||
which positions were named, and choosing between them is the user's to make
|
||||
after the file is open.
|
||||
|
||||
A Save Point whose coordinate names no turn in the file is dropped rather than
|
||||
refusing the import — the opposite of the head depth's treatment, and for a
|
||||
stated reason. A misplaced head affects every read in the file; a bookmark
|
||||
pointing outside the story affects only itself, and rejecting a whole campaign
|
||||
to protect one bookmark would lose the story to save the pointer.
|
||||
|
||||
## 10. Authoritative Narrative State
|
||||
|
||||
### 10.1 Do not retain the RPG state protocol as the product model
|
||||
|
||||
+36
-2
@@ -1,8 +1,42 @@
|
||||
# Planning Package Version
|
||||
|
||||
- **Package:** Adventure Storyteller Planning Package v2.4
|
||||
- **Package:** Adventure Storyteller Planning Package v2.5
|
||||
- **Revision date:** 2026-09-03
|
||||
- **Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted**; M4 is next to brief.
|
||||
- **Status:** Phase 0 complete; architecture selected; **Milestones M1, M2 and M3 implemented and accepted**; **M4 implemented 2026-09-03 and awaiting review.**
|
||||
|
||||
## v2.5 — M4 Implementation (2026-09-03)
|
||||
|
||||
M4 added durable named Save Points. This revision records only what the
|
||||
implementation established as fact; **no product requirement changed**, and the
|
||||
milestone is **not** marked accepted — its review has not been written.
|
||||
|
||||
- `TECHNICAL-DESIGN.md` gains **§8.8** and **§9.2**: the Save Point as a name
|
||||
plus a coordinate holding no story, restore as head movement with a bounds
|
||||
check, the rule that the branch half of the head moves only when the
|
||||
coordinate is off the path being read, restore never forking, and the bundle
|
||||
carrying Save Points independently of the head.
|
||||
- `DATA-MODEL.md` **§8** records the pointer as implemented — `(branch, depth)`
|
||||
rather than a turn id, with the reason: one coordinate holds every attempt at
|
||||
a turn and exactly one is live, so a coordinate follows a retry where a row id
|
||||
would pin a superseded take. **§29** records the checkpoints in the export.
|
||||
- `BUILD-MILESTONES.md` **M4** gains a status block: what shipped, the one
|
||||
architectural decision the milestone had to make and why it needed no new ADR,
|
||||
the test count, and the outstanding browser condition.
|
||||
- `README.md` describes Save Points as a user-facing capability.
|
||||
|
||||
**No ADR was created.** ADR 012 already decides the architecture M4 needed —
|
||||
restore reuses active-head movement — and a table is not a decision. The one
|
||||
question ADR 012 does not answer, whether restore moves the branch half of the
|
||||
head, is that same mechanism applied to a coordinate ADR 012 already defines;
|
||||
`TECHNICAL-DESIGN.md` §8.8 records the answer rather than a new ADR asserting it.
|
||||
|
||||
`SPECIFICATION.md`, `SECURITY-THREAT-MODEL.md`, `STORY-BRANCH-SEMANTICS.md` and
|
||||
`V1-ACCEPTANCE-TESTS.md` are unchanged. M4 altered no product requirement, added
|
||||
no outbound path, and implemented the checkpoint semantics
|
||||
`STORY-BRANCH-SEMANTICS.md` §18-25 already specified rather than amending them.
|
||||
|
||||
**The browser smoke test remains unperformed, now for both M3 and M4.** No
|
||||
session has had a usable browser. See `BUILD-MILESTONES.md` M3 and M4.
|
||||
|
||||
## v2.4 — Documentation Consolidation (2026-09-03)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user