Files
initiative/specs/008-persist-encounter/tasks.md

7.5 KiB

Tasks: Persist Encounter

Input: Design documents from /specs/008-persist-encounter/ Prerequisites: plan.md, spec.md, research.md, data-model.md

Tests: Included -- persistence logic warrants unit tests to cover serialization, validation, and error handling.

Organization: Tasks are grouped by user story to enable independent implementation and testing of each story.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
  • Include exact file paths in descriptions

Phase 1: Setup

Purpose: Create the persistence module structure

  • T001 Create persistence module directory at apps/web/src/persistence/

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Core storage adapter that all user stories depend on

  • T002 Implement saveEncounter(encounter: Encounter): void function in apps/web/src/persistence/encounter-storage.ts that serializes encounter state to localStorage under key "initiative:encounter" using JSON.stringify, wrapped in try/catch that silently swallows errors (quota exceeded, storage unavailable)
  • T003 Implement loadEncounter(): Encounter | null function in apps/web/src/persistence/encounter-storage.ts that reads from localStorage key "initiative:encounter", parses JSON, performs structural shape checks (object with combatants array, activeIndex number, roundNumber number; each combatant has id string and name string), rehydrates CombatantId values via combatantId(), validates through createEncounter, and returns null on any failure (parse error, shape mismatch, domain validation failure)
  • T004 Write unit tests for saveEncounter and loadEncounter in apps/web/src/persistence/__tests__/encounter-storage.test.ts covering: round-trip save/load preserves encounter state, loadEncounter returns null when localStorage is empty, returns null for non-JSON strings, returns null for JSON missing required fields, returns null for invalid encounter data (e.g. empty combatants array, out-of-bounds activeIndex)

Checkpoint: Storage adapter is complete and tested -- user story implementation can now begin


Phase 3: User Story 1 - Encounter Survives Page Reload (Priority: P1) MVP

Goal: Encounter state persists across page reloads with zero data loss

Independent Test: Set up an encounter with combatants, initiative values, and advanced turns. Call saveEncounter, then loadEncounter and verify all state matches. In the hook, verify useEffect triggers saveEncounter on state changes.

Tests for User Story 1

  • T005 [US1] Write tests in apps/web/src/persistence/__tests__/encounter-storage.test.ts for: round-trip preserves combatant IDs, names, and initiative values; round-trip preserves activeIndex and roundNumber; saving after modifications (add/remove combatant, change initiative) persists the latest state

Implementation for User Story 1

  • T006 [US1] Modify useEncounter hook in apps/web/src/hooks/use-encounter.ts to initialize state from loadEncounter() -- if it returns a valid encounter, use it instead of createDemoEncounter(); derive nextId counter from highest numeric suffix in existing combatant IDs (parse c-{N} pattern)
  • T007 [US1] Add a useEffect in apps/web/src/hooks/use-encounter.ts that calls saveEncounter(encounter) whenever the encounter state changes

Checkpoint: User Story 1 is fully functional -- encounter survives page reload


Phase 4: User Story 2 - Fresh Start with No Saved Data (Priority: P2)

Goal: First-time users see the default demo encounter

Independent Test: With no localStorage data, load the app and verify the demo encounter (Aria, Brak, Cael) is displayed.

Implementation for User Story 2

  • T008 [US2] Verify and document in tests at apps/web/src/persistence/__tests__/encounter-storage.test.ts that loadEncounter() returns null when localStorage has no "initiative:encounter" key, confirming the useEncounter hook falls back to createDemoEncounter()

Checkpoint: User Story 2 confirmed -- no saved data results in demo encounter


Phase 5: User Story 3 - Graceful Handling of Corrupt Data (Priority: P3)

Goal: Corrupt or invalid saved data never crashes the app; falls back to demo encounter

Independent Test: Write various malformed values to the "initiative:encounter" localStorage key and verify loadEncounter() returns null for each.

Tests for User Story 3

  • T009 [US3] Add tests in apps/web/src/persistence/__tests__/encounter-storage.test.ts for corrupt data scenarios: non-object JSON (string, number, array, null), object with wrong types for fields (combatants as string, activeIndex as string), combatant entries missing id or name, valid JSON structure but domain-invalid data (zero combatants, negative roundNumber)

Implementation for User Story 3

  • T010 [US3] Verify loadEncounter() structural checks in apps/web/src/persistence/encounter-storage.ts cover all corrupt data scenarios from T009 -- adjust shape validation if any test cases reveal gaps

Checkpoint: All corrupt data scenarios handled gracefully


Phase 6: Polish & Cross-Cutting Concerns

  • T011 Run pnpm check to verify formatting, linting, type checking, and all tests pass
  • T012 Run quickstart.md manual verification steps to validate end-to-end behavior

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies
  • Foundational (Phase 2): Depends on Phase 1 -- BLOCKS all user stories
  • User Story 1 (Phase 3): Depends on Phase 2
  • User Story 2 (Phase 4): Depends on Phase 2 (independent of US1)
  • User Story 3 (Phase 5): Depends on Phase 2 (independent of US1, US2)
  • Polish (Phase 6): Depends on all user stories complete

User Story Dependencies

  • User Story 1 (P1): Depends on Foundational only. Core save/load wiring in the hook.
  • User Story 2 (P2): Depends on Foundational only. Verifies the null-fallback path.
  • User Story 3 (P3): Depends on Foundational only. Hardens validation against corrupt data.

Parallel Opportunities

  • T002 and T003 are sequential (same file, T003 depends on T002's storage key constant)
  • T005 and T006 can run in parallel (test file vs hook file)
  • T008 and T009 target the same test file as T004/T005 (encounter-storage.test.ts); sequence them after T005 to avoid merge conflicts. T006/T007 (hook file) can still run in parallel with test tasks.
  • US2 and US3 can proceed in parallel after Foundational phase

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Setup (T001)
  2. Complete Phase 2: Foundational (T002-T004)
  3. Complete Phase 3: User Story 1 (T005-T007)
  4. STOP and VALIDATE: Run pnpm check, manually test reload behavior
  5. This alone delivers the core value of the feature

Incremental Delivery

  1. Setup + Foundational -> Storage adapter ready
  2. Add User Story 1 -> Reload persistence works (MVP!)
  3. Add User Story 2 -> First-time experience confirmed
  4. Add User Story 3 -> Corrupt data resilience hardened
  5. Polish -> Final validation

Notes

  • All new code is in the adapter layer (apps/web/); domain and application packages are unchanged
  • Tests use a localStorage mock (Vitest's jsdom environment or manual mock)
  • The nextId counter derivation (T006) is critical to avoid ID collisions after reload
  • Commit after each phase or logical group of tasks