# Feature Specification: On-Demand Bestiary with Pre-Indexed Search **Feature Branch**: `029-on-demand-bestiary` **Created**: 2026-03-10 **Status**: Draft **Input**: User description: "Replace the bundled bestiary JSON with a two-tier architecture: a lightweight search index shipped with the app and on-demand full stat block data fetched at runtime and cached client-side." ## User Scenarios & Testing *(mandatory)* ### User Story 1 - Search All Creatures Instantly (Priority: P1) A DM searches for a creature by name. The search operates against a pre-shipped index of 3,312 creatures across 102 sources. Results appear instantly with the creature name and source display name (e.g., "Goblin (Monster Manual (2025))"). The DM selects a creature to add it to the encounter. Name, HP, AC, and initiative data are prefilled directly from the index — no network fetch required. **Why this priority**: This is the core interaction loop. Every session starts with adding creatures. Expanding from 1 source to 102 sources dramatically increases the app's usefulness, and doing it without any fetch latency preserves the current snappy UX. **Independent Test**: Can be fully tested by typing a creature name, seeing multi-source results, and adding a creature to verify HP/AC/initiative are populated correctly. **Acceptance Scenarios**: 1. **Given** the app is loaded, **When** the DM types "gob" in the search field, **Then** results include goblins from multiple sources, each labeled with the source display name, sorted alphabetically, limited to 10 results. 2. **Given** search results are visible, **When** the DM selects "Goblin (Monster Manual (2025))", **Then** a combatant is added with the correct name, HP, AC, and initiative modifier — no network request is made. 3. **Given** the app is loaded, **When** the DM types a single character, **Then** no results appear (minimum 2 characters required). 4. **Given** the app is loaded, **When** the DM searches for a creature that exists only in an obscure source (e.g., "Muk" from "Adventure with Muk"), **Then** the creature appears in results with its source name. --- ### User Story 2 - View Full Stat Block via On-Demand Source Fetch (Priority: P2) A DM clicks to view the stat block of a creature whose source data has not been loaded yet. The app displays a prompt: "Load [Source Display Name] bestiary data?" with a pre-filled URL pointing to the raw source file. The DM confirms, the app fetches the JSON, normalizes it, and caches all creatures from that source. The stat block then displays. For any subsequent creature from the same source, the stat block appears instantly without prompting. **Why this priority**: Stat blocks are essential for running combat but are secondary to adding creatures. This story also addresses the legal motivation — no copyrighted prose is shipped with the app. **Independent Test**: Can be tested by adding a creature, opening its stat block, confirming the fetch prompt, and verifying the stat block renders. Then opening another creature from the same source to verify no prompt appears. **Acceptance Scenarios**: 1. **Given** a creature from an uncached source is in the encounter, **When** the DM opens its stat block, **Then** a prompt appears asking to load the source data with an editable URL field pre-filled with the correct raw file URL. 2. **Given** the fetch prompt is visible, **When** the DM confirms the fetch, **Then** the app downloads the JSON, normalizes it, caches all creatures from that source, and displays the stat block. 3. **Given** source data for "Monster Manual (2025)" has been cached, **When** the DM opens the stat block for any other creature from that source, **Then** the stat block displays instantly with no prompt. 4. **Given** the fetch prompt is visible, **When** the DM edits the URL to point to a mirror or local server, **Then** the app fetches from the edited URL instead. 5. **Given** a creature is in the encounter, **When** the DM opens its stat block and the source is not cached, **Then** the creature's index data (HP, AC, etc.) remains visible in the combatant row regardless of whether the fetch succeeds. --- ### User Story 3 - Manual File Upload as Fetch Alternative (Priority: P3) A DM who cannot access the URL (corporate firewall, offline use) uses a file upload option to load bestiary data from a local JSON file. The file is processed identically to a fetched file — normalized and cached by source. **Why this priority**: Important for accessibility and offline scenarios, but most users will use the URL fetch. **Independent Test**: Can be tested by selecting a local JSON file in the upload dialog and verifying the stat blocks become available. **Acceptance Scenarios**: 1. **Given** the source fetch prompt is visible, **When** the DM chooses "Upload file" and selects a valid bestiary JSON from their filesystem, **Then** the app normalizes and caches the data, and stat blocks become available. 2. **Given** the DM uploads an invalid or malformed JSON file, **When** the upload completes, **Then** the app shows a user-friendly error message and allows retry. --- ### User Story 4 - Manage Cached Sources (Priority: P4) A DM wants to see which sources are cached, clear a specific source's cache, or clear all cached data. A settings/management UI provides this visibility and control. **Why this priority**: Cache management is a housekeeping task, not part of the core combat flow. Important for long-term usability but not needed for initial sessions. **Independent Test**: Can be tested by caching one or more sources, opening the management UI, verifying the list, and clearing individual or all caches. **Acceptance Scenarios**: 1. **Given** two sources have been cached, **When** the DM opens the source management UI, **Then** both sources are listed with their display names. 2. **Given** the source management UI is open, **When** the DM clears a single source, **Then** that source's data is removed and stat blocks for its creatures require re-fetching, while other cached sources remain. 3. **Given** the source management UI is open, **When** the DM clears all cached data, **Then** all source data is removed and all stat blocks require re-fetching. --- ### Edge Cases - What happens when the DM searches for a creature name that appears in multiple sources? Results show all matches, each labeled with the source display name, sorted alphabetically. - What happens when a network fetch fails mid-download? The app shows an error with the option to retry or change the URL. The creature remains in the encounter with its index data intact. - What happens when the DM adds a creature, caches its source, then clears the cache? The creature remains in the encounter with its index data. Opening the stat block triggers the fetch prompt again. - What happens when the fetched JSON does not match the expected format? The normalization adapter handles format variations as it does today. If normalization fails entirely, an error is shown. - What happens when persistent client-side storage is unavailable (private browsing, storage full)? The app falls back to in-memory caching for the current session and warns the user that data will not persist. - What happens when the browser is offline? The fetch prompt is shown but the fetch fails. The DM can use the file upload alternative. Previously cached sources remain available. ## Requirements *(mandatory)* ### Functional Requirements - **FR-001**: The app MUST ship a pre-generated search index containing creature name, source code, AC, HP average, DEX score, CR, initiative proficiency multiplier, size code, and creature type for all indexed creatures. - **FR-002**: The app MUST include a source display name map that translates source codes to human-readable names (e.g., "XMM" to "Monster Manual (2025)"). - **FR-003**: Search MUST operate against the full shipped index — case-insensitive substring match on creature name, minimum 2 characters, maximum 10 results, sorted alphabetically. - **FR-004**: Search results MUST display the source display name alongside the creature name. - **FR-005**: Adding a creature from search MUST populate name, HP, AC, and initiative data directly from the index without any network fetch. - **FR-006**: When a user views a stat block for a creature whose source is not cached, the app MUST display a prompt to load the source data. - **FR-007**: The source fetch prompt MUST include an editable URL field pre-filled with the default URL for that source's raw data file. - **FR-008**: On confirmation, the app MUST fetch the JSON, normalize it through the existing normalization pipeline, and cache all creatures from that source. - **FR-009**: Cached source data MUST persist across browser sessions using client-side storage. - **FR-010**: The app MUST provide a file upload option as an alternative to URL fetching, processing the uploaded file identically. - **FR-011**: The app MUST provide a management UI showing cached sources with options to clear individual sources or all cached data. - **FR-012**: The bundled full bestiary data file MUST be removed from the distributed app. - **FR-013**: The existing normalization adapter and tag-stripping utility MUST remain unchanged — they process fetched data exactly as before. - **FR-014**: If a fetch or upload fails, the app MUST show a user-friendly error message with options to retry or change the URL. The creature's index data MUST remain intact in the encounter. - **FR-015**: The source fetch prompt MUST appear once per source, not once per creature. After fetching a source, all its creatures' stat blocks become available. ### Key Entities - **Search Index**: Pre-shipped lightweight dataset containing mechanical facts (name, source, AC, HP, DEX, CR, initiative proficiency, size, type) for all creatures. Keyed by name + source for uniqueness. - **Source**: A D&D publication identified by a code (e.g., "XMM") with a display name (e.g., "Monster Manual (2025)"). Contains multiple creatures. Caching and fetching operate at the source level. - **Cached Source Data**: The full normalized bestiary data for a single source, stored in persistent client-side storage. Contains complete creature stat blocks including traits, actions, and descriptions. - **Creature (Index Entry)**: A lightweight record from the search index — sufficient for adding a combatant but insufficient for rendering a full stat block. - **Creature (Full)**: A complete creature record with all stat block data (traits, actions, legendary actions, etc.), available only after source data is fetched/uploaded and cached. ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001**: All 3,312 indexed creatures are searchable immediately on app load, with search results appearing within 100ms of typing. - **SC-002**: Adding a creature from search to the encounter completes without any network request and within 200ms. - **SC-003**: After a source is cached, stat blocks for any creature from that source display within 200ms with no additional prompt. - **SC-004**: The app ships zero copyrighted prose content — only mechanical facts and creature names in the index. - **SC-005**: Source data import (fetch or upload) for a typical source completes and becomes usable within 5 seconds on a standard broadband connection. - **SC-006**: Cached data persists across browser sessions — closing and reopening the browser does not require re-fetching previously loaded sources. - **SC-007**: The shipped app bundle size decreases compared to the current approach (removing the bundled full bestiary data, replacing with the lightweight index). ## Assumptions - The pre-generated search index (`data/bestiary/index.json`) is already available in the repository and maintained separately via the generation script. - The default fetch URLs follow a predictable pattern based on the source code, allowing the app to pre-fill the URL for each source. - Persistent client-side storage (e.g., IndexedDB) is available in all target browsers. Private browsing mode may limit persistence, handled as an edge case with in-memory fallback. - The existing normalization adapter can handle bestiary JSON from any of the 102 sources, not just the currently bundled one. If source-specific variations exist, adapter updates are implementation concerns. - Users are responsible for sourcing their own bestiary data files — the app provides the fetch mechanism but makes no guarantees about URL availability.