Files
initiative/specs/029-on-demand-bestiary/tasks.md

14 KiB
Raw Blame History

Tasks: On-Demand Bestiary with Pre-Indexed Search

Input: Design documents from /specs/029-on-demand-bestiary/ Prerequisites: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/

Tests: No test tasks included — not explicitly requested in the feature specification.

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 (Shared Infrastructure)

Purpose: Install dependencies and add domain types that all stories depend on

  • T001 Install idb dependency in apps/web/package.json via pnpm --filter web add idb
  • T002 Add BestiaryIndexEntry and BestiaryIndex readonly interfaces to packages/domain/src/creature-types.tsBestiaryIndexEntry has fields: name (string), source (string), ac (number), hp (number), dex (number), cr (string), initiativeProficiency (number), size (string), type (string). BestiaryIndex has fields: sources (Record<string, string>), creatures (readonly BestiaryIndexEntry[])
  • T003 Export BestiaryIndexEntry and BestiaryIndex types from packages/domain/src/index.ts

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Core adapters and port interfaces that MUST be complete before ANY user story can be implemented

CRITICAL: No user story work can begin until this phase is complete

  • T004 [P] Create apps/web/src/adapters/bestiary-index-adapter.ts — import data/bestiary/index.json as a Vite static JSON import; export a loadBestiaryIndex() function that maps compact index fields (n→name, s→source, ac→ac, hp→hp, dx→dex, cr→cr, ip→initiativeProficiency, sz→size, tp→type) to BestiaryIndex domain type; export getDefaultFetchUrl(sourceCode: string) returning https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/bestiary-{sourceCode-lowercase}.json; export getSourceDisplayName(sourceCode: string) resolving from the sources map
  • T005 [P] Create apps/web/src/adapters/bestiary-cache.ts — implement IndexedDB cache adapter using idb library; database name "initiative-bestiary", object store "sources" with keyPath "sourceCode"; implement functions: getCreature(creatureId) extracts source from ID prefix, looks up source record, finds creature by ID; isSourceCached(sourceCode) checks store; cacheSource(sourceCode, displayName, creatures) stores CachedSourceRecord with cachedAt timestamp and creatureCount; getCachedSources() returns all records' metadata; clearSource(sourceCode) deletes one record; clearAll() clears store; loadAllCachedCreatures() returns a Map<CreatureId, Creature> from all cached sources for in-memory lookup; include in-memory Map fallback if IndexedDB open fails (private browsing)
  • T006 [P] Add BestiarySourceCache port interface to packages/application/src/ports.ts — define interface with methods: getCreature(creatureId: CreatureId): Creature | undefined, isSourceCached(sourceCode: string): boolean; export from packages/application/src/index.ts

Checkpoint: Foundation ready — index adapter parses shipped data, cache adapter persists fetched data, port interface defined


Phase 3: User Story 1 — Search All Creatures Instantly (Priority: P1)

Goal: Replace the single-source bundled bestiary with multi-source index search. All 3,312 creatures searchable instantly. Adding a creature populates HP/AC/initiative from the index without any network fetch.

Independent Test: Type a creature name → see results from multiple sources with display names → select one → combatant added with correct HP, AC, initiative modifier. No network requests made.

Implementation for User Story 1

  • T007 [US1] Rewrite apps/web/src/hooks/use-bestiary.ts — replace the xmm.json dynamic import with loadBestiaryIndex() from bestiary-index-adapter; store BestiaryIndex in state; rewrite search(query) to filter index.creatures by case-insensitive substring on name (min 2 chars, max 10 results, sorted alphabetically), returning results with sourceDisplayName resolved from index.sources; keep getCreature(id) for stat block lookup (initially returns undefined for all — cache integration comes in US2); export searchIndex function and sourceDisplayName resolver; update BestiaryHook interface to expose search results as index entries with source display names
  • T008 [US1] Update apps/web/src/components/bestiary-search.tsx — modify search result rendering to show source display name alongside creature name (e.g., "Goblin (Monster Manual (2025))"); update the type of items from Creature to index-based search result type; ensure onSelectCreature callback receives the index entry data needed by addFromBestiary
  • T009 [US1] Update addFromBestiary in apps/web/src/hooks/use-encounter.ts — accept a BestiaryIndexEntry (or compatible object with name, hp, ac, dex, cr, initiativeProficiency, source) instead of requiring a full Creature; derive creatureId from {source.toLowerCase()}:{slugify(name)} using existing slug logic; call addCombatantUseCase, setHpUseCase(hp), setAcUseCase(ac); set creatureId on the combatant for later stat block lookup
  • T010 [US1] Remove data/bestiary/xmm.json from the repository (git rm); verify no remaining imports reference it; update any test files that imported xmm.json to use test fixtures or the index instead

