Files
initiative/specs/001-advance-turn/plan.md

350 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Plan: Advance Turn
**Branch**: `001-advance-turn` | **Date**: 2026-03-03 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-advance-turn/spec.md`
## Summary
Implement the AdvanceTurn domain operation as a pure function that
transitions an Encounter to the next combatant, wrapping rounds and
emitting TurnAdvanced / RoundAdvanced domain events. Stand up the
pnpm monorepo skeleton, Biome tooling, and Vitest test harness so
that all constitution merge-gate requirements are satisfied from the
first commit.
## Technical Context
**Node**: 22 LTS (pinned via `.nvmrc`)
**Language/Version**: TypeScript 5.8 (strict mode)
**Primary Dependencies**: React 19 (pin to major; minor upgrades
allowed), Vite 6.2
**Storage**: In-memory only (MVP baseline)
**Testing**: Vitest 3.0
**Lint/Format**: Biome 2.0.0 (exact version, single tool — no
Prettier, no ESLint)
**Package Manager**: pnpm 10.6 (pinned via `packageManager` field
in root `package.json`)
**Target Platform**: Static web app (modern browsers)
**Project Type**: Monorepo — library packages + web app
**Performance Goals**: N/A (walking skeleton)
**Constraints**: Domain package must have zero React/Vite imports
**Scale/Scope**: Single feature, ~5 source files, ~1 test file
## Constitution Check
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Deterministic Domain Core | PASS | `advanceTurn` is a pure function; no I/O, randomness, or clocks |
| II. Layered Architecture | PASS | `packages/domain``packages/application``apps/web`; strict dependency direction enforced by automated import check |
| III. Agent Boundary | N/A | Agent layer out of scope for this feature |
| IV. Clarification-First | PASS | No ambiguous decisions remain; spec fully clarified |
| V. Escalation Gates | PASS | Scope strictly limited to spec; out-of-scope items listed |
| VI. MVP Baseline Language | PASS | No permanent bans; "MVP baseline does not include" used |
| VII. No Gameplay Rules | PASS | No gameplay mechanics in plan or constitution |
| Merge Gate | PASS | `pnpm check` script runs format, lint, typecheck, test |
## Project Structure
### Documentation (this feature)
```text
specs/001-advance-turn/
├── spec.md
└── plan.md
```
### Source Code (repository root)
```text
packages/
├── domain/
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── types.ts # CombatantId, Combatant, Encounter
│ ├── events.ts # TurnAdvanced, RoundAdvanced, DomainEvent
│ ├── advance-turn.ts # advanceTurn pure function
│ └── index.ts # public barrel export
├── application/
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── ports.ts # EncounterStore port interface
│ ├── advance-turn-use-case.ts
│ └── index.ts
apps/
└── web/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── index.html
└── src/
├── main.tsx
├── App.tsx
└── hooks/
└── use-encounter.ts
# Testing (co-located with domain package)
packages/domain/
└── src/
└── __tests__/
└── advance-turn.test.ts
# Root config
├── .nvmrc # pins Node 22
├── pnpm-workspace.yaml
├── biome.json
├── tsconfig.base.json
└── package.json # packageManager field pins pnpm; root scripts
```
**Structure Decision**: pnpm workspace monorepo with two packages
(`domain`, `application`) and one app (`web`). Domain is
framework-agnostic TypeScript. Application imports domain only.
Web app (React + Vite) imports both.
## Tooling & Merge Gate
### Scripts (root package.json)
```jsonc
{
"scripts": {
"format": "biome format --write .",
"format:check": "biome format .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"typecheck": "tsc --build",
"test": "vitest run",
"test:watch": "vitest",
"check": "biome check . && tsc --build && vitest run"
}
}
```
`pnpm check` is the single merge gate: format + lint + typecheck +
test. The layer boundary check runs as a Vitest test (see below),
so it executes as part of `vitest run` — no separate script needed.
### Layer Boundary Enforcement
Biome does not natively support cross-package import restrictions.
A lightweight `scripts/check-layer-boundaries.mjs` script will:
1. Scan `packages/domain/src/**/*.ts` — assert zero imports from
`@initiative/application`, `apps/`, `react`, `vite`.
2. Scan `packages/application/src/**/*.ts` — assert zero imports
from `apps/`, `react`, `vite`.
3. Exit non-zero on violation with a clear error message.
This script is invoked by a Vitest test
(`packages/domain/src/__tests__/layer-boundaries.test.ts`) so it
runs automatically as part of `vitest run` inside `pnpm check`.
No separate `check:layer` script is needed — the layer boundary
check is guaranteed to execute on every merge-gate run.
### Biome Configuration (biome.json)
```jsonc
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"organizeImports": { "enabled": true },
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineWidth": 80
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
```
### TypeScript Configuration
- `tsconfig.base.json` at root: strict mode, composite projects,
path aliases (`@initiative/domain`, `@initiative/application`).
- Each package extends `tsconfig.base.json` with its own
`include`/`references`.
- `apps/web/tsconfig.json` references both packages.
## Milestones
### Milestone 1: Tooling & Domain (walking skeleton)
Stand up monorepo, Biome, Vitest, TypeScript project references,
and implement the complete AdvanceTurn domain logic with all tests.
**Exit criteria**: `pnpm check` passes. All 8 acceptance scenarios
green. Layer boundary check green. No React or Vite dependencies in
`packages/domain` or `packages/application`.
### Milestone 2: Application + Minimal Web Shell
Wire up the application use case and a minimal React UI that
displays the encounter state and has a single "Next Turn" button.
**Exit criteria**: `pnpm check` passes. Clicking "Next Turn" in the
browser advances the turn with correct round wrapping. The app
builds with `vite build`.
## Task List
### Phase 1: Setup (Milestone 1)
- [X] **T001** Initialize pnpm workspace and root config
- Create `pnpm-workspace.yaml` listing `packages/*` and `apps/*`
- Create `.nvmrc` pinning Node 22
- Create root `package.json` with `packageManager` field pinning
pnpm 10.6 and scripts (check, test, lint, format, typecheck)
- Create `biome.json` at root
- Create `tsconfig.base.json` (strict, composite, path aliases)
- **Acceptance**: `pnpm install` succeeds; `biome check .` runs
without config errors
- [X] **T002** [P] Create `packages/domain` package skeleton
- `package.json` (name: `@initiative/domain`, no dependencies)
- `tsconfig.json` extending base, composite: true
- Empty `src/index.ts`
- **Acceptance**: `tsc --build packages/domain` succeeds
- [X] **T003** [P] Create `packages/application` package skeleton
- `package.json` (name: `@initiative/application`,
depends on `@initiative/domain`)
- `tsconfig.json` extending base, references domain
- Empty `src/index.ts`
- **Acceptance**: `tsc --build packages/application` succeeds
- [X] **T004** [P] Create `apps/web` package skeleton
- `package.json` with React, Vite, depends on both packages
- `tsconfig.json` referencing both packages
- `vite.config.ts` (minimal)
- `index.html` + `src/main.tsx` + `src/App.tsx` (placeholder)
- **Acceptance**: `pnpm --filter web dev` starts; `vite build`
succeeds
- [X] **T005** Configure Vitest
- Add `vitest` as root dev dependency
- Create `vitest.config.ts` at root (workspace mode) or per
package as needed
- Verify `pnpm test` runs (0 tests, exits clean)
- **Acceptance**: `pnpm test` exits 0
- [X] **T006** Create layer boundary check script
- `scripts/check-layer-boundaries.mjs`: scans domain and
application source for forbidden imports
- `packages/domain/src/__tests__/layer-boundaries.test.ts`:
wraps the script as a Vitest test
- **Acceptance**: test passes on clean skeleton; fails if a
forbidden import is manually added (verify, then remove)
### Phase 2: Domain Implementation (Milestone 1)
- [ ] **T007** Define domain types in `packages/domain/src/types.ts`
- `CombatantId` (branded string or opaque type)
- `Combatant` (carries a CombatantId)
- `Encounter` (combatants array, activeIndex, roundNumber)
- Factory function `createEncounter` that validates INV-1, INV-2,
INV-3
- **Acceptance**: types compile; `createEncounter([])` returns
error; `createEncounter([a])` returns valid Encounter
- [ ] **T008** [P] Define domain events in
`packages/domain/src/events.ts`
- `TurnAdvanced { previousCombatantId, newCombatantId,
roundNumber }`
- `RoundAdvanced { newRoundNumber }`
- `DomainEvent = TurnAdvanced | RoundAdvanced`
- **Acceptance**: types compile; events are plain data (no
classes with methods)
- [ ] **T009** Implement `advanceTurn` in
`packages/domain/src/advance-turn.ts`
- Signature: `(encounter: Encounter) =>
{ encounter: Encounter; events: DomainEvent[] } | DomainError`
- Implements FR-001 through FR-005
- Returns error for empty combatant list (INV-1)
- Emits TurnAdvanced on every call (INV-5)
- Emits TurnAdvanced then RoundAdvanced on wrap (event order
contract)
- **Acceptance**: compiles; satisfies type contract
- [ ] **T010** Write tests for all 8 acceptance scenarios +
invariants in
`packages/domain/src/__tests__/advance-turn.test.ts`
- Scenarios 18 from spec (Given/When/Then)
- INV-1: empty encounter rejected
- INV-2: activeIndex always in bounds (property check across
scenarios)
- INV-3: roundNumber never decreases
- INV-4: determinism — same input produces same output (call
twice, assert deep equal)
- INV-5: every success emits at least TurnAdvanced
- Event ordering: on wrap, events array is
[TurnAdvanced, RoundAdvanced] in that order
- **Acceptance**: `pnpm test` — all tests green; `pnpm check` —
full pipeline green
- [ ] **T011** Export public API from `packages/domain/src/index.ts`
- Re-export types, events, `advanceTurn`, `createEncounter`
- **Acceptance**: consuming packages can
`import { advanceTurn } from "@initiative/domain"`
**Milestone 1 checkpoint**: `pnpm check` passes (format + lint +
typecheck + test + layer boundaries). All 8 scenarios + invariants
green.
### Phase 3: Application + Web Shell (Milestone 2)
- [ ] **T012** Define port interface in
`packages/application/src/ports.ts`
- `EncounterStore` port: `get(): Encounter`, `save(e: Encounter)`
- **Acceptance**: compiles; no imports from adapters or React
- [ ] **T013** Implement `AdvanceTurnUseCase` in
`packages/application/src/advance-turn-use-case.ts`
- Accepts `EncounterStore` port
- Calls `advanceTurn` from domain, saves result, returns events
- **Acceptance**: compiles; imports only from `@initiative/domain`
and local ports
- [ ] **T014** Export public API from
`packages/application/src/index.ts`
- Re-export use case and port types
- **Acceptance**: consuming app can import from
`@initiative/application`
- [ ] **T015** Implement `useEncounter` hook in
`apps/web/src/hooks/use-encounter.ts`
- In-memory implementation of `EncounterStore` port (React state)
- Exposes current encounter state + `advanceTurn` action
- Initializes with a hardcoded 3-combatant encounter for demo
- **Acceptance**: hook compiles; usable in a React component
- [ ] **T016** Wire up `App.tsx`
- Display: current combatant name, round number, combatant list
with active indicator
- Single "Next Turn" button calling the use case
- Display emitted events (optional, for demo clarity)
- **Acceptance**: `vite build` succeeds; clicking "Next Turn"
cycles through combatants and increments rounds correctly
**Milestone 2 checkpoint**: `pnpm check` passes. App runs in
browser. Full round-trip from button click → domain pure function →
UI update verified manually.
## Risks & Open Questions
| # | Item | Severity | Mitigation |
|---|------|----------|------------|
| 1 | pnpm workspace + TypeScript project references can have path resolution quirks with Vite | Low | Use `vite-tsconfig-paths` plugin if needed; test early in T004 |
| 2 | Biome config format may change across versions | Low | Pinned to exact 2.0.0; `$schema` in config validates structure |
| 3 | Layer boundary script is a lightweight grep — not a full architectural fitness function | Low | Sufficient for walking skeleton; can upgrade to a Biome plugin or `dependency-cruiser` later if needed |
## Complexity Tracking
No constitution violations. No complexity justifications needed.