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