Checkpoint: Search works across all 102 sources. Creatures can be added from any source with correct stats. No bundled copyrighted content. Stat blocks not yet available (US2).


Phase 4: User Story 2 — View Full Stat Block via On-Demand Source Fetch (Priority: P2)

Goal: When a stat block is opened for a creature whose source is not cached, prompt the user to fetch the source data from a URL. After fetching, normalize and cache all creatures from that source in IndexedDB. Subsequent lookups for any creature from that source are instant.

Independent Test: Add a creature → open its stat block → see fetch prompt with pre-filled URL → confirm → stat block renders. Open another creature from same source → stat block renders instantly with no prompt.

Implementation for User Story 2

  • T011 [P] [US2] Create apps/web/src/components/source-fetch-prompt.tsx — dialog/card component that displays "Load [sourceDisplayName] bestiary data?" with an editable URL input pre-filled via getDefaultFetchUrl(sourceCode) from bestiary-index-adapter; "Load" button triggers fetch; show loading spinner during fetch; on success call onSourceLoaded callback; on error show error message with retry option and option to change URL; include error state for network failures and normalization failures
  • T012 [US2] Integrate cache into apps/web/src/hooks/use-bestiary.ts — on mount, call loadAllCachedCreatures() from bestiary-cache to populate an in-memory Map<CreatureId, Creature>; update getCreature(id) to look up from this map; export isSourceCached(sourceCode) delegating to bestiary-cache; export fetchAndCacheSource(sourceCode, url) that fetches the URL, parses JSON, calls normalizeBestiary() from bestiary-adapter, calls cacheSource() from bestiary-cache, and updates the in-memory creature map; return cache loading state
  • T013 [US2] Update apps/web/src/components/stat-block-panel.tsx — when getCreature(creatureId) returns undefined for a combatant that has a creatureId, extract the source code from the creatureId prefix; if isSourceCached(source) is false, render SourceFetchPrompt instead of the stat block; after onSourceLoaded callback fires, re-lookup the creature and render the stat block; if source is cached but creature not found (edge case), show appropriate message

Checkpoint: Full stat block flow works end-to-end. Source data fetched once per source, cached in IndexedDB, persists across sessions.


Phase 5: User Story 3 — Manual File Upload as Fetch Alternative (Priority: P3)

Goal: Add a file upload option to the source fetch prompt so users can load bestiary data from a local JSON file when the URL is inaccessible.

Independent Test: Open source fetch prompt → click "Upload file" → select a local bestiary JSON → stat blocks become available.

Implementation for User Story 3

  • T014 [US3] Add file upload to apps/web/src/components/source-fetch-prompt.tsx — add an "Upload file" button/link below the URL fetch section; clicking opens a native file picker (<input type="file" accept=".json">); on file selection, read via FileReader, parse JSON, call normalizeBestiary(), call cacheSource(), and invoke onSourceLoaded callback; handle errors (invalid JSON, normalization failure) with user-friendly messages and retry option

Checkpoint: Both URL fetch and file upload paths work. Users in offline/restricted environments can still load stat blocks.


Phase 6: User Story 4 — Manage Cached Sources (Priority: P4)

Goal: Provide a UI for viewing which sources are cached and clearing individual or all cached data.

Independent Test: Cache one or more sources → open management UI → see cached sources listed → clear one → verify it requires re-fetch → clear all → verify all require re-fetch.

Implementation for User Story 4

  • T015 [P] [US4] Create apps/web/src/components/source-manager.tsx — component showing a list of cached sources via getCachedSources() from bestiary-cache; each row shows source display name, creature count, and a "Clear" button calling clearSource(sourceCode); include a "Clear All" button calling clearAll(); show empty state when no sources are cached; after clearing, update the in-memory creature map in use-bestiary
  • T016 [US4] Wire source manager into the app UI — add a settings/gear icon button (Lucide Settings icon) in the top bar or an accessible location that opens the SourceManager component in a dialog or panel; ensure it integrates with the existing layout without disrupting the encounter flow

