T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)
This commit is contained in:
349
specs/001-advance-turn/plan.md
Normal file
349
specs/001-advance-turn/plan.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# 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 1–8 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.
|
||||
Reference in New Issue
Block a user