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

199 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- [x] T001 Install `idb` dependency in `apps/web/package.json` via `pnpm --filter web add idb`
- [x] T002 Add `BestiaryIndexEntry` and `BestiaryIndex` readonly interfaces to `packages/domain/src/creature-types.ts``BestiaryIndexEntry` 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[])
- [x] 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
- [x] 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
- [x] 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)
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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
- [x] 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)
```text
# 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
```text
# 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