T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
.env*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.tmp
|
||||
*.swp
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
@@ -1,50 +1,145 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
<!--
|
||||
Sync Impact Report
|
||||
───────────────────
|
||||
Version change: 1.0.2 → 1.0.3 (PATCH — add merge-gate rule)
|
||||
Modified sections:
|
||||
- Development Workflow: added automated-checks merge gate
|
||||
Templates requiring updates:
|
||||
- .specify/templates/plan-template.md ✅ no update needed
|
||||
- .specify/templates/spec-template.md ✅ no update needed
|
||||
- .specify/templates/tasks-template.md ✅ no update needed
|
||||
Follow-up TODOs: none
|
||||
-->
|
||||
# Encounter Console Constitution
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
### I. Deterministic Domain Core
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
All domain logic MUST be implemented as pure state transitions.
|
||||
Given the same input state and action, the output state MUST be
|
||||
identical across runs. Side effects (I/O, randomness, clocks) MUST
|
||||
be injected at the boundary, never sourced inside the domain layer.
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
- State transitions MUST be expressed as pure functions.
|
||||
- Random values (e.g., dice rolls) MUST be passed in as resolved
|
||||
inputs, not generated within the domain.
|
||||
- Domain functions MUST NOT depend on wall-clock time, network, or
|
||||
filesystem.
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
### II. Layered Architecture
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
The codebase MUST be organized into four layers with strict
|
||||
dependency direction:
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
1. **Domain** — pure types, state transitions, validation rules.
|
||||
No imports from other layers.
|
||||
2. **Application** — orchestrates domain operations, manages
|
||||
workflows, coordinates use cases. Application defines port
|
||||
interfaces that Adapters implement. May import Domain only.
|
||||
3. **Adapters** — I/O, persistence, UI rendering, external APIs.
|
||||
May import Application and Domain.
|
||||
4. **Agent** — AI-assisted features (suggestions, analysis).
|
||||
May import Application and Domain as read-only consumers.
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
A module in an inner layer MUST NOT import from an outer layer.
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
### III. Agent Boundary
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
The agent layer MAY read domain events and current state. The agent
|
||||
MAY produce suggestions, annotations, or recommendations. The agent
|
||||
MUST NOT mutate domain state directly. All agent-originated changes
|
||||
MUST flow through the Application layer as explicit user-confirmed
|
||||
commands.
|
||||
|
||||
- Agent output MUST be clearly labeled as suggestions.
|
||||
- No silent or automatic application of agent recommendations.
|
||||
|
||||
### IV. Clarification-First
|
||||
|
||||
Before making any non-trivial assumption during specification,
|
||||
planning, or implementation, the agent MUST surface a clarification
|
||||
question to the user. "Non-trivial" means any decision that would
|
||||
alter observable behavior, data model shape, or public API surface.
|
||||
The agent MUST also ask when multiple valid interpretations exist,
|
||||
when a choice would affect architectural layering, or when scope
|
||||
would expand beyond the current spec. The agent MUST NOT silently
|
||||
choose among valid alternatives.
|
||||
|
||||
### V. Escalation Gates
|
||||
|
||||
Any feature, requirement, or scope change not present in the current
|
||||
spec MUST be rejected at implementation time until the spec is
|
||||
explicitly updated. The workflow is:
|
||||
|
||||
1. Identify the out-of-scope item.
|
||||
2. Document it as a proposal.
|
||||
3. Update the spec via `/speckit.specify` or `/speckit.clarify`.
|
||||
4. Only then proceed with implementation.
|
||||
|
||||
### VI. MVP Baseline Language
|
||||
|
||||
Constraints in this constitution and in specs MUST use MVP baseline
|
||||
language ("MVP baseline does not include X") rather than permanent
|
||||
architectural bans ("X is forbidden"). This preserves the option to
|
||||
add capabilities in future iterations without constitutional
|
||||
amendment. The current MVP baseline is local-first and single-user;
|
||||
this is a starting scope, not a permanent restriction.
|
||||
|
||||
### VII. No Gameplay Rules in Constitution
|
||||
|
||||
This constitution MUST NOT contain concrete gameplay mechanics,
|
||||
rule-system specifics, or encounter resolution logic. Such details
|
||||
belong in feature specs. The constitution governs process,
|
||||
architecture, and quality — not product behavior.
|
||||
|
||||
## Scope Constraints
|
||||
|
||||
- The Encounter Console's primary focus is initiative tracking and
|
||||
encounter state management. Adjacent capabilities (e.g., richer
|
||||
game-engine features) are not in the MVP baseline but may be
|
||||
added via spec updates in future iterations.
|
||||
- Technology choices, UI framework, and storage mechanism are
|
||||
spec-level decisions, not constitutional mandates.
|
||||
- Testing strategy (unit, integration, contract) is determined per
|
||||
feature spec; the constitution requires only that domain logic
|
||||
remains testable via pure-function assertions.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- No change may be merged unless all automated checks (tests and
|
||||
static analysis as defined by the project) pass.
|
||||
- Every feature begins with a spec (`/speckit.specify`).
|
||||
- Implementation follows the plan → tasks → implement pipeline.
|
||||
- Domain logic MUST be testable without mocks for external systems.
|
||||
- Long-running or multi-step state transitions SHOULD be verifiable
|
||||
through reproducible event logs or snapshot-style tests.
|
||||
- Commits SHOULD be atomic and map to individual tasks where
|
||||
practical.
|
||||
- Layer boundary compliance MUST be verified by automated import
|
||||
rules or architectural tests. Agent-assisted or manual review MAY
|
||||
supplement but not replace automated checks.
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
This constitution is the highest-authority document for the
|
||||
Encounter Console project. All specs, plans, and implementations
|
||||
MUST comply with its principles.
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
**Amendment procedure**:
|
||||
1. Propose change with rationale.
|
||||
2. Update constitution via `/speckit.constitution`.
|
||||
3. Verify downstream template compatibility (Sync Impact Report).
|
||||
4. A human maintainer MUST review and commit the update before
|
||||
ratification takes effect.
|
||||
5. Commit with version bump.
|
||||
|
||||
**Versioning policy**: MAJOR.MINOR.PATCH per semantic versioning.
|
||||
- MAJOR: principle removal or backward-incompatible redefinition.
|
||||
- MINOR: new principle or materially expanded guidance.
|
||||
- PATCH: clarification, typo, or non-semantic refinement.
|
||||
|
||||
**Compliance review**: Every spec and plan MUST include a
|
||||
Constitution Check section validating adherence to all principles.
|
||||
|
||||
**Version**: 1.0.3 | **Ratified**: 2026-03-03 | **Last Amended**: 2026-03-03
|
||||
|
||||
12
apps/web/index.html
Normal file
12
apps/web/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Initiative Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
apps/web/package.json
Normal file
23
apps/web/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --build && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@initiative/application": "workspace:*",
|
||||
"@initiative/domain": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
3
apps/web/src/App.tsx
Normal file
3
apps/web/src/App.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function App() {
|
||||
return <div>Initiative Tracker</div>;
|
||||
}
|
||||
12
apps/web/src/main.tsx
Normal file
12
apps/web/src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (root) {
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
14
apps/web/tsconfig.json
Normal file
14
apps/web/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../packages/domain" },
|
||||
{ "path": "../../packages/application" }
|
||||
]
|
||||
}
|
||||
6
apps/web/vite.config.ts
Normal file
6
apps/web/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
33
biome.json
Normal file
33
biome.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/dist/**",
|
||||
"!.claude/**",
|
||||
"!.specify/**",
|
||||
"!specs/**"
|
||||
]
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
11
packages/application/package.json
Normal file
11
packages/application/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@initiative/application",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@initiative/domain": "workspace:*"
|
||||
}
|
||||
}
|
||||
1
packages/application/src/index.ts
Normal file
1
packages/application/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
10
packages/application/tsconfig.json
Normal file
10
packages/application/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "../domain" }]
|
||||
}
|
||||
8
packages/domain/package.json
Normal file
8
packages/domain/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@initiative/domain",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
16
packages/domain/src/__tests__/layer-boundaries.test.ts
Normal file
16
packages/domain/src/__tests__/layer-boundaries.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { checkLayerBoundaries } from "../../../../scripts/check-layer-boundaries.mjs";
|
||||
|
||||
describe("layer boundaries", () => {
|
||||
it("domain and application packages have no forbidden imports", () => {
|
||||
const violations = checkLayerBoundaries();
|
||||
if (violations.length > 0) {
|
||||
const messages = violations.map(
|
||||
(v) =>
|
||||
`${v.file}:${v.line} imports "${v.importPath}" (forbidden: ${v.forbidden})`,
|
||||
);
|
||||
throw new Error(`Layer boundary violations:\n ${messages.join("\n ")}`);
|
||||
}
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
1
packages/domain/src/index.ts
Normal file
1
packages/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
10
packages/domain/tsconfig.json
Normal file
10
packages/domain/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__tests__"]
|
||||
}
|
||||
1550
pnpm-lock.yaml
generated
Normal file
1550
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "apps/*"
|
||||
109
scripts/check-layer-boundaries.mjs
Normal file
109
scripts/check-layer-boundaries.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
|
||||
const FORBIDDEN = {
|
||||
"packages/domain/src": [
|
||||
"@initiative/application",
|
||||
"apps/",
|
||||
"react",
|
||||
"react-dom",
|
||||
"vite",
|
||||
],
|
||||
"packages/application/src": ["apps/", "react", "react-dom", "vite"],
|
||||
};
|
||||
|
||||
/** Directories to skip when collecting files */
|
||||
const SKIP_DIRS = new Set(["__tests__", "node_modules"]);
|
||||
|
||||
/** @param {string} dir */
|
||||
function collectTsFiles(dir) {
|
||||
/** @type {string[]} */
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_DIRS.has(entry.name)) {
|
||||
results.push(...collectTsFiles(full));
|
||||
}
|
||||
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an import path matches a forbidden module.
|
||||
* "vite" should match "vite" and "vite/foo" but not "vitest".
|
||||
* @param {string} importPath
|
||||
* @param {string} forbidden
|
||||
*/
|
||||
function matchesForbidden(importPath, forbidden) {
|
||||
if (forbidden.endsWith("/")) {
|
||||
return importPath.startsWith(forbidden);
|
||||
}
|
||||
return importPath === forbidden || importPath.startsWith(`${forbidden}/`);
|
||||
}
|
||||
|
||||
/** @returns {{ file: string, line: number, importPath: string, forbidden: string }[]} */
|
||||
export function checkLayerBoundaries() {
|
||||
/** @type {{ file: string, line: number, importPath: string, forbidden: string }[]} */
|
||||
const violations = [];
|
||||
|
||||
for (const [srcDir, forbidden] of Object.entries(FORBIDDEN)) {
|
||||
const absDir = join(ROOT, srcDir);
|
||||
let files;
|
||||
try {
|
||||
files = collectTsFiles(absDir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const match = line.match(
|
||||
/(?:import|from)\s+["']([^"']+)["']|require\s*\(\s*["']([^"']+)["']\s*\)/,
|
||||
);
|
||||
if (!match) continue;
|
||||
const importPath = match[1] || match[2];
|
||||
for (const f of forbidden) {
|
||||
if (matchesForbidden(importPath, f)) {
|
||||
violations.push({
|
||||
file: relative(ROOT, file),
|
||||
line: i + 1,
|
||||
importPath,
|
||||
forbidden: f,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
// Run as CLI if invoked directly
|
||||
if (
|
||||
process.argv[1] &&
|
||||
(process.argv[1].endsWith("check-layer-boundaries.mjs") ||
|
||||
process.argv[1] === new URL(import.meta.url).pathname)
|
||||
) {
|
||||
const violations = checkLayerBoundaries();
|
||||
if (violations.length > 0) {
|
||||
console.error("Layer boundary violations found:");
|
||||
for (const v of violations) {
|
||||
console.error(
|
||||
` ${v.file}:${v.line} — imports "${v.importPath}" (forbidden: ${v.forbidden})`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("No layer boundary violations found.");
|
||||
}
|
||||
}
|
||||
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.
|
||||
160
specs/001-advance-turn/spec.md
Normal file
160
specs/001-advance-turn/spec.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Feature Specification: Advance Turn
|
||||
|
||||
**Feature Branch**: `001-advance-turn`
|
||||
**Created**: 2026-03-03
|
||||
**Status**: Draft
|
||||
**Input**: Walking-skeleton domain feature — deterministic turn advancement
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Advance Turn (Priority: P1)
|
||||
|
||||
A game master running an encounter advances the turn to the next
|
||||
combatant in initiative order. When the last combatant in the round
|
||||
finishes, the round number increments and play wraps to the first
|
||||
combatant.
|
||||
|
||||
**Why this priority**: This is the irreducible core of an initiative
|
||||
tracker. Without turn advancement, no other feature has meaning.
|
||||
|
||||
**Independent Test**: Can be fully tested as a pure state transition
|
||||
with no I/O, persistence, or UI. Given an Encounter value and an
|
||||
AdvanceTurn action, assert the resulting Encounter value and emitted
|
||||
domain events.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an encounter with combatants [A, B, C], activeIndex 0,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn,
|
||||
**Then** activeIndex is 1, roundNumber is 1,
|
||||
and a TurnAdvanced event is emitted with
|
||||
previousCombatantId A, newCombatantId B, roundNumber 1.
|
||||
|
||||
2. **Given** an encounter with combatants [A, B, C], activeIndex 1,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn,
|
||||
**Then** activeIndex is 2, roundNumber is 1,
|
||||
and a TurnAdvanced event is emitted with
|
||||
previousCombatantId B, newCombatantId C, roundNumber 1.
|
||||
|
||||
3. **Given** an encounter with combatants [A, B, C], activeIndex 2,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn,
|
||||
**Then** activeIndex is 0, roundNumber is 2,
|
||||
and events are emitted in order: TurnAdvanced
|
||||
(previousCombatantId C, newCombatantId A, roundNumber 2) then
|
||||
RoundAdvanced (newRoundNumber 2).
|
||||
|
||||
4. **Given** an encounter with combatants [A, B, C], activeIndex 2,
|
||||
roundNumber 5,
|
||||
**When** AdvanceTurn,
|
||||
**Then** activeIndex is 0, roundNumber is 6,
|
||||
and events are emitted in order: TurnAdvanced then RoundAdvanced
|
||||
(verifies round increment is not hardcoded to 2).
|
||||
|
||||
5. **Given** an encounter with a single combatant [A], activeIndex 0,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn,
|
||||
**Then** activeIndex is 0, roundNumber is 2,
|
||||
and events are emitted in order: TurnAdvanced
|
||||
(previousCombatantId A, newCombatantId A, roundNumber 2) then
|
||||
RoundAdvanced (newRoundNumber 2).
|
||||
|
||||
6. **Given** an encounter with combatants [A, B], activeIndex 0,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn is applied twice in sequence,
|
||||
**Then** after the first: activeIndex 1, roundNumber 1;
|
||||
after the second: activeIndex 0, roundNumber 2.
|
||||
|
||||
7. **Given** an encounter with an empty combatant list,
|
||||
**When** AdvanceTurn,
|
||||
**Then** the operation MUST fail with an invalid-encounter error.
|
||||
No events are emitted. State is unchanged.
|
||||
|
||||
8. **Given** an encounter with combatants [A, B, C], activeIndex 0,
|
||||
roundNumber 1,
|
||||
**When** AdvanceTurn is applied three times,
|
||||
**Then** the encounter completes a full round cycle:
|
||||
activeIndex returns to 0 and roundNumber is 2.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Empty combatant list: AdvanceTurn MUST reject with an error.
|
||||
- Single combatant: every advance wraps and increments the round.
|
||||
- Large round numbers: no overflow or special-case behavior; round
|
||||
increments uniformly.
|
||||
|
||||
## Domain Model *(mandatory)*
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Combatant**: An identified participant in the encounter. For this
|
||||
feature, a combatant is an opaque identity (e.g., a name or id).
|
||||
The MVP baseline does not include HP, conditions, or stats.
|
||||
- **Encounter**: The aggregate root. Contains an ordered list of
|
||||
combatants (pre-sorted by initiative), an activeIndex pointing to
|
||||
the current combatant, and a roundNumber (positive integer,
|
||||
starting at 1).
|
||||
|
||||
### Domain Events
|
||||
|
||||
- **TurnAdvanced**: Emitted on every successful AdvanceTurn.
|
||||
Carries: previousCombatantId, newCombatantId, roundNumber.
|
||||
- **RoundAdvanced**: Emitted when activeIndex wraps past the last
|
||||
combatant. Carries: newRoundNumber.
|
||||
|
||||
When a round boundary is crossed, both TurnAdvanced and
|
||||
RoundAdvanced MUST be emitted in that order (TurnAdvanced first).
|
||||
This emission order is part of the observable domain contract and
|
||||
MUST be verified by tests.
|
||||
|
||||
### Invariants
|
||||
|
||||
- **INV-1**: An encounter MUST have at least one combatant.
|
||||
Operations on an empty encounter MUST fail.
|
||||
- **INV-2**: activeIndex MUST always satisfy
|
||||
0 <= activeIndex < len(combatants).
|
||||
- **INV-3**: roundNumber MUST be a positive integer (>= 1) and MUST
|
||||
only increase (never decrease or reset).
|
||||
- **INV-4**: AdvanceTurn MUST be a pure function of the current
|
||||
encounter state. Given identical input, output MUST be identical.
|
||||
- **INV-5**: Every successful AdvanceTurn MUST emit at least one
|
||||
domain event (TurnAdvanced). No silent state changes.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The domain MUST expose an AdvanceTurn operation that
|
||||
accepts an Encounter and returns the next Encounter state plus
|
||||
emitted domain events.
|
||||
- **FR-002**: AdvanceTurn MUST increment activeIndex by 1, wrapping
|
||||
to 0 when past the last combatant.
|
||||
- **FR-003**: When activeIndex wraps to 0, roundNumber MUST
|
||||
increment by 1.
|
||||
- **FR-004**: AdvanceTurn on an empty encounter MUST return an error
|
||||
without modifying state or emitting events.
|
||||
- **FR-005**: Domain events MUST be returned as values from the
|
||||
operation, not dispatched via side effects.
|
||||
|
||||
### Out of Scope (MVP baseline does not include)
|
||||
|
||||
- Initiative rolling or combatant ordering logic
|
||||
- Hit points, damage, conditions, or status effects
|
||||
- Adding or removing combatants mid-encounter
|
||||
- Persistence, serialization, or storage
|
||||
- UI, CLI, or any adapter layer
|
||||
- Agent behavior or suggestions
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: All 8 acceptance scenarios pass as deterministic,
|
||||
pure-function tests with no I/O dependencies.
|
||||
- **SC-002**: Invariants INV-1 through INV-5 are verified by tests.
|
||||
- **SC-003**: The domain module has zero imports from application,
|
||||
adapter, or agent layers (layer boundary compliance).
|
||||
128
specs/001-advance-turn/tasks.md
Normal file
128
specs/001-advance-turn/tasks.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Tasks: Advance Turn
|
||||
|
||||
**Input**: Design documents from `/specs/001-advance-turn/`
|
||||
**Prerequisites**: plan.md (required), spec.md (required)
|
||||
|
||||
**Organization**: Tasks follow the phased structure from plan.md. There is only one user story (US1 — Advance Turn, P1), so phases map directly to the plan's milestones.
|
||||
|
||||
## Format: `[ID] [P?] [Story?] Description`
|
||||
|
||||
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||
- **[US1]**: User Story 1 — Advance Turn
|
||||
- Exact file paths included in every task
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (Milestone 1 — Tooling)
|
||||
|
||||
**Purpose**: Initialize pnpm monorepo, Biome, TypeScript, Vitest, and layer boundary enforcement
|
||||
|
||||
- [X] T001 Initialize pnpm workspace and root config — create `pnpm-workspace.yaml`, `.nvmrc` (Node 22), root `package.json` (with `packageManager` pinning pnpm 10.6 and scripts: check, test, lint, format, typecheck), `biome.json`, and `tsconfig.base.json` (strict, composite, path aliases)
|
||||
- [X] T002 [P] Create `packages/domain` package skeleton — `packages/domain/package.json` (`@initiative/domain`, no deps), `packages/domain/tsconfig.json` (extends base, composite), empty `packages/domain/src/index.ts`
|
||||
- [X] T003 [P] Create `packages/application` package skeleton — `packages/application/package.json` (`@initiative/application`, depends on `@initiative/domain`), `packages/application/tsconfig.json` (extends base, references domain), empty `packages/application/src/index.ts`
|
||||
- [X] T004 [P] Create `apps/web` package skeleton — `apps/web/package.json` (React 19, Vite 6.2, depends on both packages), `apps/web/tsconfig.json`, `apps/web/vite.config.ts`, `apps/web/index.html`, `apps/web/src/main.tsx`, `apps/web/src/App.tsx` (placeholder)
|
||||
- [X] T005 Configure Vitest — add `vitest` as root dev dependency, create `vitest.config.ts` at root (workspace mode or per-package), verify `pnpm test` exits 0
|
||||
- [X] T006 Create layer boundary check — `scripts/check-layer-boundaries.mjs` (scans domain/application for forbidden imports) and `packages/domain/src/__tests__/layer-boundaries.test.ts` (wraps script as Vitest test)
|
||||
|
||||
**Checkpoint**: `pnpm install` succeeds, `biome check .` runs, `tsc --build` compiles, `pnpm test` exits 0 with layer boundary test green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Domain Implementation — User Story 1: Advance Turn (Priority: P1) (Milestone 1)
|
||||
|
||||
**Goal**: Implement the complete AdvanceTurn domain logic as a pure function with all 8 acceptance scenarios and invariant tests.
|
||||
|
||||
**Independent Test**: Pure state transition — given an Encounter value and AdvanceTurn action, assert resulting Encounter and emitted domain events. No I/O, persistence, or UI needed.
|
||||
|
||||
- [ ] T007 [US1] Define domain types in `packages/domain/src/types.ts` — `CombatantId` (branded/opaque), `Combatant`, `Encounter` (combatants, activeIndex, roundNumber), factory `createEncounter` enforcing INV-1, INV-2, INV-3
|
||||
- [ ] T008 [P] [US1] Define domain events in `packages/domain/src/events.ts` — `TurnAdvanced`, `RoundAdvanced`, `DomainEvent` union (plain data, no classes)
|
||||
- [ ] T009 [US1] Implement `advanceTurn` in `packages/domain/src/advance-turn.ts` — pure function `(Encounter) => { encounter, events } | DomainError`, implements FR-001 through FR-005
|
||||
- [ ] T010 [US1] Write tests for all 8 acceptance scenarios + invariants in `packages/domain/src/__tests__/advance-turn.test.ts` — scenarios 1–8, INV-1 through INV-5, event ordering on round wrap
|
||||
- [ ] T011 [US1] Export public API from `packages/domain/src/index.ts` — re-export types, events, `advanceTurn`, `createEncounter`
|
||||
|
||||
**Checkpoint (Milestone 1)**: `pnpm check` passes (format + lint + typecheck + test + layer boundaries). All 8 scenarios + invariants green. No React/Vite imports in domain or application.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Application + Web Shell (Milestone 2)
|
||||
|
||||
**Goal**: Wire up the application use case and minimal React UI with a "Next Turn" button.
|
||||
|
||||
- [ ] T012 Define port interface in `packages/application/src/ports.ts` — `EncounterStore` port: `get(): Encounter`, `save(e: Encounter)`
|
||||
- [ ] T013 Implement `AdvanceTurnUseCase` in `packages/application/src/advance-turn-use-case.ts` — accepts `EncounterStore`, calls `advanceTurn`, saves result, returns events
|
||||
- [ ] T014 Export public API from `packages/application/src/index.ts` — re-export use case and port types
|
||||
- [ ] T015 Implement `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` — in-memory `EncounterStore` via React state, exposes encounter state + `advanceTurn` action, hardcoded 3-combatant demo
|
||||
- [ ] T016 Wire up `apps/web/src/App.tsx` — display current combatant, round number, combatant list with active indicator, "Next Turn" button, emitted events
|
||||
|
||||
**Checkpoint (Milestone 2)**: `pnpm check` passes. `vite build` succeeds. Clicking "Next Turn" cycles combatants and increments rounds correctly.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Phase 1 (Setup)**: No dependencies — start immediately
|
||||
- **Phase 2 (Domain)**: Depends on Phase 1 completion
|
||||
- **Phase 3 (App + Web)**: Depends on Phase 2 completion (needs domain types and `advanceTurn`)
|
||||
|
||||
### Within Phase 1
|
||||
|
||||
- T001 must complete first (workspace and root config)
|
||||
- T002, T003, T004 can run in parallel [P] after T001
|
||||
- T005 depends on T001 (needs root package.json)
|
||||
- T006 depends on T002 and T005 (needs domain package + Vitest)
|
||||
|
||||
### Within Phase 2
|
||||
|
||||
- T007 must complete first (types needed by everything)
|
||||
- T008 can run in parallel [P] with T007 (events are independent types)
|
||||
- T009 depends on T007 and T008 (uses types and events)
|
||||
- T010 depends on T009 (tests the implementation)
|
||||
- T011 depends on T007, T008, T009 (exports all public API)
|
||||
|
||||
### Within Phase 3
|
||||
|
||||
- T012 first (port interface)
|
||||
- T013 depends on T012 (uses port)
|
||||
- T014 depends on T013 (exports use case)
|
||||
- T015 depends on T014 (uses application layer)
|
||||
- T016 depends on T015 (uses hook)
|
||||
|
||||
---
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
```text
|
||||
# After T001 completes:
|
||||
T002, T003, T004 — all package skeletons in parallel
|
||||
|
||||
# After T007 starts:
|
||||
T008 — domain events can be written in parallel with types
|
||||
|
||||
# Independent stories: only one user story (US1), so parallelism is within-phase only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (Milestone 1)
|
||||
|
||||
1. Complete Phase 1: Setup (T001–T006)
|
||||
2. Complete Phase 2: Domain (T007–T011)
|
||||
3. **STOP and VALIDATE**: `pnpm check` passes, all 8 scenarios green
|
||||
|
||||
### Full Feature (Milestone 2)
|
||||
|
||||
4. Complete Phase 3: App + Web Shell (T012–T016)
|
||||
5. **VALIDATE**: `pnpm check` passes, app runs in browser
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All task IDs (T001–T016) match plan.md — no scope expansion
|
||||
- Single user story (US1: Advance Turn) — no cross-story dependencies
|
||||
- Tests (T010) are included as specified in plan.md and spec.md
|
||||
- Domain package must have zero React/Vite imports (enforced by T006)
|
||||
17
tsconfig.base.json
Normal file
17
tsconfig.base.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "packages/domain" },
|
||||
{ "path": "packages/application" },
|
||||
{ "path": "apps/web" }
|
||||
]
|
||||
}
|
||||
8
vitest.config.ts
Normal file
8
vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["packages/*/src/**/*.test.ts"],
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user