8.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
pnpm check # Merge gate — must pass before every commit (audit + knip + biome + oxlint + typecheck + test/coverage + jscpd)
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, routing, 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)
- 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 (unnecessary type assertions, deprecated APIs,
replaceAllpreference,String.raw). Configured in.oxlintrc.json. - TypeScript strict mode with
verbatimModuleSyntax. Use.jsextensions in relative imports when required by the repo's ESM settings (e.g.,./types.js). - 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
packages/*/src/__tests__/*.test.ts. Test pure functions directly; map acceptance scenarios and invariants from specs to individualit()blocks. - Feature specs live in
specs/NNN-feature-name/with spec.md (and optionally plan.md, tasks.md for new work). Specs describe features, not individual changes. The project constitution is at.specify/memory/constitution.md. - Component props — max 8 explicitly declared props per component interface (enforced by
scripts/check-component-props.mjs). Use React context for shared state; reserve props for per-instance config (data items, layout variants, refs). - Quality gates are enforced at pre-commit via Lefthook's
pnpm check— the project's single earliest enforcement point. No gate may exist only as a CI step or manual process.
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
Speckit (/speckit.* skills) manages the spec-driven development pipeline. Specs are living documents that describe features, not individual changes.
Issue-driven workflow
/write-issue— create a well-structured Gitea issue via interactive interview/integrate-issue <number>— fetch an issue, route it to the right spec, and update the spec with the new/changed requirements. Then implement directly./sync-issue <number>— push acceptance criteria from the spec back to the Gitea issue
RPI skills (Research → Plan → Implement)
rpi-research— deep codebase research producing a written report indocs/agents/research/rpi-plan— interactive phased implementation plan indocs/agents/plans/rpi-implement— execute a plan file phase by phase with automated + manual verification
Choosing the right workflow by scope
| 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 |
Speckit manages what to build (specs as living documents). RPI manages how to build it (research, planning, execution). The full speckit pipeline is for new features. For changes to existing features, update the spec via /integrate-issue, then use RPI skills if the change is non-trivial.
Current feature specs
specs/001-combatant-management/— CRUD, persistence, clear, batch add, confirm buttonsspecs/002-turn-tracking/— rounds, turn order, advance/retreat, top barspecs/003-combatant-state/— HP, AC, conditions, concentration, initiativespecs/004-bestiary/— search index, stat blocks, source management, panel UXspecs/005-player-characters/— persistent player character templates (CRUD), search & add to encounters, color/icon visual distinction,PlayerCharacterStoreport
Constitution (key principles)
The constitution (.specify/memory/constitution.md) governs all feature work:
- Deterministic Domain Core — Pure state transitions only; no I/O, randomness, or clocks in domain.
- Layered Architecture — Domain → Application → Adapters. Never skip layers or reverse dependencies.
- Clarification-First — Ask before making non-trivial assumptions.
- MVP Baseline — Say "MVP baseline does not include X", never permanent bans.
- Spec-driven features — Features are described in living specs; evolve existing specs via
/integrate-issue, create new ones via/speckit.specify. Bug fixes and tooling changes do not require specs.
Active Technologies
- TypeScript 5.8 (strict mode,
verbatimModuleSyntax) + React 19, Vite 6, Tailwind CSS v4, Lucide Reac (005-player-characters) - localStorage (new key
"initiative:player-characters") (005-player-characters)
Recent Changes
- 005-player-characters: Added TypeScript 5.8 (strict mode,
verbatimModuleSyntax) + React 19, Vite 6, Tailwind CSS v4, Lucide Reac