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>
151 lines
8.2 KiB
Markdown
151 lines
8.2 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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`, via `idb` wrapper)
|
|
- **`data/bestiary/index.json`** — pre-built search index for creature lookup, generated by `scripts/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 `.js` extensions in relative imports.
|
|
- **Branded types** for identity values (e.g., `CombatantId`). Prefer immutability/`readonly` where practical.
|
|
- **Domain events** are plain data objects with a `type` discriminant — 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`](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>`:
|
|
|
|
```typescript
|
|
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 via `AdapterContext` instead
|
|
- **Mocking contexts**: Use `AllProviders` and drive state through real hooks instead of `vi.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(...)`) over `expect(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 one `expect()`. 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`](.specify/memory/constitution.md). Key rules: deterministic domain core, strict layer boundaries, clarification before assumptions.
|