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>
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
undoStack.length <= 50at all timesredoStackis empty after any non-undo/redo actionundoStack.length + redoStack.lengthrepresents the total history depth (not capped as a whole — redo can grow up to 50 if all actions are undone)- Each stack entry is a valid, complete
Encountersnapshot - Undo followed by redo returns the encounter to the exact same state