Skip to content

Content epochs Revision 2 — resolve red-team F1–F3 - #584

Merged
The-Running-Dev merged 4 commits into
mainfrom
design-content-epochs-redteam-fixes
Oct 8, 2026
Merged

The-Running-Dev merged 4 commits into
mainfrom
design-content-epochs-redteam-fixes

Conversation

@The-Running-Dev

@The-Running-Dev The-Running-Dev commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Resolves the three defects in design/redteam/2026-10-08-10-design.md (target 22af559). Each has a dated 2026-10-08 entry in design/90-decisions.md that cites its finding ID.

Changes

  • F1, BLOCKING. When a save that already carries an epoch entry migrates to a different campaignVersion, it now also gets a LoggedMigration { seq, system: "migration", from, to } entry.

    • Before this, a migrated save that already had content entries ended up in a state that C26 and deserialize refuse.
    • C3, C22 and C26 are restated over epoch entries, meaning content entries and migration entries together. §2 and §10.2 change to match.
    • A migration over a log with no epoch entry appends nothing and keeps formatVersion: 1. A host that migrates but never adopts writes the same saves it writes today.
    • A save carrying a migration entry can no longer be replayed. branchSession refuses it, capture records none, and the §14 harness fails a fixture that carries one.
    • Retention and the string table only count epochs after the last migration entry.
    • Design: 16-content-epochs §3.5.
  • F2, LOCAL. Story-graph adoptContent now also requires two things after the new content is applied:

    • the node at currentNodeId must still be a ChoiceNode;
    • at least one of its choices must be available.

    Adoption runs no settle, so without this check a player could be left on a node with no possible action. Contract: story-graph §8.1. Design: 16 §5.5.

  • F3, STRUCTURAL. getStrings(sessionId) now returns the union of the string tables of every epoch the session is still using. Where two epochs define the same key, the newer one wins.

    • New invariant C27: a session's table never loses a key.
    • A client that misses a key fetches the table again.
    • No type, signature or MCP schema changes.
    • Design: 16 §5.7 and Clients §2.

Generated pages are regenerated, and the guide's content-epochs subsection is updated and stamped.

Decisions signed off in review

  • F1 scope: the migration entry is added only when the save already carries an epoch entry. The first draft added it to every migration that changes the campaign version, and that was narrowed in review. The narrower rule keeps the byte-identical promise for hosts that never adopt.
  • F3: the union strings lookup is kept. A key that both epochs define shows the newer wording, which matches the content a stale client's action will resolve against.

Not in this PR

  • A capture gap that predates content epochs: 08 never checks replayCompatible, so a migrated session with no migration entry can be captured. It is recorded in 90-decisions.md's open register under Found by the content-epochs red-team revision.

Verified

  • ./build/Test-Documentation.ps1: drift check passed across 19 generated pages and the guide, and link and terminology checks passed across 135 Markdown files.
  • git diff --check: clean.
  • Not run locally: the production Docusaurus build, because docs.ps1 is not installed. Rely on this PR's Verify Documentation Build check.
  • Not run: the engine tests. This PR changes design only; no code under src/engine/ changed.

F1: a migration that changes campaignVersion appends a LoggedMigration entry, so a migrated
content-bearing save stays inside C26; the entry ends replay (branch, capture, harness refuse it).
F2: story-graph adoption requires the current node to stay a ChoiceNode with an available choice,
since adoption runs no settle.
F3: getStrings returns the union of the session's live epochs, newest winning per key (new C27),
rather than a Scene epoch stamp plus a getStrings version parameter.
A migration appends a LoggedMigration only when the log already carries an epoch entry, so a
host that migrates but never adopts keeps formatVersion 1 and byte-identical saves (engine
rollback stays safe). Capture now refuses every replayCompatible: false session, since a migrated
log without epoch entries carries no entry to refuse on.
Refusing every replayCompatible: false session fixes a capture gap that predates content epochs,
so it is a separate unit, not part of F1. 08 is back to refusing a session whose log carries a
migration entry, and the gap is in the open register.
@The-Running-Dev
The-Running-Dev enabled auto-merge (squash) October 8, 2026 21:47
@The-Running-Dev
The-Running-Dev merged commit c4824db into main Oct 8, 2026
8 checks passed
@The-Running-Dev
The-Running-Dev deleted the design-content-epochs-redteam-fixes branch October 8, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant