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:
JesseMarkowitz
2026-09-03 13:54:44 -04:00
co-authored by Claude Opus 5
parent 7f082b61d8
commit c8755c21c2
10 changed files with 2216 additions and 144 deletions
+102
View File
@@ -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.
## 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
@@ -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.
## 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
@@ -369,6 +436,41 @@ dropped.
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
+37 -1
View File
@@ -74,6 +74,14 @@ story_profile:
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.
Potential model roles:
@@ -109,7 +117,26 @@ Rules:
- 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.
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
@@ -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.
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
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
View File
@@ -1,7 +1,7 @@
# Adventure Storyteller Planning Package
**Status:** Phase 0 complete; architecture selected; **Milestones M1 and M2 implemented and accepted (2026-09-02)**.
**Production coding:** Underway, milestone by milestone. M1 and M2 are done; M3 is the next milestone to brief.
**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, 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.
@@ -168,8 +168,13 @@ Milestone M2 COMPLETE (2026-09-02)
policy
|
v
Milestone M3 NEXT — brief not yet prepared
non-destructive undo/redo
Milestone M3 COMPLETE (2026-09-03)
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
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.**
M1 and M2 are complete and accepted; the evidence is in `reports/M1-*.md` and
`reports/M2-*.md`. **No M3 brief has been prepared.** The current action is to
write one, informed by the post-M2 corrections below and by
`reports/M2-IMPLEMENTATION-REPORT.md` §Q, which records that M3's chokepoints
were left untouched or simplified by M2 and that the Phase 0B undo/redo spike
still applies.
M1, M2 and M3 are complete and accepted; the evidence is in `reports/M1-*.md`,
`reports/M2-*.md` and `reports/M3-IMPLEMENTATION-REPORT.md` — the last of which
is M3's primary evidence record as well as its review, since no separate M3
baseline report was produced. **No M4 brief has been prepared.** The current
action is to write one, informed by the post-M3 corrections below, by the note
`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)
+77
View File
@@ -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.
### 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
Undo must restore more than visible transcript text.
@@ -242,6 +265,28 @@ User action
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
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.
## 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
The user should be able to correct authoritative story state without rewriting prose.
+88
View File
@@ -361,6 +361,58 @@ Abandoned history must:
- stop influencing current state/context/memory/summary,
- 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
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.
### 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.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.
**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
Retain AI-DnD's useful lineage-aware memory foundation, but align it with the product authority model.
+63 -2
View File
@@ -1,7 +1,8 @@
# Adventure Storyteller — V1 Acceptance Tests
**Status:** v1.1 planning/release contract — updated after Phase 0B, and after M2 for
the security contract (H10 strengthened, H12 added)
**Status:** v1.2 planning/release contract — updated after Phase 0B, after M2 for
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.
## 1. Test Philosophy
@@ -575,6 +576,17 @@ All retained turns can be traversed backward safely.
### Partial
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
@@ -689,6 +701,26 @@ Mara wears a green cloak.
- downstream state is re-evaluated,
- 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
@@ -1358,6 +1390,21 @@ No external API credentials are embedded in campaign export.
- import does not silently Redo to the newest retained turn,
- 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
@@ -1490,6 +1537,20 @@ No condition exists where:
- branch head advances incorrectly,
- 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
+43 -2
View File
@@ -1,8 +1,49 @@
# Planning Package Version
**Package:** Adventure Storyteller Planning Package v2.2
**Package:** Adventure Storyteller Planning Package v2.3
**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)
File diff suppressed because it is too large Load Diff