Checkpoint: Cache management fully functional. Users can inspect and clear cached data.


Phase 7: Polish & Cross-Cutting Concerns

Purpose: Edge cases, fallbacks, and merge gate validation

  • T017 Verify IndexedDB-unavailable fallback in apps/web/src/adapters/bestiary-cache.ts — ensure that when IndexedDB open fails (e.g., private browsing), the adapter silently falls back to in-memory storage; show a non-blocking warning via console or UI toast that cached data will not persist across sessions
  • T018 Run pnpm check (knip + format + lint + typecheck + test) and fix all issues — ensure no unused exports from removed xmm.json references; verify layer boundary checks pass; fix any TypeScript errors from type changes; ensure Biome formatting is correct

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies — can start immediately
  • Foundational (Phase 2): Depends on Phase 1 completion — BLOCKS all user stories
  • US1 (Phase 3): Depends on Phase 2 — core search and add flow
  • US2 (Phase 4): Depends on Phase 3 — stat block fetch needs working search/add
  • US3 (Phase 5): Depends on Phase 4 — file upload extends the fetch prompt from US2
  • US4 (Phase 6): Depends on US2 (Phase 4) — T015 needs the in-memory creature map from T012
  • Polish (Phase 7): Depends on all user stories being complete

User Story Dependencies

  • US1 (P1): Requires Foundational (Phase 2) — no other story dependencies
  • US2 (P2): Requires US1 (uses rewritten use-bestiary hook and creatureId links)
  • US3 (P3): Requires US2 (extends source-fetch-prompt.tsx created in US2)
  • US4 (P4): Requires US2 (T012 establishes the in-memory creature map that T015 must update after clearing)

Within Each User Story

  • Adapter/model tasks before hook integration tasks
  • Hook integration before component tasks
  • Core implementation before edge cases

Parallel Opportunities

  • Phase 2: T004, T005, T006 can all run in parallel (different files, no dependencies)
  • Phase 3: T007 first, then T008/T009 can parallelize (different files), T010 after all
  • Phase 4: T011 can parallelize with T012 (different files), T013 after both
  • Phase 6: T015 depends on T012 (US2); T016 after T015

Parallel Example: Phase 2 (Foundational)

# All three foundational tasks can run simultaneously:
Task T004: "Create bestiary-index-adapter.ts"   (apps/web/src/adapters/)
Task T005: "Create bestiary-cache.ts"            (apps/web/src/adapters/)
Task T006: "Add BestiarySourceCache port"        (packages/application/src/ports.ts)

Parallel Example: User Story 1

# After T007 (rewrite use-bestiary.ts):
Task T008: "Update bestiary-search.tsx"          (apps/web/src/components/)
Task T009: "Update addFromBestiary"              (apps/web/src/hooks/use-encounter.ts)
# Then T010 after both complete

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Setup (T001T003)
  2. Complete Phase 2: Foundational (T004T006)
  3. Complete Phase 3: User Story 1 (T007T010)
  4. STOP and VALIDATE: Search works across 102 sources, creatures add with correct stats, no bundled copyrighted content
  5. Deploy/demo if ready — app is fully functional for adding creatures; stat blocks unavailable until US2

Incremental Delivery

  1. Setup + Foundational → Foundation ready
  2. US1 → Multi-source search and add (MVP!)
  3. US2 → On-demand stat blocks via URL fetch
  4. US3 → File upload alternative
  5. US4 → Cache management
  6. Polish → Edge cases and merge gate
  7. Each story adds value without breaking previous stories

Notes

  • [P] tasks = different files, no dependencies
  • [Story] label maps task to specific user story for traceability
  • US4 (cache management) is independent of US2/US3 and can be built in parallel if desired
  • The existing normalizeBestiary() and stripTags() are unchanged — no tasks needed for them
  • The existing stat-block.tsx rendering component is unchanged
  • The existing encounter-storage.ts persistence is unchanged
  • Commit after each task or logical group
  • Stop at any checkpoint to validate story independently