Files
initiative/specs/006-undo-redo/data-model.md
Lukas f6766b729d Rename spec 037-undo-redo to 006-undo-redo for sequential numbering
Delete merged feature branches (005–037) that inflated the auto-increment
counter in create-new-feature.sh, and renumber the undo-redo spec to
follow the existing 001–005 sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:32:29 +01:00

3.4 KiB

Data Model: Undo/Redo

Feature: 006-undo-redo Date: 2026-03-26

Entities

UndoRedoState

Represents the complete undo/redo history for an encounter session.

Field Type Description
undoStack Encounter[] Ordered list of encounter snapshots, most recent last. Max 50 entries.
redoStack Encounter[] Ordered list of encounter snapshots accumulated by undo operations. Cleared on any new action.

Encounter (existing, unchanged)

Each stack entry is a full Encounter snapshot as defined in packages/domain/src/types.ts. No schema changes to the encounter type.

Field Type Description
combatants Combatant[] Ordered list of combatants
activeIndex number Index of the active combatant
roundNumber number Current round number

State Transitions

pushUndo(state, snapshot) -> UndoRedoState

Push a snapshot onto the undo stack. If the stack exceeds 50 entries, drop the oldest (index 0). Clear the redo stack.

Precondition: snapshot is a valid Encounter Postcondition: undoStack length <= 50, redoStack is empty

undo(state, currentEncounter) -> { state: UndoRedoState, encounter: Encounter } | DomainError

Pop the most recent snapshot from the undo stack. Push the current encounter onto the redo stack. Return the popped snapshot as the new current encounter.

Precondition: undoStack is non-empty Postcondition: undoStack length decremented by 1, redoStack length incremented by 1 Error: "nothing-to-undo" if undoStack is empty

redo(state, currentEncounter) -> { state: UndoRedoState, encounter: Encounter } | DomainError

Pop the most recent snapshot from the redo stack. Push the current encounter onto the undo stack. Return the popped snapshot as the new current encounter.

Precondition: redoStack is non-empty Postcondition: redoStack length decremented by 1, undoStack length incremented by 1 Error: "nothing-to-redo" if redoStack is empty

clearHistory() -> UndoRedoState

Reset both stacks to empty. Used when the encounter is cleared.

Postcondition: undoStack and redoStack are both empty

Persistence

Storage Keys

Key Content Format
initiative:encounter:undo Undo stack JSON array of serialized Encounter objects
initiative:encounter:redo Redo stack JSON array of serialized Encounter objects

Serialization

Stacks are serialized as JSON arrays of Encounter objects, identical to the existing encounter serialization format. On load, each entry is validated using the same rehydration logic as loadEncounter().

Failure Modes

  • localStorage quota exceeded: Stacks continue in-memory; persistence is best-effort. Silently swallow write errors (matching existing encounter persistence pattern).
  • Corrupt data on load: Start with empty stacks. Log no error (matching existing pattern).
  • Schema mismatch after upgrade: Invalid entries are dropped during rehydration; stacks may be shorter than persisted but never contain invalid data.

Invariants

  1. undoStack.length <= 50 at all times
  2. redoStack is empty after any non-undo/redo action
  3. undoStack.length + redoStack.length represents the total history depth (not capped as a whole — redo can grow up to 50 if all actions are undone)
  4. Each stack entry is a valid, complete Encounter snapshot
  5. Undo followed by redo returns the encounter to the exact same state