Replace direct adapter/persistence imports with context-based injection (AdapterContext + useAdapters) so tests use in-memory implementations instead of vi.mock. Migrate component tests from context mocking to AllProviders with real hooks. Extract export/import logic from ActionBar into useEncounterExportImport hook. Add bestiary-cache and bestiary-index-adapter test suites. Raise adapter coverage thresholds (68→80 lines, 56→62 branches). 77 test files, 891 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.2 KiB
CLAUDE.md
Initiative is a browser-based combat encounter tracker for tabletop RPGs (D&D 5.5e, Pathfinder 2e). It runs entirely client-side — no backend, no accounts — with localStorage and IndexedDB for persistence.
Commands
pnpm check # Merge gate — must pass before every commit (audit + knip + biome + oxlint + typecheck + test/coverage + jscpd + jsinspect)
pnpm oxlint # Type-aware linting (oxlint — complements Biome)
pnpm knip # Unused code detection (Knip)
pnpm test # Run all tests (Vitest)
pnpm test:watch # Tests in watch mode
pnpm typecheck # tsc --build (project references)
pnpm lint # Biome lint
pnpm format # Biome format (writes)
pnpm check:props # Component prop count enforcement (max 8)
pnpm --filter web dev # Vite dev server (localhost:5173)
pnpm --filter web build # Production build
Run a single test file: pnpm vitest run packages/domain/src/__tests__/advance-turn.test.ts
Architecture
Strict layered architecture with ports/adapters and enforced dependency direction:
apps/web (React 19 + Vite) → packages/application (use cases) → packages/domain (pure logic)
- Domain — Pure functions, no I/O, no framework imports. All state transitions are deterministic. Errors returned as values (
DomainError), never thrown. Adapters may throw only for programmer errors. - Application — Orchestrates domain calls via port interfaces (
EncounterStore,BestiarySourceCache). No business logic here. - Web — React adapter. Implements ports using hooks/state. All UI components and user interaction live here.
Layer boundaries are enforced by scripts/check-layer-boundaries.mjs, which runs as a Vitest test. Domain and application must never import from React, Vite, or upper layers.
Data & Storage
- localStorage — encounter persistence (adapter layer, JSON serialization)
- IndexedDB — bestiary source cache (
apps/web/src/adapters/bestiary-cache.ts, viaidbwrapper) data/bestiary/index.json— pre-built search index for creature lookup, generated byscripts/generate-bestiary-index.mjs
Project Structure
apps/web/ React app — components, hooks, adapters
packages/domain/src/ Pure state transitions, types, validation
packages/application/src/ Use cases, port interfaces
data/bestiary/ Bestiary search index
scripts/ Build tooling (layer checks, index generation)
specs/NNN-feature-name/ Feature specs (spec.md, plan.md, tasks.md)
.specify/ Speckit config (templates, scripts, constitution)
docs/agents/ RPI skill artifacts (research reports, plans)
.claude/skills/ Agent skills (rpi-research, rpi-plan, rpi-implement)
Tech Stack
- TypeScript 5.8 (strict mode,
verbatimModuleSyntax) - React 19, Vite 6, Tailwind CSS v4
- Lucide React (icons)
idb(IndexedDB wrapper for bestiary cache)- Biome 2.4 (formatting + linting), oxlint (type-aware linting), Knip (unused code), jscpd (copy-paste detection), jsinspect-plus (structural duplication)
- Vitest (testing, v8 coverage), Lefthook (pre-commit hooks)
Conventions
- Biome 2.4 for formatting and linting (no Prettier, no ESLint). Tab indentation, 80-char lines. Imports are auto-organized alphabetically.
- oxlint for type-aware linting that Biome can't do. Configured in
.oxlintrc.json. - TypeScript strict mode with
verbatimModuleSyntax. Use.jsextensions in relative imports. - Branded types for identity values (e.g.,
CombatantId). Prefer immutability/readonlywhere practical. - Domain events are plain data objects with a
typediscriminant — no classes. - Tests live in
__tests__/directories adjacent to source. See the Testing section below for conventions on mocking, assertions, and per-layer approach. - Quality gates are enforced at pre-commit via Lefthook (parallel jobs). No gate may exist only as a CI step or manual process.
For component prop rules, export format compatibility, and ADRs, see docs/conventions.md.
Testing
Philosophy
Test user-visible behavior, not implementation details. A good test answers "does this feature work?" not "does this internal function get called?"
Adapter Injection
Adapters (storage, cache, browser APIs) are provided via AdapterContext. Production wires real implementations; tests wire in-memory implementations. This means:
- No
vi.mock()for adapter or persistence modules - Tests control adapter behavior by configuring the in-memory implementation
- Type changes in adapter interfaces are caught at compile time
Per-Layer Approach
| Layer | How to test |
|---|---|
Domain (packages/domain) |
Pure unit tests, no mocks, test invariants and acceptance scenarios |
Application (packages/application) |
Mock port interfaces only, use real domain logic |
| Hooks (context-wrapped) | Test via renderHook with AllProviders wrapping in-memory adapters |
| Hooks (component-specific) | Test through the component that uses them |
| Components | Render with AllProviders, use in-memory adapters, use userEvent for interactions |
Test Data
Use factory functions from apps/web/src/__tests__/factories/ to construct domain objects. Each factory provides sensible defaults overridden via Partial<T>:
import { buildEncounter } from "../../__tests__/factories/build-encounter.js";
import { buildCombatant } from "../../__tests__/factories/build-combatant.js";
const encounter = buildEncounter({
combatants: [buildCombatant({ name: "Goblin" })],
activeIndex: 0,
roundNumber: 1,
});
Add new factory files as needed (one per domain type). Don't inline test data construction — use factories so type changes are caught at compile time.
Anti-Patterns
vi.mock()for adapters: Use in-memory adapter implementations viaAdapterContextinstead- Mocking contexts: Use
AllProvidersand drive state through real hooks instead ofvi.mock("../../contexts/..."). Exception: context mocks are acceptable when the component under test requires specific state machine states that cannot be reached through adapter configuration alone — document the reason in a comment at the top of the test file. - Stubbing child components: Render real children; stub only if the child has heavy I/O that can't be mocked at the adapter level
- Asserting mock call counts: Prefer asserting what the user sees (
screen.getByText(...)) overexpect(mockFn).toHaveBeenCalledWith(...) - Testing internal state: Don't assert
result.current.suggestionIndex === 0; assert the first suggestion is highlighted - Assertion-free tests: Every
it()block must contain at least oneexpect(). Tests that render without asserting inflate coverage without catching bugs.
Self-Review Checklist
Before finishing a change, consider:
- Is this the simplest approach that solves the current problem?
- Is there duplication that hurts readability? (But don't abstract prematurely.)
- Are errors handled correctly and communicated sensibly to the user?
- Does the UI follow modern patterns and feel intuitive to interact with?
Speckit Workflow
Specs are living documents in specs/NNN-feature-name/ that describe features, not individual changes. Use /speckit.* and RPI skills (rpi-research, rpi-plan, rpi-implement) to manage them — skill descriptions have full usage details.
| Scope | Workflow |
|---|---|
| Bug fix / CSS tweak | Just fix it, commit |
| Small change to existing feature | /integrate-issue → implement → commit |
| Larger addition to existing feature | /integrate-issue → rpi-research → rpi-plan → rpi-implement |
| New feature | /speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.implement |
Research scope: Always scan for existing patterns similar to what the feature needs. Identify extraction and consolidation opportunities before implementation, not during.
Constitution
Project principles governing all feature work are in .specify/memory/constitution.md. Key rules: deterministic domain core, strict layer boundaries, clarification before assumptions.