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
|
||||
|
||||
Reference in New Issue
Block a user