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
59 lines
2.0 KiB
Python
59 lines
2.0 KiB
Python
"""The adventure endpoints, split across one module per group of routes.
|
|
|
|
Importing this package registers every route, because each endpoint module
|
|
decorates its handlers with the shared `router` from `deps`. The import order
|
|
below is the order FastAPI matches paths in. No two routes here shadow each
|
|
other, so the order is for reading rather than for correctness.
|
|
|
|
Read the modules in this order to follow a turn from end to end:
|
|
|
|
deps the router and the ownership check every endpoint runs
|
|
paging reading a window of actions and numbering its attempts
|
|
nodes moving around the story tree
|
|
turns playing a turn, and the lock that allows only one at a time
|
|
takes retries and the attempts that collect at one coordinate
|
|
branches where a story splits
|
|
checkpoints Save Points: durable names for positions the head can return to
|
|
|
|
What this package re-exports, and what it deliberately does not:
|
|
|
|
Pure helpers and handlers are re-exported below, so `adventures.ACTION_PAGE` and
|
|
`adventures.undo_turn` keep working. The names a test replaces are not, and you
|
|
must reach those as `adventures.turns.<name>`. Rebinding a re-exported alias
|
|
changes only the alias, so patching `adventures.generate_turn` would leave every
|
|
caller reading the original. Leaving those names off raises `AttributeError`
|
|
instead, which is the failure you want.
|
|
"""
|
|
from .deps import router
|
|
|
|
# Imported for the side effect of registering routes. The names are unused here.
|
|
from . import ( # noqa: F401
|
|
crud,
|
|
turns,
|
|
takes,
|
|
branches,
|
|
checkpoints,
|
|
bundle_io,
|
|
refresh,
|
|
insights,
|
|
memories,
|
|
actions,
|
|
)
|
|
from ... import limits # noqa: F401 `adventures.limits` is patched by tests.
|
|
from .crud import SNIPPET_MAX, _snippet
|
|
from .paging import ACTION_PAGE
|
|
from .takes import redo_turn, retry_action, undo_turn
|
|
from .turns import world_delta_of
|
|
|
|
__all__ = [
|
|
"ACTION_PAGE",
|
|
"SNIPPET_MAX",
|
|
"_snippet",
|
|
"limits",
|
|
"redo_turn",
|
|
"retry_action",
|
|
"router",
|
|
"undo_turn",
|
|
"world_delta_of",
|
|
]
|