Implement the 029-on-demand-bestiary feature that replaces the bundled XMM bestiary JSON with a compact search index (~350KB) and on-demand source loading, where users explicitly provide a URL or upload a JSON file to fetch full stat block data per source, which is then normalized and cached in IndexedDB (with in-memory fallback) so creature stat blocks load instantly on subsequent visits while keeping the app bundle small and never auto-fetching copyrighted content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
198
specs/029-on-demand-bestiary/tasks.md
Normal file
198
specs/029-on-demand-bestiary/tasks.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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 (T001–T003)
|
||||
2. Complete Phase 2: Foundational (T004–T006)
|
||||
3. Complete Phase 3: User Story 1 (T007–T010)
|
||||
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
|
||||
Reference in New Issue
Block a user