From f3a38d44be2bf15a3f6c87516050edbabd0be5f5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 16 Apr 2026 13:32:21 -0600 Subject: [PATCH] =?UTF-8?q?chore(root):=20scaffold=20monorepo=20=E2=80=94?= =?UTF-8?q?=20Phase=200=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 64 + .gitignore | 12 + .sisyphus/boulder.json | 14 + .sisyphus/plans/rete-rules-engine.md | 3215 ++++++++++++++++++++++++++ LICENSE | 21 + README.md | 22 + docs/PHASES.md | 194 ++ eslint.config.js | 38 + lefthook.yml | 4 + package.json | 27 + packages/chess/README.md | 1 + packages/chess/RULES.md | 371 +++ packages/chess/package.json | 21 + packages/chess/src/index.ts | 2 + packages/chess/tsconfig.json | 12 + packages/chess/vitest.config.ts | 9 + packages/rete/README.md | 1 + packages/rete/SPEC.md | 208 ++ packages/rete/package.json | 22 + packages/rete/src/index.ts | 3 + packages/rete/tsconfig.json | 9 + packages/rete/vitest.config.ts | 8 + packages/server/PROTOCOL.md | 291 +++ packages/server/README.md | 1 + packages/server/package.json | 15 + packages/server/src/index.ts | 2 + packages/server/tsconfig.json | 11 + packages/server/vitest.config.ts | 8 + playwright.config.ts | 18 + tsconfig.base.json | 19 + tsconfig.json | 9 + vitest.workspace.ts | 7 + 32 files changed, 4659 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .sisyphus/boulder.json create mode 100644 .sisyphus/plans/rete-rules-engine.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/PHASES.md create mode 100644 eslint.config.js create mode 100644 lefthook.yml create mode 100644 package.json create mode 100644 packages/chess/README.md create mode 100644 packages/chess/RULES.md create mode 100644 packages/chess/package.json create mode 100644 packages/chess/src/index.ts create mode 100644 packages/chess/tsconfig.json create mode 100644 packages/chess/vitest.config.ts create mode 100644 packages/rete/README.md create mode 100644 packages/rete/SPEC.md create mode 100644 packages/rete/package.json create mode 100644 packages/rete/src/index.ts create mode 100644 packages/rete/tsconfig.json create mode 100644 packages/rete/vitest.config.ts create mode 100644 packages/server/PROTOCOL.md create mode 100644 packages/server/README.md create mode 100644 packages/server/package.json create mode 100644 packages/server/src/index.ts create mode 100644 packages/server/tsconfig.json create mode 100644 packages/server/vitest.config.ts create mode 100644 playwright.config.ts create mode 100644 tsconfig.base.json create mode 100644 tsconfig.json create mode 100644 vitest.workspace.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6929408 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + name: Typecheck, Lint, Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Typecheck + run: bun run typecheck + - name: Lint + run: bun run lint + - name: Test with coverage + run: bun run test:coverage + + build: + name: Build packages + runs-on: ubuntu-latest + needs: check + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Build + run: bun run build + + audit: + name: Security audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Audit + run: bun audit || true + + # e2e: + # name: E2E (Playwright) + # runs-on: ubuntu-latest + # # Enabled in Phase 3 when chess UI exists diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b706fd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +.sisyphus/evidence/ +*.log +.DS_Store +coverage/ +playwright-report/ +test-results/ +.vite/ +*.tsbuildinfo +bun.lock +node-compile-cache/ diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json new file mode 100644 index 0000000..f59ca7c --- /dev/null +++ b/.sisyphus/boulder.json @@ -0,0 +1,14 @@ +{ + "active_plan": "/home/joey/Projects/rules/.sisyphus/plans/rete-rules-engine.md", + "started_at": "2026-04-16T19:18:14.222Z", + "session_ids": [ + "ses_26872eb19ffeDMaEoxdTsorFej", + "ses_26842c860ffec63Ov7Llrz7Lyg", + "ses_26843b5b0ffejVpXmmHcQ8CoUp", + "ses_268415bf0ffernrRKPtHro341E", + "ses_2684239eeffeaz52nJeIRqphnJ", + "ses_2683e067effeLzndjG25qlXH3G" + ], + "plan_name": "rete-rules-engine", + "agent": "atlas" +} \ No newline at end of file diff --git a/.sisyphus/plans/rete-rules-engine.md b/.sisyphus/plans/rete-rules-engine.md new file mode 100644 index 0000000..cba03b8 --- /dev/null +++ b/.sisyphus/plans/rete-rules-engine.md @@ -0,0 +1,3215 @@ +# TypeScript Rete-Based Rules Engine + Custom-Rules Chess Demo + +## TL;DR + +> **Quick Summary**: Build `@paratype/rete` (a Doorenbos-style Rete II rules engine in TypeScript with Immer-backed time-travel) and `@paratype/chess` (a browser chess game where every rule — including FIDE rules and 15 preset custom rules — is expressed as Rete productions) served by a `@paratype/chess-server` authoritative Bun WebSocket server for multiplayer. +> +> **Deliverables**: +> - `packages/rete`: Rete engine with alpha/beta nodes, joins, negation, NCC, existential, aggregation, derived facts, cycle detection, event log, N-tick Immer snapshots, TS builder API, JSON serialization (handler-registry pattern) +> - `packages/chess`: Browser chess game with FIDE rules expressed as Rete productions, 15 preset custom rules (toggle between games), React/Vite UI, localStorage persistence, JSON ruleset/game export-import +> - `packages/chess-server`: Bun WebSocket authoritative server with rooms, reconnection, move validation, deterministic broadcast +> - `docs/PHASES.md`, `packages/rete/SPEC.md`, `packages/chess/RULES.md`, `packages/server/PROTOCOL.md` (Phase 0 specification locks) +> - Full CI (typecheck, lint, Vitest with coverage, Playwright E2E, bundle-size, security audit) +> +> **Estimated Effort**: XL (5-phase plan, 70+ tasks) +> **Parallel Execution**: YES — heavy parallelism within waves, strict sequencing between phases +> **Critical Path**: Phase 0 specs → monorepo scaffold → Phase 1 alpha/beta network → Phase 2 advanced nodes → Phase 3 chess engine-as-rules → Phase 4 multiplayer server → Final QA + +--- + +## Context + +### Original Request +Build a TypeScript Rete-based rules engine for browser games (inspired by [paranim/pararules](https://github.com/paranim/pararules) in Nim), with immutable data structures enabling time-rewind and debugging. Use it to power a browser chess game with user-customizable rules at runtime (inspired by [chess.dougdoug.com](https://chess.dougdoug.com)). + +### Interview Summary + +**Key Decisions (all confirmed by user)**: +- **Repo**: Monorepo with `packages/rete` (engine), `packages/chess` (browser game), `packages/server` (WebSocket server) +- **Rule authoring**: Typed TS builder API + JSON serialization via handler-registry pattern (no `eval`, no function-to-string) +- **Fact model**: Strict EAV `(id, attr, value)` — pararules parity +- **Time-travel**: Event log + Immer snapshots every N ticks +- **Chess integration**: Chess rules ARE Rete productions (no chess.js) +- **Feature scope**: Full Doorenbos-style Rete II (alpha, beta, joins, negation, NCC, existential, aggregation, derived facts, cycle detection) +- **Tooling**: Bun workspaces + Vitest + tsc --noEmit + tsup (engine) + Vite (chess demo) +- **Testing**: TDD for engine core; tests-after + Playwright for chess demo; Playwright QA for all tasks +- **Chess v1**: 15 preset rules with toggle UI (between-games toggle only in v1) +- **Persistence**: localStorage auto-save + JSON export/import +- **Play mode**: Networked multiplayer via WebSocket +- **Networking**: Authoritative server — engine on server, clients send intents, server broadcasts events +- **Packaging**: `@paratype/rete`, `@paratype/chess`, `@paratype/chess-server`, MIT license + +**Research Findings** (condensed): +- **Pararules uses strict EAV, alpha+beta networks, joins, conditions, derived facts via `thenFinally`, cycle detection via recursion limit** — no native negation/aggregation/NCC; emulated via derived facts. User chose to extend to full Rete II. +- **No production-ready TS Rete engine exists** — Nools is dead (2019), Rete.js is a node editor (not a rules engine), node-rules is not true Rete. Must build greenfield. +- **Immer is the best immutable fit** — structural sharing, draft-based mutations, minimal API overhead. +- **Chess.js hardcodes FIDE rules** — no hook system; validates the "chess-as-rules" architectural choice. +- **Runtime rule injection is THE feature** for chess.dougdoug.com-style play. + +### Metis Review (Gaps Addressed) + +**Metis identified 36 ambiguity points and classified combined risk as non-linear.** Key resolutions now baked into this plan: + +- **"Rete II" is fixed**: Doorenbos 1995 thesis (unlinking, right/left activation, enumerated node types: alpha, beta, join, negation, NCC, existential, aggregation). No other interpretation accepted. +- **Phasing is mandatory**: 5 phases (0: specs → 1: pararules parity → 2: Rete II + local chess → 3: time-travel + presets + UI → 4: multiplayer). Phase N+1 tasks do not start until Phase N acceptance gate is green. +- **RHS serialization = handler-registry pattern**: JSON rules store `{conditions: [...], handler: "registeredName", args: [...]}`. Handlers are registered TS functions in a per-package registry. Zero `eval`, zero function-to-string, zero arbitrary JS in saved JSON. +- **Fact ID authority = server-minted** in multiplayer; client only references positions/piece-ids opaquely; deterministic across clients. +- **Conflict resolution = deterministic**: `salience desc → specificity desc → insertion order asc`. Documented in SPEC.md. +- **Match refraction = once per unique match** (CLIPS-style). Re-fires only when fact identity or bound variables change. +- **RHS purity contract**: No `Date.now()`, `Math.random()`, or I/O in rule RHS. Enforced via ESLint rule + dev-mode runtime guard that wraps globals in engine package. +- **Rule set immutability during active game** — in v1, rules cannot be added/removed mid-game; toggle only between games (simplifies time-travel + multiplayer determinism). +- **Preset rules are dev-authored TS** in v1 — no user JS upload (would be RCE vector on server). +- **Protocol versioning**: Every WebSocket message includes `v: 1`; mismatch = hard disconnect. + +--- + +## Work Objectives + +### Core Objective +Deliver a production-quality Rete-based rules engine in TypeScript (with time-travel via Immer) and a fully functional browser chess demo (networked multiplayer, 15 custom rule presets) that proves out the engine as a game-logic substrate. + +### Concrete Deliverables + +**Packages**: +- `packages/rete/` — Published as `@paratype/rete` (ESM + CJS + .d.ts via tsup) +- `packages/chess/` — Published as `@paratype/chess` (chess UI, Vite dev server, bundled static) +- `packages/server/` — Published as `@paratype/chess-server` (Bun HTTP+WebSocket server) + +**Specifications** (Phase 0 locks): +- `packages/rete/SPEC.md` — Engine semantics (fact shape, ID authority, conflict resolution, refraction, iteration order, truth maintenance, cycle limit, RHS purity, JSON schema, named Rete II reference) +- `docs/PHASES.md` — 5-phase plan with gates, non-goals, perf budgets, demo scenarios +- `packages/chess/RULES.md` — 15 concrete preset rules with compat matrix and test scenarios +- `packages/server/PROTOCOL.md` — WebSocket message schemas, reconnection flow, rate limits + +**Infrastructure**: +- `.github/workflows/ci.yml` — Typecheck, lint, test+coverage, Playwright, bundle-size, `bun audit` +- Root `bunfig.toml`, `tsconfig.base.json`, `eslint.config.js`, `vitest.workspace.ts`, `playwright.config.ts` +- Pre-commit hook enforcing `bun run check` (lefthook or simple-git-hooks) + +**Acceptance Gates** (per phase): executable verification commands (see each phase's wave). + +### Definition of Done + +Run from repo root: +```bash +bun install # → 0 errors +bun run check # tsc --noEmit + eslint + vitest → all green +bun run test:coverage # engine ≥90% line, chess ≥70%, server ≥80% +bun run playwright test # all E2E scenarios pass +bun run build # all three packages build → dist/ populated +bun run size-limit # engine < 50KB min+gz, chess < 200KB min+gz +bun run replay-determinism # hash of replayed state == recorded hash, 100% match +bun run server & sleep 1 && bun run test:integration # real WebSocket handshake, move exchange +``` + +### Must Have +- Doorenbos-style Rete II: alpha network, beta network, join nodes, negation nodes, NCC nodes, existential nodes, aggregation nodes, derived-fact production with `thenFinally`-equivalent semantics, cycle detection with configurable recursion limit (default 64) +- Strict EAV fact model with typed attributes; type-safe TS builder API with autocompleted attr names +- JSON serialization of all rules via handler-registry pattern (round-trip equivalence tested per rule) +- Deterministic tick execution: documented conflict resolution (salience → specificity → insertion-order); iteration of Set/Map replaced with sorted arrays everywhere; no `Date.now`/`Math.random`/I/O in RHS +- Immer-backed working memory snapshots at configurable interval N (default 30 ticks); append-only event log with monotonic sequence numbers; replay produces byte-identical state (verified via state hash) +- Full FIDE chess rules expressed as Rete productions in `@paratype/chess`: piece placement, legal move generation per piece, turn order, captures, check detection, castling, en passant, promotion, checkmate, stalemate, 50-move rule, threefold repetition, insufficient material +- 15 concrete preset custom rules in `@paratype/chess` with compatibility matrix; toggleable between games; each with unit tests and at least one Playwright scenario +- Chess UI (React + Vite): 8×8 board with drag-drop moves, legal-move highlighting, rule-toggle screen, save/load UI, JSON export/import, undo via time-travel (to previous turn boundary) +- localStorage auto-save (per tick end) with schema-versioned payload; restore on page load; JSON export-import with validation +- Bun WebSocket server with: room create/join/leave (6-char room codes, 60s reconnect window), authoritative move validation, fact-delta broadcast, protocol versioning (`v` field), rate limit (100 msg/sec/client), 64KB message cap, origin allow-list, structured logging (pino) +- CI green on ubuntu-latest with Bun latest; bundle-size enforced; `bun audit` green +- ≥90% line coverage for `@paratype/rete`; ≥70% for `@paratype/chess`; ≥80% for `@paratype/chess-server` +- Conventional Commits; phase boundaries tagged (`v0.1.0-phase1`, etc.); pre-commit hook runs `bun run check` + +### Must NOT Have (Guardrails) + +**Scope exclusions (v1)**: +- NO chess AI, puzzles, tutorials, opening books, ELO, matchmaking, tournaments, leaderboards +- NO social features: chat, emotes, friends, profiles, avatars +- NO rule marketplace, remote rule sharing, user-authored JS rule upload +- NO mid-game rule toggle (toggle only between games in v1) +- NO spectators in v1 (2-player rooms only) +- NO server-side game persistence across restart (in-memory rooms only) +- NO mobile-native clients (responsive web only) +- NO accounts, OAuth, email, password, analytics, telemetry, i18n +- NO additional games on top of the engine in this plan +- NO visual rule editor / node graph editor (toggle-only UI in v1) +- NO pararules' Nim macro equivalents via runtime code-gen or `eval` +- NO external TS Rete library dependency (greenfield build) +- NO chess.js dependency (chess rules ARE Rete productions) +- NO Stockfish or other chess engines +- NO persistent user data beyond localStorage + +**Code-quality exclusions**: +- NO `as any`, `as unknown as X`, `@ts-ignore`, `@ts-expect-error` in engine package (ESLint-enforced) +- NO `Date.now()`, `Math.random()`, `performance.now()`, `setTimeout`, `setInterval`, `fetch`, `console.log` inside engine RHS code paths (ESLint override on engine package) +- NO raw `Set` or `Map` iteration in engine hot paths (must sort to array first) +- NO circular package dependencies (`@paratype/chess` may import `@paratype/rete`; reverse forbidden) +- NO internal JSDoc (public API only); NO over-validation inside module boundaries +- NO premature abstraction / "framework" layer between engine and chess +- NO generic names in engine code: `data`, `result`, `item`, `temp`, `obj`, `foo` + +--- + +## Verification Strategy (MANDATORY) + +> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions. + +### Test Decision +- **Infrastructure exists**: NO (fresh repo; infrastructure built in Phase 0 scaffold) +- **Automated tests**: YES (TDD for engine, tests-after for chess/server) +- **Framework**: Vitest (unit/integration), Playwright (E2E browser), custom Bun scripts (WebSocket integration, replay-determinism) +- **TDD workflow**: For engine tasks, each task follows RED (failing Vitest) → GREEN (minimal impl) → REFACTOR (clean up while tests remain green) + +### QA Policy +Every task MUST include agent-executable QA scenarios. Evidence saved to `.sisyphus/evidence/task-{N}-{slug}.{ext}`. + +- **Engine unit tests**: `bun test -t ""` with exact expected PASS/FAIL line; evidence = stdout log +- **Chess UI**: Playwright (playwright skill) — specific `[data-square="e2"]`, `[data-piece="white-pawn"]` selectors; evidence = screenshot + trace +- **Server integration**: scripted Bun WebSocket client against running server process; evidence = transcript JSON +- **Determinism**: `bun run scripts/hash-state.ts ` produces sha256; evidence = hash file +- **Bundle size**: `bun run size-limit`; evidence = stdout showing kb count +- **Build**: `bun run build` → inspect `packages/*/dist/`; evidence = `ls -la` output +- **CI**: `gh run list --limit 1 --json conclusion` → "success"; evidence = run URL + +### Mandatory QA Scenario Requirements +Every task MUST have: +- At least 1 happy-path scenario with exact commands, inputs, and assertions +- At least 1 failure/edge-case scenario (invalid input, missing dep, rejected move, protocol mismatch, etc.) +- Evidence path: `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}` +- Specific selectors/data, not vague descriptions +- Binary pass/fail result (no "looks correct") + +--- + +## Execution Strategy + +### Phase Structure (Metis-directed) + +5 phases, strictly sequential. Phase N+1 cannot begin until Phase N acceptance gate (see each phase's final wave) is green. + +- **Phase 0** — Specification Lock (Wave P0.1 parallel spec authoring, Wave P0.2 scaffold) +- **Phase 1** — Engine Parity with Pararules (alpha/beta, joins, conditions, derived facts, cycle detection, builder API, JSON handler-registry, basic Immer state) +- **Phase 2** — Rete II Extensions + Chess Engine (negation, NCC, existential, aggregation; full FIDE chess as Rete productions; local 2-player via hot-seat for internal validation only) +- **Phase 3** — Time-Travel + Presets + UI (event log + snapshots; replay determinism; 15 preset custom rules; React UI; localStorage; JSON import/export) +- **Phase 4** — Authoritative Multiplayer Server (WebSocket server, rooms, reconnection, protocol v1; client networking layer; end-to-end multiplayer scenarios) +- **Final Wave** — 4 parallel review agents (plan compliance, code quality, manual QA, scope fidelity) → user okay → DONE + +### Parallel Execution Waves + +``` +Phase 0 — Specification Lock + Wave P0.1 (parallel spec authoring — 4 tasks): + ├── P0.1 SPEC.md (engine semantics) [deep] + ├── P0.2 PHASES.md (phase gates) [writing] + ├── P0.3 RULES.md (15 preset custom rules) [deep] + └── P0.4 PROTOCOL.md (WS protocol v1) [deep] + + Wave P0.2 (after P0.1, sequential foundation): + ├── P0.5 Monorepo scaffold (bun workspaces, tsconfig, eslint, vitest, playwright) [unspecified-high] + └── P0.6 CI pipeline + pre-commit hook [unspecified-high] + + GATE: SPEC/PHASES/RULES/PROTOCOL reviewed; bun install + bun run check green; CI green + +Phase 1 — Engine Pararules Parity (TDD) + Wave P1.1 (parallel engine primitives — 6 tasks): + ├── P1.1 Schema + Fact type with typed attrs [deep] + ├── P1.2 Working memory (WM) storage + retrieval [deep] + ├── P1.3 Alpha network (fact indexing by (id,attr))[deep] + ├── P1.4 Session + lifecycle (init, add, fireRules)[deep] + ├── P1.5 TS builder API + handler registry [deep] + └── P1.6 JSON serialization (round-trip) [deep] + + Wave P1.2 (parallel join mechanics — 4 tasks): + ├── P1.7 Beta network (memory + token propagation) [deep] + ├── P1.8 Join nodes with variable binding [deep] + ├── P1.9 Condition filters (`cond` analog) [deep] + └── P1.10 Query API (query / queryAll) [deep] + + Wave P1.3 (parallel advanced parity — 3 tasks): + ├── P1.11 Derived facts (thenFinally equivalent) [deep] + ├── P1.12 Cycle detection (recursion limit) [deep] + └── P1.13 Deterministic conflict resolution [deep] + + Wave P1.4 (parity validation): + └── P1.14 Pararules golden-file test port [unspecified-high] + + GATE: Engine v0.1.0-phase1 tag; 90% coverage; all golden tests green; `bun run check` green + +Phase 2 — Rete II Extensions + Chess Engine + Wave P2.1 (parallel Rete II nodes — 4 tasks): + ├── P2.1 Negation nodes (NOT) [deep] + ├── P2.2 Existential nodes (EXISTS) [deep] + ├── P2.3 NCC nodes (not-count-condition) [deep] + └── P2.4 Aggregation nodes (count/sum/collect/min/max) [deep] + + Wave P2.2 (chess foundation — parallel 4 tasks): + ├── P2.5 Chess attribute schema & piece fact shape [deep] + ├── P2.6 Starting-position fact generator [quick] + ├── P2.7 Square coordinate & color helpers [quick] + └── P2.8 Piece movement primitive rules (directions/steps) [deep] + + Wave P2.3 (chess legal-move rules — parallel 6 tasks): + ├── P2.9 Pawn move/capture rules [deep] + ├── P2.10 Knight move rules [deep] + ├── P2.11 Bishop/Rook/Queen sliding rules [deep] + ├── P2.12 King move rules [deep] + ├── P2.13 Turn order + move legality integration [deep] + └── P2.14 Capture resolution rules [deep] + + Wave P2.4 (chess special rules — parallel 4 tasks): + ├── P2.15 Castling (kingside + queenside with history flags) [deep] + ├── P2.16 En passant (single-tick capture window) [deep] + ├── P2.17 Promotion (to Q/R/B/N) [deep] + └── P2.18 Check detection rule [deep] + + Wave P2.5 (chess endgames — parallel 4 tasks): + ├── P2.19 Checkmate detection [deep] + ├── P2.20 Stalemate detection [deep] + ├── P2.21 50-move rule + threefold repetition (aggregation-based) [deep] + └── P2.22 Insufficient material draw [deep] + + Wave P2.6 (integration): + └── P2.23 End-to-end FIDE game replay test [unspecified-high] + + GATE: Engine v0.2.0-phase2 tag; full FIDE game playable via rules only; `bun run check` green + +Phase 3 — Time-Travel + Presets + UI + Wave P3.1 (time-travel — parallel 3 tasks): + ├── P3.1 Event log (append-only, monotonic seq) [deep] + ├── P3.2 Immer snapshot every N ticks [deep] + └── P3.3 Replay engine + determinism hash verifier [deep] + + Wave P3.2 (15 preset rules — parallel 5 tasks x 3 rules each): + ├── P3.4 Presets 1-3 (pawn-focused variants) [deep] + ├── P3.5 Presets 4-6 (knight/bishop variants) [deep] + ├── P3.6 Presets 7-9 (rook/queen/king variants) [deep] + ├── P3.7 Presets 10-12 (board/geometry variants) [deep] + └── P3.8 Presets 13-15 (meta rules: HP/heal/immune) [deep] + + Wave P3.3 (UI — parallel 5 tasks): + ├── P3.9 React + Vite scaffold for chess app [visual-engineering] + ├── P3.10 Chessboard component (drag-drop, highlights) [visual-engineering] + ├── P3.11 Rule-toggle screen (list with compat warnings) [visual-engineering] + ├── P3.12 Save/Load panel + undo via time-travel [visual-engineering] + └── P3.13 JSON export/import + validation [visual-engineering] + + Wave P3.4 (persistence + integration): + ├── P3.14 localStorage auto-save + restore [unspecified-high] + └── P3.15 End-to-end UI scenario (play game, toggle rule, save, restore) [unspecified-high] + + GATE: Engine v0.3.0-phase3 tag; chess UI fully playable locally with presets; `bun run check` green + +Phase 4 — Authoritative Multiplayer + Wave P4.1 (server core — parallel 4 tasks): + ├── P4.1 Bun HTTP+WS server scaffold + config [unspecified-high] + ├── P4.2 Message schemas + validation [deep] + ├── P4.3 Room model (create/join/leave, 6-char codes) [deep] + └── P4.4 Rate limiting + origin allow-list + 64KB cap [unspecified-high] + + Wave P4.2 (server game logic — parallel 4 tasks): + ├── P4.5 Authoritative session per room [deep] + ├── P4.6 Move-intent validation + fact-delta broadcast [deep] + ├── P4.7 Reconnection flow (60s window, snapshot resume) [deep] + └── P4.8 Structured logging (pino) + metrics [unspecified-high] + + Wave P4.3 (client networking — parallel 3 tasks): + ├── P4.9 WebSocket client with reconnect + seq ack [deep] + ├── P4.10 Client prediction + server reconciliation [deep] + └── P4.11 Room lobby UI (create/join screens) [visual-engineering] + + Wave P4.4 (integration): + └── P4.12 E2E multiplayer scenario (two Playwright contexts play a full game) [unspecified-high] + + GATE: Engine v0.4.0-phase4 tag; two-browser multiplayer working end-to-end; `bun run check` green + +Final Verification Wave (4 parallel reviews) + ├── F1 Plan compliance audit (oracle) + ├── F2 Code quality review (unspecified-high) + ├── F3 Real manual QA via Playwright + scripted WS client (unspecified-high) + └── F4 Scope fidelity check (deep) + → Present results → Wait for explicit user okay → Tag v1.0.0 +``` + +### Dependency Matrix (abbreviated — full matrix embedded in each task's "Blocked By") + +- **Phase 0 tasks**: No external deps; P0.5 blocks ALL Phase 1+ tasks; P0.6 depends on P0.5 +- **P1.1-P1.6**: parallel within Wave P1.1, block P1.7-P1.10 +- **P1.7-P1.10**: parallel within Wave P1.2, block P1.11-P1.13 +- **P1.11-P1.13**: parallel within Wave P1.3, block P1.14 +- **P1.14**: Phase 1 gate; blocks all Phase 2 +- **P2.1-P2.4**: Rete II nodes, parallel, block P2.21 (aggregation-dependent) +- **P2.5-P2.8**: chess foundation, parallel, block P2.9-P2.14 +- **P2.9-P2.14**: legal-move rules, parallel, block P2.15-P2.18 +- **P2.15-P2.18**: special rules, parallel, block P2.19-P2.22 +- **P2.19-P2.22**: endgames, parallel, block P2.23 +- **P2.23**: Phase 2 gate; blocks all Phase 3 +- **P3.1-P3.3**: time-travel, parallel, block P3.14 (restore requires replay) +- **P3.4-P3.8**: presets, parallel, block P3.11 (UI needs presets listed) +- **P3.9-P3.13**: UI tasks, mostly parallel (P3.10 depends on P3.9; others parallel with P3.10) +- **P3.14**: localStorage, depends on P3.3 + P3.12 +- **P3.15**: Phase 3 gate; blocks all Phase 4 +- **P4.1-P4.4**: server core, parallel, block P4.5-P4.8 +- **P4.5-P4.8**: server game logic, parallel, block P4.9-P4.11 +- **P4.9-P4.11**: client networking, parallel, block P4.12 +- **P4.12**: Phase 4 gate; blocks Final Wave +- **F1-F4**: parallel; all must APPROVE before user-okay + +### Agent Dispatch Summary + +- **Phase 0 (6)**: P0.1-P0.4 → `deep`+`writing`; P0.5-P0.6 → `unspecified-high` +- **Phase 1 (14)**: All `deep` (TDD engine work); P1.14 → `unspecified-high` +- **Phase 2 (23)**: All `deep`; P2.23 → `unspecified-high` +- **Phase 3 (15)**: P3.1-P3.8 → `deep`; P3.9-P3.13 → `visual-engineering`; P3.14-P3.15 → `unspecified-high` +- **Phase 4 (12)**: P4.1 → `unspecified-high`; P4.2-P4.3 → `deep`; P4.4 → `unspecified-high`; P4.5-P4.7 → `deep`; P4.8 → `unspecified-high`; P4.9-P4.10 → `deep`; P4.11 → `visual-engineering`; P4.12 → `unspecified-high` +- **Final (4)**: F1 → `oracle`; F2 → `unspecified-high`; F3 → `unspecified-high`; F4 → `deep` + +--- + +## TODOs + +> Implementation + Test = ONE Task. Never separate. +> EVERY task has: Recommended Agent Profile + Parallelization info + QA Scenarios. +> **A task WITHOUT QA Scenarios is INCOMPLETE. No exceptions.** + +### Phase 0 — Specification Lock + +- [x] P0.1. **Author `packages/rete/SPEC.md` — engine semantics specification** + + **What to do**: + - Create directory `packages/rete/` + - Write `packages/rete/SPEC.md` with sections (exactly these, `## ` headings): + 1. `## Fact Model` — strict EAV (id, attr, value); id minted by Session (auto-increment), opaque to users; attr is branded string literal type; value is typed per attr via schema + 2. `## ID Authority` — Session owns counter; in multiplayer, only server increments; clients receive facts with server-assigned ids + 3. `## Conflict Resolution` — deterministic order: salience desc → specificity (# of conditions) desc → rule insertion order asc + 4. `## Match Refraction` — each unique match fires once; re-fires only on fact change affecting bindings + 5. `## Iteration Order` — all Session iteration uses sorted arrays (sort keys documented per structure); no raw `Set` iteration in hot paths + 6. `## Truth Maintenance` — derived facts (thenFinally) retract when any supporting fact retracts; logical dependency tracked per derived fact + 7. `## Cycle Detection` — configurable recursion limit (default 64); exceeded → `RecursionLimitExceededError` with cycle trace + 8. `## RHS Purity Contract` — RHS may NOT call Date.now, Math.random, performance.now, setTimeout, setInterval, fetch, or any I/O; enforced via ESLint rule `no-impure-rhs` (custom rule) + dev-mode runtime global wrapping + 9. `## JSON Rule Schema` — handler-registry pattern: `{name, salience, conditions: [...], handler: "registeredName", args: JsonValue[]}`; NO function-to-string, NO eval, NO arbitrary JS + 10. `## Rete II Reference Target` — Doorenbos 1995 thesis; enumerate node types in scope: AlphaNode, BetaMemory, JoinNode, NegationNode, NccNode, ExistentialNode, AggregationNode, DerivedFactProduction + + **Must NOT do**: + - Do NOT include implementation code in SPEC.md + - Do NOT reference specific library versions + - Do NOT leave any section as TBD + + **Recommended Agent Profile**: + - **Category**: `deep` — Requires careful semantic reasoning about Rete and distributed determinism + - **Skills**: [`context7`, `web-search`] + - `context7`: Look up canonical Rete references (Forgy 1982, Doorenbos 1995) + - `web-search`: Find CLIPS/Drools/Jess documentation for conflict resolution conventions + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P0.1 (with P0.2, P0.3, P0.4) + - **Blocks**: P0.5, ALL engine implementation tasks + - **Blocked By**: None — start immediately + + **References**: + + **Pattern References**: + - Pararules README semantics: https://github.com/paranim/pararules#overview + + **API/Type References**: + - Will be source of truth — no prior file + + **External References**: + - Doorenbos 1995: "Production Matching for Large Learning Systems" — canonical Rete II thesis + - CLIPS reference manual — conflict resolution strategies + - Pararules source: https://github.com/paranim/pararules/blob/master/src/pararules/engine.nim + + **WHY Each Reference Matters**: + - Doorenbos is the only authoritative source for "Rete II"; without it, scope ambiguity persists + - CLIPS refraction/salience is the de facto industry standard + - Pararules defines our baseline behavior to match + + **Acceptance Criteria**: + - [ ] File `packages/rete/SPEC.md` exists + - [ ] `[ "$(grep -c '^## ' packages/rete/SPEC.md)" -ge "10" ]` → true (exactly 10 `## ` sections) + - [ ] `grep -q 'Doorenbos' packages/rete/SPEC.md` → 0 exit + - [ ] `grep -q 'handler-registry' packages/rete/SPEC.md` → 0 exit + - [ ] `grep -q 'no-impure-rhs' packages/rete/SPEC.md` → 0 exit + + **QA Scenarios**: + + ``` + Scenario: SPEC.md exists with required structure + Tool: Bash + Preconditions: clean repo + Steps: + 1. Run: test -f packages/rete/SPEC.md + 2. Run: grep -c '^## ' packages/rete/SPEC.md + 3. Run: for term in "Fact Model" "ID Authority" "Conflict Resolution" "Match Refraction" "Iteration Order" "Truth Maintenance" "Cycle Detection" "RHS Purity Contract" "JSON Rule Schema" "Rete II Reference Target"; do grep -q "^## $term" packages/rete/SPEC.md || echo "MISSING: $term"; done + Expected Result: Step 1 exit 0; Step 2 outputs exactly 10; Step 3 outputs nothing (no MISSING lines) + Failure Indicators: missing file, section count != 10, any MISSING line + Evidence: .sisyphus/evidence/task-P0.1-spec-exists.log + + Scenario: SPEC.md forbids eval in JSON schema section + Tool: Bash + Preconditions: SPEC.md written + Steps: + 1. Run: awk '/^## JSON Rule Schema/,/^## /' packages/rete/SPEC.md | grep -qiE 'eval|function-to-string|arbitrary JS' && echo "OK" || echo "MISSING_FORBID_EVAL" + Expected Result: stdout "OK" + Evidence: .sisyphus/evidence/task-P0.1-json-forbid.log + ``` + + **Commit**: YES + - Message: `docs(rete): author engine specification (SPEC.md)` + - Files: `packages/rete/SPEC.md` + - Pre-commit: none (doc-only commit; hook runs `bun run check` which no-ops on empty repo) + +- [x] P0.2. **Author `docs/PHASES.md` — phase gates + non-goals + perf budgets** + + **What to do**: + - Create `docs/PHASES.md` with sections (exact headings): + - `## Phase 0 — Specification Lock` + - `## Phase 1 — Pararules Parity` + - `## Phase 2 — Rete II + Chess Engine` + - `## Phase 3 — Time-Travel + Presets + UI` + - `## Phase 4 — Authoritative Multiplayer` + - `## Non-Goals (v1)` + - `## Performance Budgets` + - `## Demo Scenarios` + - Each phase section: bullet-listed in-scope deliverables + executable acceptance-gate commands + explicit Must-NOT-Have exclusions + - Non-Goals: copy the plan's "Must NOT Have" list + - Performance Budgets: `insert(fact)` < 0.5ms @ 10k facts; `fireRules()` < 5ms for chess ruleset; replay 1000 events < 500ms; engine bundle < 50KB min+gz; chess bundle < 200KB min+gz; server tick broadcast < 50ms p99 + - Demo Scenarios: one per phase, each a scripted flow (e.g., "Phase 1 demo: run `bun test packages/rete` — all pararules golden tests pass") + + **Must NOT do**: + - Do NOT duplicate SPEC.md content; link to it + - Do NOT set unrealistic budgets (these are contractual) + + **Recommended Agent Profile**: + - **Category**: `writing` — Documentation authoring, prose-heavy + - **Skills**: [`web-search`] + - `web-search`: Reference typical WebSocket server perf budgets and bundle-size norms + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P0.1 (with P0.1, P0.3, P0.4) + - **Blocks**: All subsequent task enumeration validation + - **Blocked By**: None + + **References**: + + **Pattern References**: None (new doc) + + **External References**: + - bundlephobia.com for bundle size norms + - Pino docs for server-logging perf norms + + **Acceptance Criteria**: + - [ ] File `docs/PHASES.md` exists + - [ ] `[ "$(grep -c '^## Phase' docs/PHASES.md)" -eq "5" ]` → true (Phase 0..4) + - [ ] `grep -q 'Non-Goals' docs/PHASES.md` → 0 exit + - [ ] `grep -q 'Performance Budgets' docs/PHASES.md` → 0 exit + - [ ] `grep -q '< 50KB' docs/PHASES.md` → 0 exit + + **QA Scenarios**: + + ``` + Scenario: PHASES.md has all required sections + Tool: Bash + Preconditions: none + Steps: + 1. Run: test -f docs/PHASES.md + 2. Run: for h in "Phase 0 — Specification Lock" "Phase 1 — Pararules Parity" "Phase 2 — Rete II + Chess Engine" "Phase 3 — Time-Travel + Presets + UI" "Phase 4 — Authoritative Multiplayer" "Non-Goals (v1)" "Performance Budgets" "Demo Scenarios"; do grep -qF "## $h" docs/PHASES.md || echo "MISSING: $h"; done + Expected Result: step 1 exit 0; step 2 outputs nothing + Evidence: .sisyphus/evidence/task-P0.2-sections.log + + Scenario: Perf budgets are numeric and concrete (failure path) + Tool: Bash + Preconditions: PHASES.md written + Steps: + 1. Run: awk '/^## Performance Budgets/,/^## /' docs/PHASES.md | grep -E '(TBD|TODO|FIXME)' && echo "FAIL: placeholder found" || echo "OK" + Expected Result: stdout "OK" + Evidence: .sisyphus/evidence/task-P0.2-budgets.log + ``` + + **Commit**: YES + - Message: `docs(root): author PHASES.md with phase gates and perf budgets` + - Files: `docs/PHASES.md` + - Pre-commit: none + +- [x] P0.3. **Author `packages/chess/RULES.md` — 15 concrete preset custom rules** + + **What to do**: + - Create directory `packages/chess/` + - Write `packages/chess/RULES.md` listing exactly 15 preset custom rules + - Each rule has `### {rule-name}` heading, plus bullet subsections: `**ID**`, `**Description**`, `**Base Rule Affected**` (which FIDE production it modifies, or "additive"), `**Mode**` (additive | override), `**Incompatible With**` (list of other rule IDs), `**Test Scenarios**` (≥3 concrete scenarios describing input board state + expected behavior), `**Edge Cases**` (interaction with en passant, castling, promotion as relevant) + - Propose 15 concrete rules; include at least 3 from each category: movement-modifier (e.g., "Pawns may move backward"), piece-ability (e.g., "King heals +1HP when not in check"), win-condition (e.g., "Capture any piece to win"), board-geometry (e.g., "Board wraps horizontally"), meta-state (e.g., "Pieces have 3 HP; captures deal 1 damage") + + **Must NOT do**: + - Do NOT leave any rule as "TBD" or "example rule" + - Do NOT allow two rules to be mutually required (circular dependency) + - Do NOT define rules requiring user-authored JS (v1 preset-only constraint) + + **Recommended Agent Profile**: + - **Category**: `deep` — Game design + rule-interaction reasoning + - **Skills**: [`web-search`] + - `web-search`: Survey chess variants (Fairy chess, Pocket chess, Really Bad Chess) for rule inspiration + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P0.1 (with P0.1, P0.2, P0.4) + - **Blocks**: P3.4-P3.8 (preset implementations) + - **Blocked By**: None + + **References**: + - Fairy chess variants: https://en.wikipedia.org/wiki/Fairy_chess_piece + - Chess.dougdoug.com (concept inspiration) + + **Acceptance Criteria**: + - [ ] File `packages/chess/RULES.md` exists + - [ ] `[ "$(grep -c '^### ' packages/chess/RULES.md)" -eq "15" ]` → true (exactly 15 rule headings) + - [ ] `grep -c '\*\*ID\*\*:' packages/chess/RULES.md` == 15 + - [ ] `grep -c '\*\*Incompatible With\*\*:' packages/chess/RULES.md` == 15 + - [ ] Rule IDs unique: `grep -oE '\*\*ID\*\*: [a-z-]+' packages/chess/RULES.md | sort -u | wc -l` == 15 + + **QA Scenarios**: + + ``` + Scenario: Exactly 15 unique preset rules defined + Tool: Bash + Steps: + 1. Run: test -f packages/chess/RULES.md + 2. Run: grep -c '^### ' packages/chess/RULES.md + 3. Run: grep -oE '\*\*ID\*\*: [a-z0-9-]+' packages/chess/RULES.md | sort -u | wc -l + Expected Result: step 1 exit 0; step 2 outputs 15; step 3 outputs 15 + Evidence: .sisyphus/evidence/task-P0.3-rules-count.log + + Scenario: No TBD placeholders (failure path) + Tool: Bash + Steps: + 1. Run: grep -E '(TBD|TODO|FIXME|example rule|placeholder)' packages/chess/RULES.md && echo "FAIL" || echo "OK" + Expected Result: stdout "OK" + Evidence: .sisyphus/evidence/task-P0.3-no-placeholders.log + ``` + + **Commit**: YES + - Message: `docs(chess): author RULES.md with 15 concrete preset custom rules` + - Files: `packages/chess/RULES.md` + - Pre-commit: none + +- [x] P0.4. **Author `packages/server/PROTOCOL.md` — WebSocket protocol v1** + + **What to do**: + - Create directory `packages/server/` + - Write `packages/server/PROTOCOL.md` defining WebSocket protocol v1 + - Include `## Overview` explaining: all messages have top-level `v: 1`; all include `seq: number` (monotonic); all include `ts: number` (unix ms); mismatched `v` → hard disconnect; max message 64KB; rate limit 100 msg/sec/client; origin allow-list + - Enumerate at least 8 message types, each as `### Message: {name}` with subsections: `**Direction**` (C→S | S→C | bidir), `**Purpose**`, `**JSON Schema**` (fenced zod-like pseudo-schema or JSON example), `**Example**` (fenced json), `**Error Cases**` (listed) + - Required message types: `room.create`, `room.join`, `room.leave`, `game.move` (C→S intent), `game.state` (S→C full snapshot on join/reconnect), `game.delta` (S→C fact changes per tick), `game.end`, `error` + - Include `## Reconnection Flow` — client disconnects, 60s window, reconnect with last seen `seq`, server replays deltas since that seq + - Include `## Auth` — room code 6 chars [A-Z0-9]; optional room token (UUID v4) returned on create; every subsequent message includes token + - Include `## Rate Limiting` — token bucket per connection, 100 msg/sec, burst 20; over-limit → disconnect with `error` code `RATE_LIMIT` + + **Must NOT do**: + - Do NOT define message types requiring session persistence across server restart (v1 in-memory only) + - Do NOT define spectator-related messages (v1 2-player only) + - Do NOT define rule-mutation-during-game messages (v1 between-games only) + + **Recommended Agent Profile**: + - **Category**: `deep` — Protocol design requires precision and failure-mode reasoning + - **Skills**: [`web-search`, `code-search`] + - `web-search`: Look at lichess/chess.com WebSocket patterns + - `code-search`: Find battle-tested WebSocket protocols (e.g., y-websocket, automerge) + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P0.1 (with P0.1, P0.2, P0.3) + - **Blocks**: Phase 4 (all server tasks) + - **Blocked By**: None + + **References**: + - y-websocket protocol docs (well-designed minimal WS protocol) + - RFC 6455 (WebSocket) for base protocol + + **Acceptance Criteria**: + - [ ] File `packages/server/PROTOCOL.md` exists + - [ ] `[ "$(grep -c '^### Message: ' packages/server/PROTOCOL.md)" -ge "8" ]` → true + - [ ] `grep -q 'Reconnection Flow' packages/server/PROTOCOL.md` → 0 exit + - [ ] `grep -q 'Rate Limiting' packages/server/PROTOCOL.md` → 0 exit + - [ ] `grep -q 'v: 1' packages/server/PROTOCOL.md` → 0 exit + + **QA Scenarios**: + + ``` + Scenario: Protocol defines all 8+ required message types + Tool: Bash + Steps: + 1. Run: test -f packages/server/PROTOCOL.md + 2. Run: grep -c '^### Message: ' packages/server/PROTOCOL.md + 3. Run: for m in "room.create" "room.join" "room.leave" "game.move" "game.state" "game.delta" "game.end" "error"; do grep -qF "### Message: $m" packages/server/PROTOCOL.md || echo "MISSING: $m"; done + Expected Result: step 1 exit 0; step 2 ≥ 8; step 3 outputs nothing + Evidence: .sisyphus/evidence/task-P0.4-messages.log + + Scenario: Rate-limit and auth sections present (failure path) + Tool: Bash + Steps: + 1. Run: grep -qc 'Rate Limiting' packages/server/PROTOCOL.md && grep -qc 'Auth' packages/server/PROTOCOL.md && echo "OK" || echo "FAIL" + Expected Result: stdout "OK" + Evidence: .sisyphus/evidence/task-P0.4-sections.log + ``` + + **Commit**: YES + - Message: `docs(server): author PROTOCOL.md defining WebSocket protocol v1` + - Files: `packages/server/PROTOCOL.md` + - Pre-commit: none + +- [x] P0.5. **Scaffold monorepo skeleton (Bun workspaces + tsconfig + eslint + vitest + playwright)** + + **What to do**: + - Root: `package.json` with `"workspaces": ["packages/*"]`, `"private": true`, `"packageManager": "bun@latest"` + - Root scripts: `check` (runs typecheck + lint + test), `typecheck` (bun x tsc -b), `lint` (bun x eslint), `test` (bun x vitest run), `test:coverage` (vitest run --coverage), `build` (bun run --filter '*' build), `size-limit` (placeholder) + - Root `tsconfig.base.json`: target ES2022, module ESNext, moduleResolution Bundler, strict: true, noImplicitAny, exactOptionalPropertyTypes, noUncheckedIndexedAccess, verbatimModuleSyntax + - Root `tsconfig.json`: references to all packages + - Root `eslint.config.js` (flat): typescript-eslint strict preset; `no-restricted-globals` ban `Date`, `Math.random`, `performance`, `setTimeout`, `setInterval`, `fetch` within `packages/rete/src/**/rhs/**` and engine RHS paths (override-based); `@typescript-eslint/no-explicit-any` error + - Root `vitest.workspace.ts` listing all packages + - Root `playwright.config.ts` with chess app base URL (http://localhost:5173) + - Packages: `packages/rete/package.json` (`"name": "@paratype/rete"`, type module, main dist/index.js, types dist/index.d.ts), `tsconfig.json` extending base, empty `src/index.ts` with `export {}`, `README.md` (one-line description) + - Same skeleton for `packages/chess` (`"name": "@paratype/chess"`) and `packages/server` (`"name": "@paratype/chess-server"`) + - Add `.gitignore`: `node_modules/`, `dist/`, `.sisyphus/evidence/`, `*.log`, `.DS_Store`, `coverage/`, `playwright-report/`, `test-results/` + - Add `LICENSE` (MIT) with paratype org name + - Add root `README.md`: project overview, link to SPEC/PHASES/RULES/PROTOCOL + + **Must NOT do**: + - Do NOT install production dependencies beyond what's needed for scaffolding (TypeScript, Vitest, ESLint, Playwright, tsup) + - Do NOT add Immer/React/Vite yet (Phase 3 concern) + - Do NOT add WebSocket / pino yet (Phase 4 concern) + - Do NOT write any engine/chess/server source code beyond `export {}` + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` — Tooling setup with many moving parts + - **Skills**: [`context7`] + - `context7`: Look up Bun workspace, Vitest workspace, Playwright, ESLint flat config docs + + **Parallelization**: + - **Can Run In Parallel**: NO (sole foundation task) + - **Parallel Group**: Wave P0.2 (sequential) + - **Blocks**: P0.6 and ALL implementation tasks + - **Blocked By**: P0.1, P0.2 (need SPEC to know package boundaries) + + **References**: + + **Pattern References**: None (greenfield) + + **External References**: + - Bun workspaces: https://bun.sh/docs/install/workspaces + - Vitest workspace: https://vitest.dev/guide/workspace + - Playwright config: https://playwright.dev/docs/test-configuration + - typescript-eslint flat config: https://typescript-eslint.io/packages/typescript-eslint/#flat-config + + **Acceptance Criteria**: + - [ ] `bun install` exits 0 + - [ ] `bun run check` exits 0 (zero tests OK; zero lint errors) + - [ ] `bun run build` exits 0 (emits dist/ for each package OR exits 0 with skip — depends on tsup wiring; at minimum `tsc -b` passes) + - [ ] Files exist: `package.json`, `tsconfig.base.json`, `tsconfig.json`, `eslint.config.js`, `vitest.workspace.ts`, `playwright.config.ts`, `.gitignore`, `LICENSE`, `README.md` + - [ ] Directory tree: `packages/rete/{package.json,tsconfig.json,src/index.ts,README.md}`, same for `chess` and `server` + + **QA Scenarios**: + + ``` + Scenario: Fresh clone installs and checks clean + Tool: Bash + Preconditions: repo on fresh checkout; Bun installed + Steps: + 1. Run: bun install 2>&1 | tee /tmp/p05-install.log + 2. Run: bun run check 2>&1 | tee /tmp/p05-check.log + 3. Run: bun run build 2>&1 | tee /tmp/p05-build.log + Expected Result: step 1 exits 0; step 2 exits 0; step 3 exits 0; no errors in logs + Failure Indicators: any non-zero exit, "error" token in logs + Evidence: .sisyphus/evidence/task-P0.5-install-check-build.log + + Scenario: ESLint rejects Math.random in engine RHS path (failure path validating config correctness) + Tool: Bash + Preconditions: scaffold complete + Steps: + 1. Create temp file: mkdir -p packages/rete/src/rhs && printf 'export const x = () => Math.random();\n' > packages/rete/src/rhs/_temp.ts + 2. Run: bun run lint 2>&1 | tee /tmp/p05-lint-fail.log + 3. Capture exit: echo "exit=$?" + 4. Cleanup: rm packages/rete/src/rhs/_temp.ts + Expected Result: step 2 outputs ESLint error referencing Math.random and exits non-zero + Evidence: .sisyphus/evidence/task-P0.5-lint-rejects-random.log + + Scenario: Workspace package names are correct + Tool: Bash + Steps: + 1. Run: jq -r .name packages/rete/package.json + 2. Run: jq -r .name packages/chess/package.json + 3. Run: jq -r .name packages/server/package.json + Expected Result: outputs "@paratype/rete", "@paratype/chess", "@paratype/chess-server" respectively + Evidence: .sisyphus/evidence/task-P0.5-pkg-names.log + ``` + + **Commit**: YES + - Message: `chore(root): scaffold monorepo with Bun workspaces, TypeScript, Vitest, ESLint, Playwright` + - Files: `package.json`, `tsconfig.base.json`, `tsconfig.json`, `eslint.config.js`, `vitest.workspace.ts`, `playwright.config.ts`, `.gitignore`, `LICENSE`, `README.md`, `packages/*/package.json`, `packages/*/tsconfig.json`, `packages/*/src/index.ts`, `packages/*/README.md`, `bun.lockb` + - Pre-commit: `bun run check` (hook installed next task) + +- [ ] P0.6. **CI pipeline (`.github/workflows/ci.yml`) + pre-commit hook (lefthook)** + + **What to do**: + - Create `.github/workflows/ci.yml`: + - Trigger: pull_request, push to main + - Jobs: `check` (typecheck, lint, test with coverage upload), `build` (build all packages, upload dist artifacts), `e2e` (Playwright headless), `size` (bundle size check), `audit` (`bun audit`) + - All on ubuntu-latest with `oven-sh/setup-bun@v1` pinning to stable + - Cache: `~/.bun/install/cache` + - Upload Playwright traces on failure + - Create `lefthook.yml` at root with pre-commit hook running `bun run check` (fast — typecheck + lint + unit tests only, not Playwright) + - Install lefthook as dev dep; add `postinstall` script running `bunx lefthook install` + - Add `.github/workflows/README.md` explaining CI status badges + - Add size-limit config to root `package.json` (size-limit dev dep; initial budget: engine 50KB, chess 200KB — both placeholders until dist exists; the CI job passes when empty) + + **Must NOT do**: + - Do NOT add Node.js matrix (Bun only, per decision) + - Do NOT add deployment workflows (out of scope) + - Do NOT skip `bun audit` (security requirement) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [`context7`, `code-search`] + - `context7`: Look up current `oven-sh/setup-bun` action options + - `code-search`: Find production CI workflows for Bun monorepos on grep.app + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave P0.2 (after P0.5) + - **Blocks**: All subsequent commits (CI becomes a required status check) + - **Blocked By**: P0.5 + + **References**: + - setup-bun action: https://github.com/oven-sh/setup-bun + - lefthook: https://github.com/evilmartians/lefthook + - size-limit: https://github.com/ai/size-limit + + **Acceptance Criteria**: + - [ ] `.github/workflows/ci.yml` exists and passes `actionlint` (`bun x @action-validator/cli action-validator .github/workflows/ci.yml` OR `gh workflow view` after push) + - [ ] `lefthook.yml` exists at root + - [ ] `bun run check` is wired as pre-commit (running `bunx lefthook run pre-commit` executes check) + - [ ] First push triggers CI; all jobs green + - [ ] `gh run list --limit 1 --json conclusion -q '.[0].conclusion'` returns `"success"` + + **QA Scenarios**: + + ``` + Scenario: CI green on first push + Tool: Bash + Preconditions: remote configured; push enabled + Steps: + 1. Run: git add -A && git commit -m "ci: verify pipeline" --allow-empty + 2. Run: git push + 3. Wait: sleep 120 (or poll with gh run watch) + 4. Run: gh run list --limit 1 --json conclusion,databaseId,url -q '.[0]' + Expected Result: stdout contains `"conclusion":"success"` and a URL + Evidence: .sisyphus/evidence/task-P0.6-ci-success.json + + Scenario: Pre-commit hook blocks bad commit (failure path) + Tool: Bash + Preconditions: hook installed + Steps: + 1. Run: echo 'const x: any = 1;' > packages/rete/src/_bad.ts + 2. Run: git add packages/rete/src/_bad.ts + 3. Run: git commit -m "bad" 2>&1 | tee /tmp/p06-hook.log; echo "exit=$?" + 4. Cleanup: git reset HEAD && rm packages/rete/src/_bad.ts + Expected Result: commit fails; log shows ESLint "no-explicit-any" error + Evidence: .sisyphus/evidence/task-P0.6-hook-blocks.log + + Scenario: actionlint accepts workflow + Tool: Bash + Steps: + 1. Run: bun x @action-validator/cli action-validator .github/workflows/ci.yml + Expected Result: exit 0 + Evidence: .sisyphus/evidence/task-P0.6-actionlint.log + ``` + + **Commit**: YES + - Message: `ci(root): add GitHub Actions pipeline and lefthook pre-commit hook` + - Files: `.github/workflows/ci.yml`, `.github/workflows/README.md`, `lefthook.yml`, `package.json` (size-limit config + lefthook dep), `bun.lockb` + - Pre-commit: `bun run check` + +### Phase 1 — Engine Pararules Parity (TDD) + +- [ ] P1.1. **Schema + Fact type with typed attributes (TDD)** + + **What to do**: + - RED: In `packages/rete/src/schema.test.ts`, write failing tests: + - `defineSchema({ Health: 'number', Position: 'Vec2' })` returns object with keyed attrs typed correctly + - Attempting to create a `Fact` with wrong value type for an attr produces a TypeScript type error (type-level test via `@ts-expect-error` comments in a `.type-test.ts` file) + - Runtime fact creation: `fact(id, attr, value)` returns `{ id, attr, value }` with branded types + - GREEN: Implement in `packages/rete/src/schema.ts`: + - `export function defineSchema>(defs: S)` returning typed schema object + - `export type Fact` as tagged union discriminated by `attr` key + - `export function fact(id: EntityId, attr: K, value: S[K]): Fact` + - `EntityId` as branded `number` via `type EntityId = number & { readonly __brand: 'EntityId' }` + - REFACTOR: Extract type utilities to `schema.types.ts` if file exceeds 150 LOC; add JSDoc on public exports only + - Export from `packages/rete/src/index.ts` + + **Must NOT do**: + - Do NOT use `any` or `unknown as X` casts + - Do NOT expose Immer (Phase 3 concern) + - Do NOT allow runtime attr name collisions silently — error-throw on duplicate + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [`context7`] + - `context7`: Look up TypeScript branded types and discriminated unions best practices + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 (with P1.2, P1.3, P1.4, P1.5, P1.6) + - **Blocks**: P1.7-P1.13 (beta network, derived facts all depend on Fact type) + - **Blocked By**: P0.5, P0.6 (scaffold + CI); P0.1 (SPEC.md defines fact shape) + + **References**: + + **Pattern References**: + - `packages/rete/SPEC.md` §Fact Model — canonical fact shape + + **External References**: + - Branded types: https://egghead.io/blog/using-branded-types-in-typescript + - Discriminated unions: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/schema.test.ts` → all green + - [ ] `bun x tsc --noEmit -p packages/rete/tsconfig.json` → 0 errors + - [ ] Type-level tests in `schema.type-test.ts` compile (failures are intentional via `@ts-expect-error`) + - [ ] Coverage of `schema.ts` ≥ 95% line + + **QA Scenarios**: + + ``` + Scenario: Schema + fact round-trip with correct types + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/schema.test.ts 2>&1 | tee /tmp/p11-test.log + 2. Run: grep -E '(PASS|FAIL|Tests )' /tmp/p11-test.log + Expected Result: output contains "PASS" and final line "Tests {N} passed" with 0 failures + Evidence: .sisyphus/evidence/task-P1.1-schema-tests.log + + Scenario: Type-level rejection of invalid value (failure path) + Tool: Bash + Steps: + 1. Run: bun x tsc --noEmit -p packages/rete/tsconfig.json 2>&1 | tee /tmp/p11-tsc.log + 2. Run: grep -c 'error TS' /tmp/p11-tsc.log + Expected Result: step 1 exits 0; step 2 outputs 0 (all @ts-expect-error annotations consumed cleanly) + Evidence: .sisyphus/evidence/task-P1.1-tsc.log + ``` + + **Commit**: YES + - Message: `feat(rete): add schema and typed Fact primitives (P1.1)` + - Files: `packages/rete/src/schema.ts`, `packages/rete/src/schema.types.ts`, `packages/rete/src/schema.test.ts`, `packages/rete/src/schema.type-test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.2. **Working-memory (WM) storage + retrieval (TDD)** + + **What to do**: + - RED: `packages/rete/src/wm.test.ts` — failing tests: + - `WM.insert(id, attr, value)` stores fact; duplicate `(id, attr)` replaces value (update semantics per SPEC) + - `WM.retract(id, attr)` removes fact; returns true if existed, false if not + - `WM.contains(id, attr)` returns boolean + - `WM.get(id, attr)` returns value or undefined + - `WM.allFacts()` returns sorted stable array (sort key: `[id, attr]`) — iteration determinism per SPEC §Iteration Order + - GREEN: `packages/rete/src/wm.ts` — `class WorkingMemory` using `Map>`; `allFacts()` flattens and sorts + - REFACTOR: Add internal change-subscription hook (array of listener callbacks) called on every insert/retract — used later by alpha network. Document the subscription API in JSDoc. + + **Must NOT do**: + - Do NOT emit events during iteration (mutation-during-iteration = undefined behavior) + - Do NOT expose raw Map objects (encapsulation) + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 (with P1.1, P1.3-P1.6) + - **Blocks**: P1.3 (alpha consumes WM events), P1.7 (beta), P1.10 (query) + - **Blocked By**: P0.5, P0.6, P0.1 + + **References**: + - `packages/rete/SPEC.md` §Fact Model, §Iteration Order + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/wm.test.ts` all green + - [ ] Coverage ≥ 95% + - [ ] No raw Map/Set exposed in public API (`grep -E 'export (const|function|class).*(Map|Set)' packages/rete/src/wm.ts` empty) + + **QA Scenarios**: + + ``` + Scenario: WM insert/get/retract/contains semantics + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/wm.test.ts 2>&1 | tee /tmp/p12.log + Expected Result: "Tests {N} passed, 0 failed" + Evidence: .sisyphus/evidence/task-P1.2-wm.log + + Scenario: allFacts() returns deterministic order (failure path for non-determinism) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/wm.test.ts -t "allFacts deterministic order" 2>&1 | tee /tmp/p12-order.log + Expected Result: test named "allFacts deterministic order" passes; verifies same order across multiple invocations with Map insertion-order permutation + Evidence: .sisyphus/evidence/task-P1.2-wm-order.log + ``` + + **Commit**: YES + - Message: `feat(rete): add WorkingMemory with deterministic iteration (P1.2)` + - Files: `packages/rete/src/wm.ts`, `packages/rete/src/wm.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.3. **Alpha network: fact indexing by (id, attr) pattern (TDD)** + + **What to do**: + - RED: `packages/rete/src/alpha.test.ts` — failing tests: + - AlphaNode matches facts by optional id-wildcard + required attr key; stores matched facts in AlphaMemory + - Inserting a fact dispatches it to all matching AlphaNodes + - Retracting a fact removes it from AlphaMemories + - A condition like `(Player, X, ?x)` creates one alpha node indexed by `(attr=X, id=Player)`; `(?id, X, ?x)` indexed by `(attr=X)` + - GREEN: `packages/rete/src/alpha.ts`: + - `class AlphaNetwork` subscribes to `WorkingMemory` events + - `class AlphaNode` with `condition: { id?: EntityId, attr: AttrKey }` + - `class AlphaMemory` holds `Fact[]` sorted by (id, attr) + - `AlphaNetwork.buildNode(cond)` — memoized: same condition → same node (sharing) + - Emits change events (`activate(fact)`, `deactivate(fact)`) to downstream (beta) subscribers + - REFACTOR: Extract indexing (attr → AlphaNode[]) as inverted index; ensure O(1) dispatch per fact + + **Must NOT do**: + - Do NOT scan all alpha nodes per fact (must use index) + - Do NOT retain references to retracted facts + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [`context7`] + - `context7`: Rete alpha network implementation patterns + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 + - **Blocks**: P1.7 (beta network), P1.8 (joins) + - **Blocked By**: P0.5, P0.6, P0.1, P1.2 (WM events) + + **References**: + - `packages/rete/SPEC.md` §Fact Model + - Doorenbos thesis §2.2 (Alpha Network) + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/alpha.test.ts` all green + - [ ] Coverage ≥ 90% + - [ ] Dispatch is O(1) per fact: benchmark test asserting 10k inserts in <50ms + + **QA Scenarios**: + + ``` + Scenario: Alpha network dispatches to matching nodes only + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/alpha.test.ts 2>&1 | tee /tmp/p13.log + Expected Result: "Tests {N} passed, 0 failed" + Evidence: .sisyphus/evidence/task-P1.3-alpha.log + + Scenario: Alpha dispatch performance (failure path if slow) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/alpha.test.ts -t "dispatch 10000 facts in under 50ms" 2>&1 | tee /tmp/p13-perf.log + Expected Result: test passes; log includes timing assertion under 50ms + Evidence: .sisyphus/evidence/task-P1.3-alpha-perf.log + ``` + + **Commit**: YES + - Message: `feat(rete): add AlphaNetwork with inverted-index dispatch (P1.3)` + - Files: `packages/rete/src/alpha.ts`, `packages/rete/src/alpha.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.4. **Session lifecycle: init, add rule, fire (TDD)** + + **What to do**: + - RED: `packages/rete/src/session.test.ts` — failing tests: + - `const session = new Session(schema, { autoFire: false })` creates session + - `session.add(rule)` registers a rule (rule definition opaque for now; covered by P1.5) + - `session.insert(id, attr, value)` / `session.retract(id, attr)` delegate to WM + - `session.fireRules()` returns number of rules that fired + - With `autoFire: true`, insert/retract auto-calls fireRules + - `session.fireRules({ recursionLimit: 64 })` — cycle detection (covered by P1.12, stub throws) + - GREEN: `packages/rete/src/session.ts`: + - `class Session` holding `WorkingMemory`, `AlphaNetwork`, `ProductionNode[]`, config `{ autoFire, recursionLimit }` + - Public API: `add(prod)`, `insert`, `retract`, `fireRules`, `contains`, `get`, `allFacts` + - Fire: iterate pending activations in deterministic order (per SPEC conflict resolution), call RHS, repeat until fixed-point or recursion limit + + **Must NOT do**: + - Do NOT leak internal AlphaNetwork / beta / production types to public API + - Do NOT implement conflict resolution yet (P1.13) — stub with insertion-order + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 + - **Blocks**: P1.7-P1.14 (all downstream engine tasks need Session) + - **Blocked By**: P1.2, P1.3 (WM + Alpha ready) + + **References**: + - `packages/rete/SPEC.md` §Conflict Resolution (stub per insertion-order), §RHS Purity Contract + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/session.test.ts` all green + - [ ] Public API surface locked via `type` export; `tsd` or `expect-type` verifies no `any` leaks + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + + ``` + Scenario: Session lifecycle (insert, fire, retract) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/session.test.ts 2>&1 | tee /tmp/p14.log + Expected Result: "Tests {N} passed, 0 failed" + Evidence: .sisyphus/evidence/task-P1.4-session.log + + Scenario: autoFire flag controls behavior (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/session.test.ts -t "autoFire=false does not fire on insert" 2>&1 | tee /tmp/p14-af.log + Expected Result: named test passes + Evidence: .sisyphus/evidence/task-P1.4-session-autofire.log + ``` + + **Commit**: YES + - Message: `feat(rete): add Session lifecycle (P1.4)` + - Files: `packages/rete/src/session.ts`, `packages/rete/src/session.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.5. **Typed TS builder API + handler registry (TDD)** + + **What to do**: + - RED: `packages/rete/src/builder.test.ts` — failing tests: + - `rule('name').what((Player, X, v('x'))).what((Player, Y, v('y'))).then('moveHandler', ['x', 'y'])` produces a `RuleDefinition` object with conditions and `handler` ref + - `HandlerRegistry.register('moveHandler', (session, match) => { ... })` stores the function + - Attempting to build a rule referencing an unregistered handler throws (at build time, not fire time) + - Variable bindings use `v('name')` helper; unbound variables cause type error + - GREEN: `packages/rete/src/builder.ts` — fluent builder returning `RuleDefinition` + - GREEN: `packages/rete/src/registry.ts` — `HandlerRegistry` (Map-backed, with `register`, `get`, `has`, `verify`) + - Session.add validates all referenced handlers exist via `registry.verify(rule)` + + **Must NOT do**: + - Do NOT allow function references directly in conditions (must be via registry name) — this enforces JSON serializability from day 1 + - Do NOT use `eval` or `new Function` + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [`context7`] + - `context7`: TypeScript builder-pattern type inference + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 + - **Blocks**: P1.6 (JSON serialization needs builder output), P1.7+ (all rule tests use builder) + - **Blocked By**: P1.1 (schema types) + + **References**: + - `packages/rete/SPEC.md` §JSON Rule Schema (handler-registry pattern) + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/builder.test.ts` green + - [ ] `bun test packages/rete/src/registry.test.ts` green + - [ ] Coverage ≥ 90% + - [ ] `grep -r "new Function\|eval(" packages/rete/src` → empty + + **QA Scenarios**: + + ``` + Scenario: Builder produces serializable rule definitions + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/builder.test.ts 2>&1 | tee /tmp/p15.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.5-builder.log + + Scenario: No eval/Function anywhere in engine src (failure path) + Tool: Bash + Steps: + 1. Run: grep -rE "new Function|eval\(" packages/rete/src 2>&1 | tee /tmp/p15-grep.log; echo "exit=$?" + Expected Result: grep exits 1 (no matches); log empty + Evidence: .sisyphus/evidence/task-P1.5-no-eval.log + ``` + + **Commit**: YES + - Message: `feat(rete): add typed rule builder + handler registry (P1.5)` + - Files: `packages/rete/src/builder.ts`, `packages/rete/src/registry.ts`, `packages/rete/src/builder.test.ts`, `packages/rete/src/registry.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.6. **JSON serialization round-trip (TDD)** + + **What to do**: + - RED: `packages/rete/src/serialize.test.ts` — failing tests: + - `serialize(rule)` produces JSON conforming to SPEC §JSON Rule Schema + - `deserialize(json, registry)` produces a `RuleDefinition` equivalent (deep-equal after normalization) + - Round-trip: `deserialize(serialize(rule)) ≡ rule` for every shape (conditions, variables, handler refs, salience) + - Deserialization with unknown handler throws `UnknownHandlerError` + - Schema validation (zod or hand-rolled) rejects malformed JSON + - GREEN: `packages/rete/src/serialize.ts` with `serialize`, `deserialize`, exported JSON schema (as `RULE_SCHEMA_V1` constant) + + **Must NOT do**: + - Do NOT support "v0" or back-compat (there is no prior version) + - Do NOT serialize runtime function references + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave P1.1 + - **Blocks**: P3.13 (JSON import/export UI), P4.2 (server protocol) + - **Blocked By**: P1.5 (builder types) + + **References**: + - `packages/rete/SPEC.md` §JSON Rule Schema + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/serialize.test.ts` green + - [ ] Round-trip test covers ≥10 distinct rule shapes + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + + ``` + Scenario: Round-trip 10 distinct rule shapes + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/serialize.test.ts 2>&1 | tee /tmp/p16.log + 2. Run: grep -c "round-trip shape" /tmp/p16.log + Expected Result: all tests pass; step 2 outputs ≥ 10 + Evidence: .sisyphus/evidence/task-P1.6-roundtrip.log + + Scenario: Malformed JSON rejected (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/serialize.test.ts -t "malformed JSON throws" 2>&1 | tee /tmp/p16-bad.log + Expected Result: named test passes + Evidence: .sisyphus/evidence/task-P1.6-bad-json.log + ``` + + **Commit**: YES + - Message: `feat(rete): add JSON serialize/deserialize round-trip (P1.6)` + - Files: `packages/rete/src/serialize.ts`, `packages/rete/src/serialize.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.7. **Beta network: memory + token propagation (TDD)** + + **What to do**: + - RED: `packages/rete/src/beta.test.ts` — failing tests covering single-condition rule (beta reduces to alpha), two-condition rule (one join), three-condition chain + - GREEN: `packages/rete/src/beta.ts` — `BetaMemory`, `Token` (parent + fact chain), activation/deactivation propagation; each production node accumulates full matches + + **Must NOT do**: + - Do NOT allocate new Tokens on every fact change if shared chains unchanged (reuse via parent reference) + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [`context7`] + + **Parallelization**: + - **Can Run In Parallel**: YES (Wave P1.2 with P1.8, P1.9, P1.10) + - **Blocks**: P1.11-P1.14, P2.* + - **Blocked By**: P1.3 (alpha), P1.4 (session) + + **References**: `packages/rete/SPEC.md` §Iteration Order; Doorenbos §2.4 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/beta.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + + ``` + Scenario: Multi-condition rule produces join matches + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/beta.test.ts 2>&1 | tee /tmp/p17.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.7-beta.log + + Scenario: Retraction removes join matches (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/beta.test.ts -t "retraction removes dependent tokens" 2>&1 | tee /tmp/p17-ret.log + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.7-retract.log + ``` + + **Commit**: YES + - Message: `feat(rete): add BetaMemory + Token propagation (P1.7)` + - Files: `packages/rete/src/beta.ts`, `packages/rete/src/beta.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.8. **Join nodes with variable binding (TDD)** + + **What to do**: + - RED: `packages/rete/src/join.test.ts` — join on shared variable `?id` (e.g., `(?id, X, ?x)` ∧ `(?id, Y, ?y)` must match when id is the same), numeric equality tests + - GREEN: `packages/rete/src/join.ts` — JoinNode with tests[] (equality constraints between left token's binding and right fact's field) + - Handle many-to-many, many-to-one, and cross-product cases + + **Must NOT do**: + - Do NOT implement inequality tests yet (those go in P1.9 conditions) + + **Recommended Agent Profile**: + - **Category**: `deep` + + **Parallelization**: YES — Wave P1.2 + - **Blocks**: P1.11-P1.14, P2.* + - **Blocked By**: P1.7 + + **References**: Doorenbos §2.5 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/join.test.ts` green + - [ ] Coverage ≥ 90% + - [ ] Benchmark: 100 entities × 3-condition join < 10ms + + **QA Scenarios**: + + ``` + Scenario: Multi-variable join matches entities consistently + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/join.test.ts 2>&1 | tee /tmp/p18.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.8-join.log + + Scenario: Join perf benchmark (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/join.test.ts -t "3-condition join on 100 entities under 10ms" 2>&1 | tee /tmp/p18-perf.log + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.8-join-perf.log + ``` + + **Commit**: YES + - Message: `feat(rete): add JoinNode with variable-binding equality tests (P1.8)` + - Files: `packages/rete/src/join.ts`, `packages/rete/src/join.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.9. **Condition filters (cond equivalent) (TDD)** + + **What to do**: + - RED: `packages/rete/src/condition.test.ts` — filter predicates applied after join; predicates are registered (via registry, for JSON serializability) + - GREEN: `packages/rete/src/condition.ts` — `FilterNode` holding `predicate: string` (registry key) + `args: JsonValue[]`; applies to incoming tokens + + **Must NOT do**: + - Do NOT allow inline arrow functions in conditions (must use registry) + + **Recommended Agent Profile**: `deep` + + **Parallelization**: YES — Wave P1.2 + - **Blocks**: P1.11-P1.14, P2.* + - **Blocked By**: P1.8 (join produces tokens to filter) + + **References**: `packages/rete/SPEC.md` §JSON Rule Schema + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/condition.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + + ``` + Scenario: Filter predicate correctly rejects tokens + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/condition.test.ts 2>&1 | tee /tmp/p19.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.9-cond.log + + Scenario: Unregistered predicate throws (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/condition.test.ts -t "unknown predicate throws" 2>&1 + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.9-unknown.log + ``` + + **Commit**: YES + - Message: `feat(rete): add FilterNode with registered predicates (P1.9)` + - Files: `packages/rete/src/condition.ts`, `packages/rete/src/condition.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.10. **Query API: query / queryAll (TDD)** + + **What to do**: + - RED: `packages/rete/src/query.test.ts` — `session.query(rule)` returns first match or throws; `session.queryAll(rule)` returns all; `session.query(rule, { bindings })` filters by binding value + - GREEN: `packages/rete/src/query.ts` — wraps production node's accumulated matches; deterministic order per SPEC + + **Must NOT do**: + - Do NOT allow query on rules without registered production + + **Recommended Agent Profile**: `deep` + + **Parallelization**: YES — Wave P1.2 + - **Blocks**: P2.* + - **Blocked By**: P1.8 (beta produces tokens) + + **References**: `packages/rete/SPEC.md` §Iteration Order + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/query.test.ts` green + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + + ``` + Scenario: query returns deterministic ordering + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/query.test.ts 2>&1 | tee /tmp/p110.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.10-query.log + + Scenario: query on missing rule throws (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/query.test.ts -t "query on unknown rule throws" 2>&1 + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.10-unknown.log + ``` + + **Commit**: YES + - Message: `feat(rete): add query/queryAll API (P1.10)` + - Files: `packages/rete/src/query.ts`, `packages/rete/src/query.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.11. **Derived facts via thenFinally-equivalent (TDD)** + + **What to do**: + - RED: `packages/rete/src/derived.test.ts` — `rule.thenFinally('aggregateHandler', [])` fires after all activations of a tick; derived facts auto-retract when supporting matches disappear (truth maintenance) + - GREEN: `packages/rete/src/derived.ts` — `ProductionNode.thenFinally` handler; tracks derived facts per match chain; on match removal, retracts corresponding derived fact + + **Must NOT do**: + - Do NOT allow derived fact id collision with user facts (derived facts use negative EntityIds) + + **Recommended Agent Profile**: `deep` + + **Parallelization**: YES — Wave P1.3 + - **Blocks**: P1.14, P2.21 (repetition detection uses derived facts) + - **Blocked By**: P1.7-P1.10 + + **References**: `packages/rete/SPEC.md` §Truth Maintenance + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/derived.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + + ``` + Scenario: thenFinally aggregates after tick + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/derived.test.ts 2>&1 | tee /tmp/p111.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.11-derived.log + + Scenario: Derived fact retracts when support retracts (failure path) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/derived.test.ts -t "derived retracts on support loss" 2>&1 + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.11-retract.log + ``` + + **Commit**: YES + - Message: `feat(rete): add derived facts with thenFinally + truth maintenance (P1.11)` + - Files: `packages/rete/src/derived.ts`, `packages/rete/src/derived.test.ts`, `packages/rete/src/index.ts` + - Pre-commit: `bun run check` + +- [ ] P1.12. **Cycle detection with recursion limit (TDD)** + + **What to do**: + - RED: `packages/rete/src/cycle.test.ts` — rule A inserts fact triggering rule B inserting fact triggering A (cycle); `fireRules({ recursionLimit: 4 })` throws `RecursionLimitExceededError` with cycle trace; `recursionLimit: 0` disables (for advanced use) + - GREEN: wire recursion counter into Session.fireRules; build cycle trace (last N activations); error includes rule names + + **Must NOT do**: + - Do NOT silently skip cycles (error must be loud) + + **Recommended Agent Profile**: `deep` + + **Parallelization**: YES — Wave P1.3 + - **Blocks**: P1.14 + - **Blocked By**: P1.4 (session) + + **References**: `packages/rete/SPEC.md` §Cycle Detection + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/cycle.test.ts` green + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + + ``` + Scenario: Cycle exceeding limit throws with trace + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/cycle.test.ts 2>&1 | tee /tmp/p112.log + Expected Result: all pass; trace includes rule names + Evidence: .sisyphus/evidence/task-P1.12-cycle.log + + Scenario: recursionLimit 0 allows unlimited (failure path for infinite loop detection) + Tool: Bash + Steps: + 1. Run: timeout 5 bun test packages/rete/src/cycle.test.ts -t "recursionLimit 0 runs to natural fixpoint" 2>&1 + Expected Result: pass within 5s (natural fixpoint reached) + Evidence: .sisyphus/evidence/task-P1.12-unlimited.log + ``` + + **Commit**: YES + - Message: `feat(rete): add cycle detection with recursionLimit (P1.12)` + - Files: `packages/rete/src/cycle.ts`, `packages/rete/src/cycle.test.ts`, `packages/rete/src/session.ts` + - Pre-commit: `bun run check` + +- [ ] P1.13. **Deterministic conflict resolution (TDD)** + + **What to do**: + - RED: `packages/rete/src/conflict.test.ts` — given N matching activations, firing order is: salience desc → specificity (# conditions) desc → insertion order asc; deterministic across runs + - GREEN: `packages/rete/src/conflict.ts` — `orderActivations(activations)` pure function; integrate into Session.fireRules + + **Must NOT do**: + - Do NOT use `Math.random` for tiebreaking + - Do NOT sort by rule name lexicographically (that hides bugs via alphabetization) + + **Recommended Agent Profile**: `deep` + + **Parallelization**: YES — Wave P1.3 + - **Blocks**: P1.14 + - **Blocked By**: P1.4 + + **References**: `packages/rete/SPEC.md` §Conflict Resolution + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/conflict.test.ts` green + - [ ] Fuzz test: 100 random rule sets, 2 identical runs → identical fire order + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + + ``` + Scenario: Fire order matches spec for mixed salience + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/conflict.test.ts 2>&1 | tee /tmp/p113.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P1.13-conflict.log + + Scenario: Determinism fuzz (failure path for non-det) + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/conflict.test.ts -t "fuzz 100 rule sets yield identical fire order" 2>&1 + Expected Result: pass + Evidence: .sisyphus/evidence/task-P1.13-fuzz.log + ``` + + **Commit**: YES + - Message: `feat(rete): add deterministic conflict resolution (P1.13)` + - Files: `packages/rete/src/conflict.ts`, `packages/rete/src/conflict.test.ts`, `packages/rete/src/session.ts` + - Pre-commit: `bun run check` + +- [ ] P1.14. **Pararules golden-file test port** + + **What to do**: + - Port 5-10 representative pararules tests from `paranim/pararules/tests/*.nim` to TS/Vitest under `packages/rete/tests/golden/` + - Each test = fixture (fact insertion script + rule definitions) + expected query results snapshot + - Add Vitest snapshots for derived-fact cases + - Document each golden's pararules-origin line reference in a `GOLDEN-MAP.md` + + **Must NOT do**: + - Do NOT skip tests that exercise derived facts / multi-condition joins + + **Recommended Agent Profile**: `unspecified-high` + - **Skills**: [`repo-analysis`] + - `repo-analysis`: Retrieve pararules tests from GitHub + + **Parallelization**: NO — Wave P1.4 (parity gate) + - **Blocks**: Phase 2 start + - **Blocked By**: P1.1-P1.13 + + **References**: + - https://github.com/paranim/pararules/blob/master/tests/test1.nim + - https://github.com/paranim/pararules/blob/master/tests/test2.nim + - https://github.com/paranim/pararules/blob/master/tests/test3.nim + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/tests/golden` → all green + - [ ] `packages/rete/tests/golden/GOLDEN-MAP.md` lists each ported test with origin line + - [ ] Coverage of engine src ≥ 90% + - [ ] Tag `v0.1.0-phase1` + + **QA Scenarios**: + + ``` + Scenario: Golden suite passes end-to-end + Tool: Bash + Steps: + 1. Run: bun test packages/rete/tests/golden 2>&1 | tee /tmp/p114.log + 2. Run: bun run test:coverage -- --coverage.reporter=text packages/rete 2>&1 | tee /tmp/p114-cov.log + 3. Run: grep -oE 'All files.*[0-9.]+' /tmp/p114-cov.log | head -1 + Expected Result: all tests pass; line coverage ≥ 90% + Evidence: .sisyphus/evidence/task-P1.14-golden.log, .sisyphus/evidence/task-P1.14-cov.log + + Scenario: Phase 1 tag exists + Tool: Bash + Steps: + 1. Run: git tag v0.1.0-phase1 + 2. Run: git tag | grep v0.1.0-phase1 + Expected Result: tag output present + Evidence: .sisyphus/evidence/task-P1.14-tag.log + ``` + + **Commit**: YES + - Message: `test(rete): port pararules golden tests; tag Phase 1 parity (P1.14)` + - Files: `packages/rete/tests/golden/*.test.ts`, `packages/rete/tests/golden/GOLDEN-MAP.md` + - Pre-commit: `bun run check` + - Post-commit: `git tag v0.1.0-phase1` + +### Phase 2 — Rete II Extensions + Chess Engine + +- [ ] P2.1. **Negation nodes (NOT) (TDD)** + + **What to do**: + - RED: `packages/rete/src/negation.test.ts` — `rule.whatNot((Player, Dead, v(true)))` matches only when no fact satisfies the negated pattern; activation toggles when blocking fact inserted/retracted + - GREEN: `packages/rete/src/negation.ts` — `NegationNode` per Doorenbos §2.6; counts matching facts; token passes iff count is zero + + **Must NOT do**: implement unsafe NOT (unbound vars in NOT) — reject at build time + + **Recommended Agent Profile**: `deep`; Skills: [`context7`] + **Parallelization**: YES — Wave P2.1 (with P2.2, P2.3, P2.4) + **Blocks**: P2.13, P2.18 (check detection uses NOT) + **Blocked By**: P1.14 (Phase 1 gate) + + **References**: Doorenbos §2.6.1 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/negation.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + ``` + Scenario: NOT fires when pattern absent; retracts when inserted + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/negation.test.ts 2>&1 | tee /tmp/p21.log + Expected Result: all pass + Evidence: .sisyphus/evidence/task-P2.1-not.log + Scenario: Unsafe NOT (unbound var) rejected at build (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/negation.test.ts -t "unsafe NOT rejected" 2>&1 + Expected Result: pass + Evidence: .sisyphus/evidence/task-P2.1-unsafe.log + ``` + + **Commit**: YES — `feat(rete): add negation nodes (NOT) (P2.1)` — files: `packages/rete/src/negation.ts`, `packages/rete/src/negation.test.ts` + +- [ ] P2.2. **Existential nodes (EXISTS) (TDD)** + + **What to do**: `rule.whatExists((Attacker, AttacksSquare, v('sq')))` — EXISTS is negation-of-negation; propagate token if ≥1 matching fact. GREEN: `packages/rete/src/existential.ts` + + **Must NOT do**: double-count (increment on same fact twice) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.1 + **Blocks**: P2.18, P2.19 + **Blocked By**: P1.14 + **References**: Doorenbos §2.6.2 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/existential.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + ``` + Scenario: EXISTS toggles correctly + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/existential.test.ts 2>&1 | tee /tmp/p22.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.2-exists.log + Scenario: Multiple supporting facts do not re-activate (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/existential.test.ts -t "single activation despite multiple supports" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.2-single.log + ``` + + **Commit**: YES — `feat(rete): add existential nodes (EXISTS) (P2.2)` + +- [ ] P2.3. **NCC nodes: not-count-condition (TDD)** + + **What to do**: Subconjunction negation — "no matching combination of N conditions exists". GREEN: `packages/rete/src/ncc.ts`. Per Doorenbos §2.6.3, NCC is a sub-network whose top-level production feeds a negation partner. + + **Must NOT do**: collapse NCC into NOT (NCC is strictly more powerful) + **Recommended Agent Profile**: `deep`; Skills: [`context7`] + **Parallelization**: YES — Wave P2.1 + **Blocks**: P2.19 + **Blocked By**: P1.14, P2.1 (reuses negation machinery) + **References**: Doorenbos §2.6.3 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/ncc.test.ts` green + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + ``` + Scenario: NCC rejects when combination exists + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/ncc.test.ts 2>&1 | tee /tmp/p23.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.3-ncc.log + Scenario: NCC partner cleanup on retract (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/ncc.test.ts -t "NCC partner cleans up on retract" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.3-retract.log + ``` + + **Commit**: YES — `feat(rete): add NCC nodes (P2.3)` + +- [ ] P2.4. **Aggregation nodes: count/sum/collect/min/max (TDD)** + + **What to do**: `rule.whatAggregate(count, (?id, Health, v('h')))` returns count bound to variable. Support `count`, `sum`, `min`, `max`, `collect` (array). Incremental update: maintain running total rather than full recompute. GREEN: `packages/rete/src/aggregate.ts` + + **Must NOT do**: full-recompute on every change (performance); operate on raw Set iteration + **Recommended Agent Profile**: `deep`; Skills: [`context7`] + **Parallelization**: YES — Wave P2.1 + **Blocks**: P2.21 (50-move + threefold use aggregation) + **Blocked By**: P1.14 + + **References**: Drools aggregation patterns + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/aggregate.test.ts` green + - [ ] Benchmark: 1000 facts × 5 aggregators < 20ms per full re-run + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + ``` + Scenario: All 5 aggregators produce correct values + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/aggregate.test.ts 2>&1 | tee /tmp/p24.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.4-agg.log + Scenario: Incremental sum on retract (failure path for full recompute) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/aggregate.test.ts -t "sum updates incrementally on retract" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.4-incr.log + ``` + + **Commit**: YES — `feat(rete): add aggregation nodes (count/sum/collect/min/max) (P2.4)` + +- [ ] P2.5. **Chess attribute schema + piece fact shape** + + **What to do**: `packages/chess/src/schema.ts` — define attrs: `PieceType` (pawn|knight|bishop|rook|queen|king), `Color` (white|black), `Square` (a1..h8 as number 0..63), `Position` (id→Square), `HasMoved` (bool for castling), `Turn` (color), `HalfmoveClock` (number), `FullmoveNumber` (number), `EnPassantTarget` (Square?); piece entity convention (each piece = one entity with multiple attrs) + - TDD the schema types (compile-time only test via `expect-type`) + + **Must NOT do**: use strings for squares (numeric 0..63 for perf) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.2 + **Blocks**: P2.8-P2.22 + **Blocked By**: P1.14 + + **References**: `packages/chess/RULES.md`, `packages/rete/SPEC.md` + + **Acceptance Criteria**: + - [ ] `packages/chess/src/schema.ts` exports typed schema + - [ ] `bun x tsc -b packages/chess` → 0 errors + - [ ] `bun test packages/chess/src/schema.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Chess schema compiles with strict types + Tool: Bash + Steps: + 1. Run: bun x tsc -b packages/chess 2>&1 | tee /tmp/p25.log + 2. Run: bun test packages/chess/src/schema.test.ts 2>&1 | tee /tmp/p25-test.log + Expected: step 1 exit 0; step 2 all pass + Evidence: .sisyphus/evidence/task-P2.5-schema.log + Scenario: Square is numeric 0..63 (failure path for string squares) + Tool: Bash + Steps: 1. Run: grep -E "type Square = .*0..63|type Square = .*number" packages/chess/src/schema.ts + Expected: match present + Evidence: .sisyphus/evidence/task-P2.5-square.log + ``` + + **Commit**: YES — `feat(chess): add attribute schema and piece fact shape (P2.5)` + +- [ ] P2.6. **Starting-position fact generator** + + **What to do**: `packages/chess/src/starting-position.ts` — `generateStartingPosition(session)` inserts 32 piece facts for FIDE start. TDD via snapshot of `session.allFacts()` sorted output. + + **Must NOT do**: hardcode as JSON fixture (must be generated deterministically) + **Recommended Agent Profile**: `quick` + **Parallelization**: YES — Wave P2.2 + **Blocks**: P2.8+ + **Blocked By**: P2.5 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/starting-position.test.ts` green + - [ ] Facts match FIDE snapshot + + **QA Scenarios**: + ``` + Scenario: Starting position snapshot matches FIDE + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/starting-position.test.ts 2>&1 | tee /tmp/p26.log + Expected: all pass; snapshot file `__snapshots__/starting-position.test.ts.snap` exists + Evidence: .sisyphus/evidence/task-P2.6-start.log + Scenario: Exactly 32 pieces (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/starting-position.test.ts -t "inserts exactly 32 piece entities" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.6-count.log + ``` + + **Commit**: YES — `feat(chess): add starting-position fact generator (P2.6)` + +- [ ] P2.7. **Square + color helpers** + + **What to do**: `packages/chess/src/coord.ts` — pure functions: `fileOf(square)`, `rankOf(square)`, `squareFromFileRank(f, r)`, `colorOf(square)` (light/dark), `oppositeColor(c)`, `isOnBoard(f, r)`; TDD each + + **Must NOT do**: use string representations internally + **Recommended Agent Profile**: `quick` + **Parallelization**: YES — Wave P2.2 + **Blocks**: P2.9-P2.12 + **Blocked By**: P2.5 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/coord.test.ts` green + - [ ] Coverage ≥ 100% + + **QA Scenarios**: + ``` + Scenario: Coord helpers pure + total + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/coord.test.ts 2>&1 | tee /tmp/p27.log + Expected: all pass; 100% line coverage + Evidence: .sisyphus/evidence/task-P2.7-coord.log + Scenario: Off-board rejection (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/coord.test.ts -t "isOnBoard rejects out-of-range" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.7-off.log + ``` + + **Commit**: YES — `feat(chess): add coordinate + color helpers (P2.7)` + +- [ ] P2.8. **Piece movement primitive rules (directions + steps)** + + **What to do**: `packages/chess/src/rules/primitives.ts` — rule-level primitives that legal-move rules build on: `StraightLineMoves`, `DiagonalMoves`, `SingleStepMoves`, `KnightOffsets`, `PawnSingleAdvance`, `PawnDoubleAdvance`, `PawnDiagonalCapture`. Each primitive is one Rete production generating candidate moves as derived facts (e.g., `CandidateMove(pieceId, targetSquare)`). + + **Must NOT do**: embed legality checks (check/pin/etc) in primitives (those layer in P2.13+) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.2 + **Blocks**: P2.9-P2.14 + **Blocked By**: P2.5, P2.7 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/primitives.test.ts` green + - [ ] Each primitive is a registered rule (listed in a primitives manifest) + + **QA Scenarios**: + ``` + Scenario: Primitives generate candidate moves + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/primitives.test.ts 2>&1 | tee /tmp/p28.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.8-prims.log + Scenario: Primitives do NOT generate captures (separation of concerns, failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/primitives.test.ts -t "primitives produce only non-capture candidates" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.8-sep.log + ``` + + **Commit**: YES — `feat(chess): add movement primitive rules (P2.8)` + +- [ ] P2.9. **Pawn move/capture rules** + + **What to do**: `packages/chess/src/rules/pawn.ts` — productions: `PawnSingleMove`, `PawnDoubleMoveFromHome`, `PawnDiagonalCapture`. Use primitives + filters. Color-aware (white advances +rank, black -rank). Emit `LegalMove(pieceId, from, to)` derived facts. TDD each case including blocked paths. + + **Must NOT do**: handle en passant yet (P2.16) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 (with P2.10-P2.14) + **Blocks**: P2.13, P2.16, P2.17 + **Blocked By**: P2.8 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/pawn.test.ts` green + - [ ] Tests cover: single move, double from home, blocked by own piece, blocked by enemy, diagonal capture, no diagonal without capture + + **QA Scenarios**: + ``` + Scenario: All pawn movement and capture cases + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/pawn.test.ts 2>&1 | tee /tmp/p29.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.9-pawn.log + Scenario: Pawn cannot move diagonally without capture (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/pawn.test.ts -t "pawn diagonal without capture rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.9-diag.log + ``` + + **Commit**: YES — `feat(chess): add pawn move/capture rules (P2.9)` + +- [ ] P2.10. **Knight move rules** + + **What to do**: `packages/chess/src/rules/knight.ts` — 8 L-offsets; leap over other pieces; `LegalMove` emission + + **Must NOT do**: filter path squares (knight leaps) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 + **Blocks**: P2.13 + **Blocked By**: P2.8 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/knight.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Knight L-moves from all positions + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/knight.test.ts 2>&1 | tee /tmp/p210.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.10-knight.log + Scenario: Knight leaps over pieces (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/knight.test.ts -t "knight ignores intervening pieces" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.10-leap.log + ``` + + **Commit**: YES — `feat(chess): add knight move rules (P2.10)` + +- [ ] P2.11. **Bishop/Rook/Queen sliding rules** + + **What to do**: `packages/chess/src/rules/sliding.ts` — `SlidingMove` production parameterized by directions (diagonal, orthogonal, both); uses aggregation or sequential tokens to stop at first blocker (own = stop before; enemy = capture then stop) + + **Must NOT do**: generate moves beyond blocker + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 + **Blocks**: P2.13, P2.15 (castling reads rook moves) + **Blocked By**: P2.8 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/sliding.test.ts` green + - [ ] Tests cover all three pieces × blocker scenarios + + **QA Scenarios**: + ``` + Scenario: Sliding moves stop correctly + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/sliding.test.ts 2>&1 | tee /tmp/p211.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.11-sliding.log + Scenario: Sliding piece cannot jump (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/sliding.test.ts -t "bishop stops at blocker" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.11-stop.log + ``` + + **Commit**: YES — `feat(chess): add bishop/rook/queen sliding rules (P2.11)` + +- [ ] P2.12. **King move rules (basic)** + + **What to do**: `packages/chess/src/rules/king.ts` — 8 adjacent squares; excludes squares occupied by own piece. Castling deferred to P2.15; check-aware rejection deferred to P2.13. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 + **Blocks**: P2.13, P2.15, P2.18, P2.19 + **Blocked By**: P2.8 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/king.test.ts` green + + **QA Scenarios**: + ``` + Scenario: King single-step moves + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/king.test.ts 2>&1 | tee /tmp/p212.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.12-king.log + Scenario: King blocked by own piece (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/king.test.ts -t "king blocked by own piece" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.12-block.log + ``` + + **Commit**: YES — `feat(chess): add king basic move rules (P2.12)` + +- [ ] P2.13. **Turn order + move legality integration** + + **What to do**: `packages/chess/src/rules/turn.ts` — only pieces of current turn's color generate legal moves; after move, turn flips; move-intent fact (`AttemptedMove`) validated vs `LegalMove` set; on success, update piece positions + retract old `LegalMove` facts. Uses negation to reject intents with no matching LegalMove. + + **Must NOT do**: allow movement into check (that's P2.18, but at least queue the integration point here) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 + **Blocks**: P2.14, all further rules + **Blocked By**: P2.9, P2.10, P2.11, P2.12, P2.1 (negation) + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/turn.test.ts` green + - [ ] Full single move validated and applied + + **QA Scenarios**: + ``` + Scenario: Legal move applied; turn switches + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/turn.test.ts 2>&1 | tee /tmp/p213.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.13-turn.log + Scenario: Illegal move rejected, turn unchanged (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/turn.test.ts -t "illegal move rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.13-illegal.log + ``` + + **Commit**: YES — `feat(chess): add turn order + move integration (P2.13)` + +- [ ] P2.14. **Capture resolution rules** + + **What to do**: `packages/chess/src/rules/capture.ts` — when a LegalMove targets an enemy-occupied square, applying the move retracts the captured piece's facts (Position, PieceType, Color) via the RHS handler. + + **Must NOT do**: modify captured piece's facts (they retract entirely in FIDE; other presets may vary — handled in presets) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.3 + **Blocks**: P2.16, P2.19-P2.22 + **Blocked By**: P2.13 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/capture.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Capture removes enemy piece + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/capture.test.ts 2>&1 | tee /tmp/p214.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.14-cap.log + Scenario: Cannot capture own piece (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/capture.test.ts -t "cannot capture own piece" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.14-own.log + ``` + + **Commit**: YES — `feat(chess): add capture resolution (P2.14)` + +- [ ] P2.15. **Castling (kingside + queenside)** + + **What to do**: `packages/chess/src/rules/castling.ts` — productions requiring: King has not moved (HasMoved=false), relevant Rook has not moved, no pieces between, king not in check, transit squares not attacked. Two-piece move: king + rook positions updated atomically. + + **Must NOT do**: allow castling through check + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.4 (with P2.16-P2.18) + **Blocks**: P2.23 (integration test) + **Blocked By**: P2.11, P2.12, P2.18 (check detection for transit squares) + + **References**: FIDE §3.8.2 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/castling.test.ts` green + - [ ] Tests: kingside, queenside, rejected after king moves, rejected through check + + **QA Scenarios**: + ``` + Scenario: Both castling directions + rejection cases + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/castling.test.ts 2>&1 | tee /tmp/p215.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.15-castle.log + Scenario: Castling rejected through check (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/castling.test.ts -t "castling rejected when king passes attacked square" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.15-through.log + ``` + + **Commit**: YES — `feat(chess): add castling rules (P2.15)` + +- [ ] P2.16. **En passant (single-tick capture window)** + + **What to do**: `packages/chess/src/rules/enpassant.ts` — after a pawn's double-advance, set `EnPassantTarget(turn, square)` fact for one turn; eligible-pawn rule emits LegalMove that captures via adjacent target; target fact retracts on next turn. + + **Must NOT do**: allow en passant beyond one turn window + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.4 + **Blocks**: P2.23 + **Blocked By**: P2.9, P2.14 + + **References**: FIDE §3.7.3 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/enpassant.test.ts` green + + **QA Scenarios**: + ``` + Scenario: En passant capture works within 1-turn window + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/enpassant.test.ts 2>&1 | tee /tmp/p216.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.16-ep.log + Scenario: En passant disallowed after window closes (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/enpassant.test.ts -t "en passant window closes after one turn" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.16-window.log + ``` + + **Commit**: YES — `feat(chess): add en passant rule (P2.16)` + +- [ ] P2.17. **Promotion** + + **What to do**: `packages/chess/src/rules/promotion.ts` — when a pawn reaches final rank, retract pawn PieceType fact and insert new PieceType (Q/R/B/N). The choice is specified in the `AttemptedMove` fact via `promoteTo` field; default to Q if missing. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.4 + **Blocks**: P2.23 + **Blocked By**: P2.9 + + **References**: FIDE §3.7.5 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/promotion.test.ts` green + - [ ] Tests: promotion to Q/R/B/N, default-to-queen + + **QA Scenarios**: + ``` + Scenario: Pawn promotion to each valid piece + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/promotion.test.ts 2>&1 | tee /tmp/p217.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.17-promo.log + Scenario: Invalid promotion target rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/promotion.test.ts -t "promotion to king rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.17-invalid.log + ``` + + **Commit**: YES — `feat(chess): add pawn promotion rule (P2.17)` + +- [ ] P2.18. **Check detection** + + **What to do**: `packages/chess/src/rules/check.ts` — derived fact `InCheck(color)` when any enemy piece has a LegalMove targeting that color's king. Uses EXISTS node. Rules that would leave own king in check are filtered out of LegalMove (self-check filter). + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.4 + **Blocks**: P2.15 (castling through check), P2.19, P2.23 + **Blocked By**: P2.2 (exists), P2.9-P2.14 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/check.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Check detected; self-check prevented + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/check.test.ts 2>&1 | tee /tmp/p218.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.18-check.log + Scenario: Move leaving own king in check rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/check.test.ts -t "move exposing own king rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.18-self.log + ``` + + **Commit**: YES — `feat(chess): add check detection + self-check filter (P2.18)` + +- [ ] P2.19. **Checkmate detection** + + **What to do**: `packages/chess/src/rules/checkmate.ts` — derived fact `GameOver(result, reason)` when: `InCheck(turn)` AND no LegalMove exists for any piece of `turn`. Uses NCC. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.5 (with P2.20-P2.22) + **Blocks**: P2.23 + **Blocked By**: P2.3 (NCC), P2.18 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/checkmate.test.ts` green + - [ ] Tests: Fool's Mate, Scholar's Mate, back-rank mate + + **QA Scenarios**: + ``` + Scenario: Checkmate positions detected + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/checkmate.test.ts 2>&1 | tee /tmp/p219.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.19-mate.log + Scenario: Check without mate is not mate (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/checkmate.test.ts -t "check with escape is not mate" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.19-noesc.log + ``` + + **Commit**: YES — `feat(chess): add checkmate detection (P2.19)` + +- [ ] P2.20. **Stalemate detection** + + **What to do**: `packages/chess/src/rules/stalemate.ts` — `GameOver('draw', 'stalemate')` when: NOT `InCheck(turn)` AND no LegalMove exists for `turn`. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.5 + **Blocks**: P2.23 + **Blocked By**: P2.3 (NCC), P2.18 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/stalemate.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Stalemate detected + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/stalemate.test.ts 2>&1 | tee /tmp/p220.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.20-stale.log + Scenario: Checkmate not mistaken for stalemate (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/stalemate.test.ts -t "checkmate distinguished from stalemate" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.20-dist.log + ``` + + **Commit**: YES — `feat(chess): add stalemate detection (P2.20)` + +- [ ] P2.21. **50-move rule + threefold repetition (aggregation-based)** + + **What to do**: `packages/chess/src/rules/draws.ts` — track halfmove clock (resets on pawn move or capture) via a rule; 50-move rule fires at 100 halfmoves. For threefold, maintain a `PositionHash` fact per tick; aggregation counts occurrences of each hash; threshold of 3 → draw claim available. + + **Must NOT do**: auto-claim (threefold is claimable, but plan keeps it auto-triggered on 3rd occurrence for simplicity; documented) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.5 + **Blocks**: P2.23 + **Blocked By**: P2.4 (aggregation), P2.14 + + **References**: FIDE §5.2.2, §5.2.3 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/draws.test.ts` green + + **QA Scenarios**: + ``` + Scenario: 50-move + threefold detected + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/draws.test.ts 2>&1 | tee /tmp/p221.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.21-draws.log + Scenario: Clock reset on capture (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/draws.test.ts -t "halfmove clock resets on capture" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.21-reset.log + ``` + + **Commit**: YES — `feat(chess): add 50-move and threefold repetition rules (P2.21)` + +- [ ] P2.22. **Insufficient material draw** + + **What to do**: `packages/chess/src/rules/insufficient.ts` — draw when material sets are: KvK, KvK+N, KvK+B, K+BvK+B (same color bishop). Uses aggregation count over piece types. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P2.5 + **Blocks**: P2.23 + **Blocked By**: P2.4 (aggregation) + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/rules/insufficient.test.ts` green + + **QA Scenarios**: + ``` + Scenario: All 4 insufficient-material configurations detected + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/insufficient.test.ts 2>&1 | tee /tmp/p222.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P2.22-insuf.log + Scenario: Bishops on opposite colors NOT draw (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/rules/insufficient.test.ts -t "opposite-color bishops is not insufficient" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P2.22-opp.log + ``` + + **Commit**: YES — `feat(chess): add insufficient material draw (P2.22)` + +- [ ] P2.23. **End-to-end FIDE game replay integration test** + + **What to do**: `packages/chess/tests/fide-games/`: include 5 famous games as PGN fixtures (Immortal, Opera, Evergreen, Kasparov vs Topalov 1999, Deep Blue vs Kasparov G6 1997). Write a runner that parses PGN, drives moves through the engine, asserts each move accepted, asserts terminal state (mate/draw/resign). Resigns are not a chess rule — handled as UI-only terminal state for now; filter those from fixtures. + + **Must NOT do**: add a PGN parser dependency (write minimal hand-rolled parser for SAN within `packages/chess/src/pgn.ts`) + **Recommended Agent Profile**: `unspecified-high` + **Parallelization**: NO — Wave P2.6 (gate) + **Blocks**: Phase 3 + **Blocked By**: P2.1-P2.22 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/tests/fide-games` → all 5 games replay to completion + - [ ] Phase 2 tag: `git tag v0.2.0-phase2` + + **QA Scenarios**: + ``` + Scenario: 5 classic games replay end-to-end + Tool: Bash + Steps: + 1. Run: bun test packages/chess/tests/fide-games 2>&1 | tee /tmp/p223.log + 2. Run: grep -c 'PASS.*\.pgn' /tmp/p223.log + Expected: all tests pass; grep >= 5 + Evidence: .sisyphus/evidence/task-P2.23-games.log + Scenario: Phase 2 tag created + Tool: Bash + Steps: 1. Run: git tag v0.2.0-phase2 && git tag | grep v0.2.0-phase2 + Expected: tag present + Evidence: .sisyphus/evidence/task-P2.23-tag.log + ``` + + **Commit**: YES — `test(chess): replay 5 classic FIDE games; tag Phase 2 (P2.23)`; post-commit: `git tag v0.2.0-phase2` + +### Phase 3 — Time-Travel + Presets + UI + +- [ ] P3.1. **Event log: append-only, monotonic sequence numbers (TDD)** + + **What to do**: `packages/rete/src/eventlog.ts` — `class EventLog` records every `insert(id, attr, value)`, `retract(id, attr)`, and rule-fire as `{ seq, ts, kind, payload }`. Append-only; `getSince(seq)` returns entries after seq. Session integrates: every state-mutating call appends to log (if log attached). Tests cover monotonic seq, replay-safe encoding, payload determinism. + + **Must NOT do**: allow out-of-order writes + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.1 (with P3.2, P3.3) + **Blocks**: P3.3, P3.14, P4.7 + **Blocked By**: P2.23 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/eventlog.test.ts` green + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + ``` + Scenario: Log records every mutation monotonically + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/eventlog.test.ts 2>&1 | tee /tmp/p31.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.1-eventlog.log + Scenario: Out-of-order append rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/eventlog.test.ts -t "out-of-order append throws" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P3.1-order.log + ``` + + **Commit**: YES — `feat(rete): add append-only event log with monotonic sequence (P3.1)` + +- [ ] P3.2. **Immer snapshot every N ticks (TDD)** + + **What to do**: `packages/rete/src/snapshot.ts` — on every Nth `fireRules()` call (configurable, default N=30), capture full WM state via Immer's `produce`. Structural sharing minimizes copies. `getSnapshotAt(seq)` returns nearest snapshot ≤ seq. Add `Session` option `snapshotInterval: number`. + + **Must NOT do**: snapshot mid-tick (must be at tick boundary only) + **Recommended Agent Profile**: `deep`; Skills: [`context7`] + **Parallelization**: YES — Wave P3.1 + **Blocks**: P3.3, P3.14 + **Blocked By**: P2.23 + + **References**: Immer docs + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/snapshot.test.ts` green + - [ ] Memory test: 1000 ticks with N=30 produces ~33 snapshots, total memory < 10MB for chess-sized WM + - [ ] Coverage ≥ 90% + + **QA Scenarios**: + ``` + Scenario: Snapshots captured at expected interval + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/snapshot.test.ts 2>&1 | tee /tmp/p32.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.2-snap.log + Scenario: Memory bound with structural sharing (failure path if Immer misused) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/snapshot.test.ts -t "1000 ticks under 10MB" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P3.2-mem.log + ``` + + **Commit**: YES — `feat(rete): add Immer snapshots at tick boundaries (P3.2)` + +- [ ] P3.3. **Replay engine + determinism hash verifier (TDD)** + + **What to do**: `packages/rete/src/replay.ts` — `replayFromLog(log, schema, handlers): Session` reconstructs WM by replaying events on a fresh session. `stateHash(session): string` produces sha256 over sorted facts. Determinism test: recording a random fact/rule sequence, replaying, comparing hashes — must match byte-for-byte. Add `scripts/replay-determinism.ts` runner for CI. + + **Must NOT do**: depend on Map/Set iteration order (sort before hashing) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.1 + **Blocks**: P3.14, P4.7 + **Blocked By**: P3.1, P3.2 + + **Acceptance Criteria**: + - [ ] `bun test packages/rete/src/replay.test.ts` green + - [ ] `bun run scripts/replay-determinism.ts packages/chess/tests/fide-games/*.pgn` → 5/5 hash match + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + ``` + Scenario: Replay hash matches recording hash across 5 games + Tool: Bash + Steps: + 1. Run: bun test packages/rete/src/replay.test.ts 2>&1 | tee /tmp/p33.log + 2. Run: bun run scripts/replay-determinism.ts 2>&1 | tee /tmp/p33-run.log + 3. Run: grep -c 'MATCH' /tmp/p33-run.log + Expected: step 1 pass; step 3 >= 5 + Evidence: .sisyphus/evidence/task-P3.3-replay.log, .sisyphus/evidence/task-P3.3-hashes.log + Scenario: Injected non-determinism detected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/rete/src/replay.test.ts -t "non-deterministic RHS produces MISMATCH" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P3.3-mismatch.log + ``` + + **Commit**: YES — `feat(rete): add replay engine + state-hash determinism verifier (P3.3)` + +- [ ] P3.4. **Preset rules 1-3 (pawn-focused variants)** + + **What to do**: Implement 3 of the 15 presets from `packages/chess/RULES.md` (assume first 3 are pawn-focused: e.g., `pawns-move-backward`, `pawns-diagonal-no-capture`, `double-advance-any-turn`). Each preset = one or more rule definitions in `packages/chess/src/presets/{id}.ts`, a registered toggle in `packages/chess/src/presets/registry.ts`, unit tests, compatibility declarations. + + **Must NOT do**: implement presets outside the first 3 + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.2 (with P3.5-P3.8) + **Blocks**: P3.11 (UI needs presets registered) + **Blocked By**: P2.23, P0.3 (RULES.md) + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/presets/{preset-1,2,3}.test.ts` green + - [ ] 3 presets registered; registry has 3 entries in this task + + **QA Scenarios**: + ``` + Scenario: Presets 1-3 toggle on/off correctly + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets 2>&1 | tee /tmp/p34.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.4-presets.log + Scenario: Incompatible presets flag conflict (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets -t "incompatible pair flagged" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P3.4-incompat.log + ``` + + **Commit**: YES — `feat(chess): add preset rules 1-3 (P3.4)` + +- [ ] P3.5. **Preset rules 4-6 (knight/bishop variants)** + + **What to do**: Implement presets 4-6 from RULES.md. Same structure as P3.4. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.2 + **Blocks**: P3.11 + **Blocked By**: P2.23, P0.3 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/presets` includes 6 preset files green; registry has 6 entries + + **QA Scenarios**: + ``` + Scenario: Presets 4-6 functional + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets/knight*.test.ts packages/chess/src/presets/bishop*.test.ts 2>&1 | tee /tmp/p35.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.5-presets.log + Scenario: Registry expanded to 6 (failure path) + Tool: Bash + Steps: 1. Run: bun -e "import {REGISTRY} from './packages/chess/src/presets/registry.ts'; console.log(Object.keys(REGISTRY).length)" + Expected: stdout "6" + Evidence: .sisyphus/evidence/task-P3.5-count.log + ``` + + **Commit**: YES — `feat(chess): add preset rules 4-6 (P3.5)` + +- [ ] P3.6. **Preset rules 7-9 (rook/queen/king variants)** + + **What to do**: Implement presets 7-9 from RULES.md. + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.2 + **Blocks**: P3.11 + **Blocked By**: P2.23, P0.3 + + **Acceptance Criteria**: registry has 9 entries; all tests green + + **QA Scenarios**: + ``` + Scenario: Presets 7-9 functional + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets/rook*.test.ts packages/chess/src/presets/queen*.test.ts packages/chess/src/presets/king*.test.ts 2>&1 | tee /tmp/p36.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.6.log + Scenario: Registry has 9 entries (failure path) + Tool: Bash + Steps: 1. Run: bun -e "import {REGISTRY} from './packages/chess/src/presets/registry.ts'; console.log(Object.keys(REGISTRY).length)" + Expected: stdout "9" + Evidence: .sisyphus/evidence/task-P3.6-count.log + ``` + + **Commit**: YES — `feat(chess): add preset rules 7-9 (P3.6)` + +- [ ] P3.7. **Preset rules 10-12 (board/geometry variants)** + + **What to do**: Implement presets 10-12 from RULES.md — board-geometry changes (e.g., horizontal wrap). These modify coord helpers via override or interception rule. + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.2 + **Blocks**: P3.11 + **Blocked By**: P2.23, P0.3 + + **Acceptance Criteria**: registry has 12 entries; all tests green + + **QA Scenarios**: + ``` + Scenario: Geometry presets alter legal moves correctly + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets/wrap*.test.ts packages/chess/src/presets/geometry*.test.ts 2>&1 | tee /tmp/p37.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.7.log + Scenario: Wrap preset enables horizontal movement across board edge (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets/wrap-horizontal.test.ts -t "rook crosses file-a to file-h" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P3.7-wrap.log + ``` + + **Commit**: YES — `feat(chess): add preset rules 10-12 (P3.7)` + +- [ ] P3.8. **Preset rules 13-15 (meta rules: HP/heal/immunity)** + + **What to do**: Implement presets 13-15 from RULES.md — introduce HP/cooldown/immunity attributes in chess schema extensions (within chess package only, not engine). These require adding extended attrs to chess schema (via `extendChessSchema` helper), supporting facts (HP defaults to 1 for FIDE). + + **Must NOT do**: leak chess-schema extensions into engine core + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P3.2 + **Blocks**: P3.11 + **Blocked By**: P2.23, P0.3 + + **Acceptance Criteria**: registry has 15 entries; HP-aware rules tested + + **QA Scenarios**: + ``` + Scenario: HP preset: captures deal 1 damage; piece with 2 HP survives first hit + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/presets/hp*.test.ts packages/chess/src/presets/heal*.test.ts packages/chess/src/presets/immune*.test.ts 2>&1 | tee /tmp/p38.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P3.8.log + Scenario: Full registry has 15 entries (failure path) + Tool: Bash + Steps: 1. Run: bun -e "import {REGISTRY} from './packages/chess/src/presets/registry.ts'; console.log(Object.keys(REGISTRY).length)" + Expected: stdout "15" + Evidence: .sisyphus/evidence/task-P3.8-count.log + ``` + + **Commit**: YES — `feat(chess): add preset rules 13-15 (P3.8)` + +- [ ] P3.9. **React + Vite scaffold for chess app** + + **What to do**: Wire up Vite + React 19 (or latest) in `packages/chess/`: `index.html`, `src/app/main.tsx`, `src/app/App.tsx` (root, routes: Home, Game, Rules, Save), Vite config with base URL, Tailwind for styling (or CSS modules if Tailwind explicitly disliked by user; default Tailwind). Bundle size placeholder — checked by size-limit later. + + **Must NOT do**: install UI libraries beyond React, Tailwind, minimal dnd (react-dnd) if needed — no Material UI, no Ant Design + **Recommended Agent Profile**: `visual-engineering`; Skills: [`interface-design`, `context7`] + **Parallelization**: YES — Wave P3.3 (with P3.10-P3.13 — P3.10 depends on P3.9) + **Blocks**: P3.10-P3.13 + **Blocked By**: P2.23 + + **References**: Vite React guide; Tailwind setup + + **Acceptance Criteria**: + - [ ] `cd packages/chess && bun run dev` starts at localhost:5173 + - [ ] `bun run build` produces static dist/ + - [ ] Playwright can open the home route and see an app root element + + **QA Scenarios**: + ``` + Scenario: Dev server starts; home page renders root + Tool: Playwright + Preconditions: bun run dev started in background on port 5173 + Steps: + 1. Navigate to http://localhost:5173/ + 2. Wait for selector '[data-testid="app-root"]' + 3. Screenshot + Expected: root element visible + Evidence: .sisyphus/evidence/task-P3.9-home.png + + Scenario: Build produces static bundle (failure path for missing build script) + Tool: Bash + Steps: 1. Run: cd packages/chess && bun run build && ls -la dist/ + Expected: dist/ contains index.html + Evidence: .sisyphus/evidence/task-P3.9-build.log + ``` + + **Commit**: YES — `feat(chess): scaffold Vite + React app (P3.9)` + +- [ ] P3.10. **Chessboard component with drag-drop + legal-move highlights** + + **What to do**: `packages/chess/src/ui/Board.tsx` — 8×8 grid, piece SVG icons (inline or public/), drag-drop via HTML5 DnD or react-dnd; on drag-start, query engine for that piece's LegalMoves and highlight target squares; on drop, dispatch AttemptedMove fact. Uses `useSession()` hook providing reactive fact subscriptions (implemented via tick-subscription observer on session). + + **Must NOT do**: load piece images from external CDN (bundle locally) + **Recommended Agent Profile**: `visual-engineering`; Skills: [`interface-design`] + **Parallelization**: NO (depends on P3.9) — Wave P3.3 + **Blocks**: P3.15 + **Blocked By**: P3.9 + + **Acceptance Criteria**: + - [ ] Board renders with 32 pieces in starting position + - [ ] Drag pawn e2→e4: piece moves on board; engine fact updated + - [ ] Illegal move: piece snaps back; no fact change + + **QA Scenarios**: + ``` + Scenario: Legal drag-drop move applied + Tool: Playwright + Steps: + 1. Navigate to http://localhost:5173/game + 2. Locator '[data-square="e2"]' → dragTo '[data-square="e4"]' + 3. Wait selector '[data-square="e4"] [data-piece="white-pawn"]' + 4. Screenshot + Expected: pawn on e4 + Evidence: .sisyphus/evidence/task-P3.10-e2e4.png + + Scenario: Illegal move rejected (failure path) + Tool: Playwright + Steps: + 1. Navigate to /game + 2. Locator '[data-square="e2"]' dragTo '[data-square="e5"]' (illegal double-plus) + 3. Wait selector '[data-square="e2"] [data-piece="white-pawn"]' (pawn still home) + 4. Screenshot + Expected: pawn returned + Evidence: .sisyphus/evidence/task-P3.10-reject.png + ``` + + **Commit**: YES — `feat(chess): add interactive Chessboard with drag-drop (P3.10)` + +- [ ] P3.11. **Rule-toggle screen (preset list + compatibility warnings)** + + **What to do**: `packages/chess/src/ui/Rules.tsx` — list all 15 presets with description, toggle switch, compat-warning banner when incompatibility detected; "Apply and start new game" button; toggles only between games (disabled during active game — grayed state). + + **Recommended Agent Profile**: `visual-engineering` + **Parallelization**: YES — Wave P3.3 + **Blocks**: P3.15 + **Blocked By**: P3.4-P3.8, P3.9 + + **Acceptance Criteria**: + - [ ] 15 toggle rows render; enabling two incompatibles shows warning + - [ ] Starting new game applies enabled presets + + **QA Scenarios**: + ``` + Scenario: Toggle preset, start new game, effect observable + Tool: Playwright + Steps: + 1. Navigate to /rules + 2. Click '[data-preset="pawns-move-backward"] [data-role="toggle"]' + 3. Click '[data-action="start-new-game"]' + 4. Navigate to /game + 5. Locator '[data-square="e2"]' dragTo '[data-square="e1"]' (backward move; normally illegal) + 6. Wait selector '[data-square="e1"] [data-piece="white-pawn"]' + Expected: pawn moved backward + Evidence: .sisyphus/evidence/task-P3.11-back.png + + Scenario: Incompatible presets show warning (failure path) + Tool: Playwright + Steps: + 1. Navigate to /rules + 2. Enable two presets listed as incompatible in RULES.md + 3. Expect '[data-testid="compat-warning"]' visible + Expected: warning shown + Evidence: .sisyphus/evidence/task-P3.11-warn.png + ``` + + **Commit**: YES — `feat(chess): add rule-toggle UI with compatibility warnings (P3.11)` + +- [ ] P3.12. **Save/Load panel + undo via time-travel** + + **What to do**: `packages/chess/src/ui/SavePanel.tsx` + undo button in Game view; undo uses time-travel to rewind to previous `Turn`-changed fact boundary (one full move back); save panel lists slots from localStorage (schema-versioned JSON). + + **Recommended Agent Profile**: `visual-engineering` + **Parallelization**: YES — Wave P3.3 + **Blocks**: P3.14, P3.15 + **Blocked By**: P3.3, P3.9 + + **Acceptance Criteria**: + - [ ] Undo rewinds one full move + - [ ] Save to slot, reload page, load — same position + + **QA Scenarios**: + ``` + Scenario: Undo reverts one move + Tool: Playwright + Steps: + 1. Navigate to /game + 2. Drag e2→e4; drag e7→e5 + 3. Click '[data-action="undo"]' + 4. Assert '[data-square="e5"] [data-piece]' is NOT black-pawn (reverted) + 5. Assert turn indicator shows 'black' + Expected: state reverted + Evidence: .sisyphus/evidence/task-P3.12-undo.png + + Scenario: Save/load round-trip (failure path) + Tool: Playwright + Steps: + 1. Play 4 moves + 2. Click '[data-action="save"]' into slot "test" + 3. page.reload() + 4. Click '[data-action="load"]' slot "test" + 5. Assert board state matches pre-reload + Evidence: .sisyphus/evidence/task-P3.12-saveload.png + ``` + + **Commit**: YES — `feat(chess): add Save/Load panel + time-travel undo (P3.12)` + +- [ ] P3.13. **JSON export/import + validation** + + **What to do**: `packages/chess/src/ui/ImportExport.tsx` + `packages/chess/src/persist/io.ts` — export button produces a downloadable JSON file (schema: `{ version: 1, rules: [...], facts: [...] }`); import button accepts file, validates against schema (via `@paratype/rete`'s exported schema + chess extension schema), applies rules + facts. + + **Must NOT do**: allow importing from untrusted URL (file-upload only) + **Recommended Agent Profile**: `visual-engineering` + **Parallelization**: YES — Wave P3.3 + **Blocks**: P3.15 + **Blocked By**: P1.6, P3.9 + + **Acceptance Criteria**: + - [ ] Export downloads valid JSON parseable by the importer + - [ ] Invalid JSON shows user-facing error, no crash + + **QA Scenarios**: + ``` + Scenario: Export then re-import round-trip + Tool: Playwright + Steps: + 1. Navigate to /game; make 3 moves + 2. Click '[data-action="export"]'; Playwright captures download as /tmp/export.json + 3. Click '[data-action="import"]'; upload /tmp/export.json + 4. Assert board state matches pre-import + Evidence: .sisyphus/evidence/task-P3.13-export.json, .sisyphus/evidence/task-P3.13-import.png + + Scenario: Malformed JSON rejected with user message (failure path) + Tool: Playwright + Steps: + 1. Click '[data-action="import"]'; upload fixture with `{"bad":"data"}` + 2. Assert '[data-testid="import-error"]' visible with descriptive message + Evidence: .sisyphus/evidence/task-P3.13-bad.png + ``` + + **Commit**: YES — `feat(chess): add JSON export/import with validation (P3.13)` + +- [ ] P3.14. **localStorage auto-save + restore** + + **What to do**: `packages/chess/src/persist/autosave.ts` — subscribe to session tick end; on every turn boundary, write serialized state + event log to localStorage key `paratype-chess:v1:autosave`. On app load, if key present, restore via `replayFromLog`. Include schema version in payload. + + **Must NOT do**: write on every tick (too noisy); write to sessionStorage (lost on close) + **Recommended Agent Profile**: `unspecified-high` + **Parallelization**: YES — Wave P3.4 (with P3.15) + **Blocks**: P3.15 + **Blocked By**: P3.3, P3.12 + + **Acceptance Criteria**: + - [ ] After 3 moves, localStorage has `paratype-chess:v1:autosave` + - [ ] Reload page → game resumes in same position + + **QA Scenarios**: + ``` + Scenario: Autosave persists across reload + Tool: Playwright + Steps: + 1. Navigate to /game; play 5 moves + 2. localStorage.getItem('paratype-chess:v1:autosave') not null + 3. Reload + 4. Assert board state matches + Evidence: .sisyphus/evidence/task-P3.14-autosave.png + + Scenario: Schema version mismatch discards silently (failure path) + Tool: Playwright + Steps: + 1. Set localStorage to stale payload with version 0 + 2. Reload + 3. Assert new game started (no crash) + Evidence: .sisyphus/evidence/task-P3.14-stale.png + ``` + + **Commit**: YES — `feat(chess): add localStorage auto-save and restore (P3.14)` + +- [ ] P3.15. **End-to-end UI scenario (gate)** + + **What to do**: Playwright scenario at `packages/chess/e2e/full-flow.spec.ts` — open app → toggle 2 presets → start game → play 5 moves → save → reload → game restored → export → import in fresh context → play 3 more moves → undo → play until checkmate (scripted sequence) → assert Game Over banner. + + **Must NOT do**: use timing-based waits (`waitForTimeout` is banned; use selector waits) + **Recommended Agent Profile**: `unspecified-high`; Skills: [`playwright`] + **Parallelization**: NO — Wave P3.4 (gate) + **Blocks**: Phase 4 + **Blocked By**: P3.1-P3.14 + + **Acceptance Criteria**: + - [ ] `bun x playwright test packages/chess/e2e/full-flow.spec.ts` green + - [ ] Video + trace artifacts captured + - [ ] Phase 3 tag: `git tag v0.3.0-phase3` + + **QA Scenarios**: + ``` + Scenario: Full flow end-to-end + Tool: Playwright + Preconditions: bun run dev serving packages/chess + Steps: (executed by the spec file; evidence is trace + video) + Expected Result: spec passes; video shows full flow + Evidence: .sisyphus/evidence/task-P3.15-full-flow.webm, .sisyphus/evidence/task-P3.15-trace.zip + + Scenario: Phase 3 tag present + Tool: Bash + Steps: 1. Run: git tag v0.3.0-phase3 && git tag | grep v0.3.0-phase3 + Expected: present + Evidence: .sisyphus/evidence/task-P3.15-tag.log + ``` + + **Commit**: YES — `test(chess): e2e full-flow scenario; tag Phase 3 (P3.15)`; post-commit: `git tag v0.3.0-phase3` + +### Phase 4 — Authoritative Multiplayer + +- [ ] P4.1. **Bun HTTP+WS server scaffold + config** + + **What to do**: `packages/server/src/index.ts` — `Bun.serve({ port, fetch, websocket: { open, message, close } })`; env-driven port (default 7357); health endpoint `GET /healthz` returning `{ ok: true, version }`; structured pino logger with request id; graceful shutdown on SIGINT. + + **Recommended Agent Profile**: `unspecified-high`; Skills: [`context7`] + **Parallelization**: YES — Wave P4.1 (with P4.2-P4.4) + **Blocks**: P4.5-P4.11 + **Blocked By**: P3.15 + + **References**: Bun.serve docs, pino + + **Acceptance Criteria**: + - [ ] `bun run packages/server/src/index.ts` starts; `curl localhost:7357/healthz` returns 200 + - [ ] Logs emit JSON lines + + **QA Scenarios**: + ``` + Scenario: Server responds to health check + Tool: Bash + Steps: + 1. Run: bun run packages/server/src/index.ts & + 2. Sleep 2 + 3. Run: curl -sS -o /tmp/p41.json -w "%{http_code}" http://localhost:7357/healthz + 4. Kill %1 + Expected: status 200; body has {"ok":true} + Evidence: .sisyphus/evidence/task-P4.1-health.log + + Scenario: SIGINT shuts down gracefully (failure path) + Tool: Bash + Steps: + 1. Run: bun run packages/server/src/index.ts & + 2. SIGINT; wait; echo $? + Expected: exit 0 + Evidence: .sisyphus/evidence/task-P4.1-shutdown.log + ``` + + **Commit**: YES — `feat(server): scaffold Bun HTTP+WS server with health + logging (P4.1)` + +- [ ] P4.2. **Message schemas + validation (TDD)** + + **What to do**: `packages/server/src/protocol.ts` — zod schemas per PROTOCOL.md message type; `validateMessage(raw): Result`; top-level `v` version check; round-trip tested. + + **Must NOT do**: use JSON.parse without validation + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.1 + **Blocks**: P4.5, P4.6 + **Blocked By**: P0.4 (PROTOCOL.md), P3.15 + + **Acceptance Criteria**: + - [ ] `bun test packages/server/src/protocol.test.ts` green + - [ ] Coverage ≥ 95% + + **QA Scenarios**: + ``` + Scenario: All 8+ message types round-trip + Tool: Bash + Steps: 1. Run: bun test packages/server/src/protocol.test.ts 2>&1 | tee /tmp/p42.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.2-proto.log + Scenario: Malformed message rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/server/src/protocol.test.ts -t "invalid v rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.2-bad.log + ``` + + **Commit**: YES — `feat(server): add protocol schemas + validation (P4.2)` + +- [ ] P4.3. **Room model (create/join/leave, 6-char codes)** + + **What to do**: `packages/server/src/rooms.ts` — `class RoomRegistry` with `createRoom()` → 6-char [A-Z0-9] code + uuid-v4 token; `joinRoom(code, token)`; 2-player max; token-authenticated per message; TDD. + + **Must NOT do**: persist across restart (v1 constraint) + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.1 + **Blocks**: P4.5 + **Blocked By**: P3.15 + + **Acceptance Criteria**: + - [ ] `bun test packages/server/src/rooms.test.ts` green + - [ ] Code generation uniqueness fuzz (1000 codes, 0 collisions expected) + + **QA Scenarios**: + ``` + Scenario: Room create, join, duplicate-join-rejected + Tool: Bash + Steps: 1. Run: bun test packages/server/src/rooms.test.ts 2>&1 | tee /tmp/p43.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.3-rooms.log + Scenario: Third player rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/server/src/rooms.test.ts -t "third join rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.3-third.log + ``` + + **Commit**: YES — `feat(server): add room registry with codes + tokens (P4.3)` + +- [ ] P4.4. **Rate limiting + origin allow-list + 64KB cap** + + **What to do**: `packages/server/src/middleware.ts` — per-connection token bucket (100 msg/sec, burst 20); WebSocket upgrade rejects non-allow-list origins (configurable via env `ALLOWED_ORIGINS`); reject payloads > 64KB with disconnect. + + **Recommended Agent Profile**: `unspecified-high` + **Parallelization**: YES — Wave P4.1 + **Blocks**: P4.12 + **Blocked By**: P3.15 + + **Acceptance Criteria**: + - [ ] `bun test packages/server/src/middleware.test.ts` green + - [ ] Stress test: 200 msg/sec triggers RATE_LIMIT disconnect + + **QA Scenarios**: + ``` + Scenario: Rate-limit trips on over-limit + Tool: Bash + Steps: 1. Run: bun test packages/server/src/middleware.test.ts 2>&1 | tee /tmp/p44.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.4-rl.log + Scenario: Origin disallowed rejected (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/server/src/middleware.test.ts -t "origin not in allow-list rejected" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.4-origin.log + ``` + + **Commit**: YES — `feat(server): add rate-limit, origin allow-list, message-size cap (P4.4)` + +- [ ] P4.5. **Authoritative session per room** + + **What to do**: `packages/server/src/game-session.ts` — each room holds a `Session` from `@paratype/rete` + chess rules; server is the only one that calls `insert/retract/fireRules`. Fact IDs minted here only. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.2 + **Blocks**: P4.6, P4.12 + **Blocked By**: P4.1, P4.2, P4.3 + + **Acceptance Criteria**: + - [ ] `bun test packages/server/src/game-session.test.ts` green + + **QA Scenarios**: + ``` + Scenario: Each room has isolated session state + Tool: Bash + Steps: 1. Run: bun test packages/server/src/game-session.test.ts 2>&1 | tee /tmp/p45.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.5-sess.log + Scenario: Fact IDs do not collide across rooms (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/server/src/game-session.test.ts -t "room fact ids distinct" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.5-ids.log + ``` + + **Commit**: YES — `feat(server): add authoritative game session per room (P4.5)` + +- [ ] P4.6. **Move-intent validation + fact-delta broadcast** + + **What to do**: `packages/server/src/broadcast.ts` — on `game.move` intent: insert `AttemptedMove` fact; fire rules; diff pre/post WM; broadcast `game.delta` with added/removed facts to both clients. Assigned `seq` per delta for reconnection. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.2 + **Blocks**: P4.12 + **Blocked By**: P4.5 + + **Acceptance Criteria**: + - [ ] Integration test: send legal move → both clients receive delta with updated Position fact + - [ ] Illegal move → `error` message; no broadcast + + **QA Scenarios**: + ``` + Scenario: Legal move broadcast to both clients + Tool: Bash (WS client script) + Steps: + 1. Launch server + 2. Run: bun run scripts/ws-client.ts --script fixtures/two-client-legal-move.json + Expected: both clients receive matching game.delta with Position change + Evidence: .sisyphus/evidence/task-P4.6-delta.json + Scenario: Illegal move rejected; no broadcast (failure path) + Tool: Bash + Steps: 1. Run: bun run scripts/ws-client.ts --script fixtures/illegal-move.json + Expected: error to sender; zero delta messages + Evidence: .sisyphus/evidence/task-P4.6-illegal.json + ``` + + **Commit**: YES — `feat(server): add move validation + fact-delta broadcast (P4.6)` + +- [ ] P4.7. **Reconnection flow (60s window, snapshot resume)** + + **What to do**: `packages/server/src/reconnect.ts` — on disconnect, start 60s timer; during grace, incoming (code, token) matches → resume and send `game.state` (full snapshot) + all deltas since client's last `seq`. After 60s, room aborts with `game.end` broadcast to remaining client. + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.2 + **Blocks**: P4.12 + **Blocked By**: P4.5, P4.6, P3.3 (replay for determinism) + + **Acceptance Criteria**: + - [ ] Integration test: disconnect, reconnect within 30s, resume state exactly + + **QA Scenarios**: + ``` + Scenario: Reconnect within grace resumes game + Tool: Bash + Steps: 1. Run: bun run scripts/ws-client.ts --script fixtures/reconnect-within-grace.json + Expected: client B reconnects, receives state, continues game + Evidence: .sisyphus/evidence/task-P4.7-reconnect.json + Scenario: Reconnect after grace fails with game.end (failure path) + Tool: Bash + Steps: 1. Run: bun run scripts/ws-client.ts --script fixtures/reconnect-after-grace.json + Expected: rejected; remaining client received game.end + Evidence: .sisyphus/evidence/task-P4.7-expired.json + ``` + + **Commit**: YES — `feat(server): add reconnection with 60s grace + snapshot resume (P4.7)` + +- [ ] P4.8. **Structured logging + metrics** + + **What to do**: `packages/server/src/logging.ts` — pino logger with request-scoped `roomId`, `clientId`, `seq`; per-tick duration metric; `/metrics` endpoint (Prometheus text format) with counters: `rooms_active`, `messages_received_total`, `moves_validated_total{result}`, tick duration histogram. + + **Recommended Agent Profile**: `unspecified-high` + **Parallelization**: YES — Wave P4.2 + **Blocks**: P4.12 + **Blocked By**: P4.1 + + **Acceptance Criteria**: + - [ ] `curl localhost:7357/metrics` returns text/plain with expected series + + **QA Scenarios**: + ``` + Scenario: Metrics endpoint exposes required series + Tool: Bash + Steps: + 1. Run: bun run packages/server/src/index.ts & + 2. Sleep 2 + 3. Run: curl -sS http://localhost:7357/metrics | grep -E 'rooms_active|messages_received_total|moves_validated_total|tick_duration' + 4. Kill %1 + Expected: all 4 series present + Evidence: .sisyphus/evidence/task-P4.8-metrics.log + Scenario: Log lines are valid JSON (failure path) + Tool: Bash + Steps: + 1. Run: bun run packages/server/src/index.ts 2>&1 | head -20 | jq -e . + Expected: exit 0 for each line (jq parses) + Evidence: .sisyphus/evidence/task-P4.8-logs.log + ``` + + **Commit**: YES — `feat(server): add pino logging and Prometheus metrics (P4.8)` + +- [ ] P4.9. **WebSocket client library with reconnect + seq ack** + + **What to do**: `packages/chess/src/net/client.ts` — `class GameClient` with `connect(code, token)`, exponential-backoff reconnect, sequence-ack tracking, event emitter for `game.state`, `game.delta`, `error`. Client owns a local engine session but only applies deltas received from server (no self-validation of moves). + + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.3 (with P4.10, P4.11) + **Blocks**: P4.12 + **Blocked By**: P4.2 (protocol schemas) + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/net/client.test.ts` green + - [ ] Reconnect after drop succeeds within 30s + + **QA Scenarios**: + ``` + Scenario: Client handshake + delta application + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/net/client.test.ts 2>&1 | tee /tmp/p49.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.9-client.log + Scenario: Reconnect after forced disconnect (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/net/client.test.ts -t "reconnect restores state" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.9-recon.log + ``` + + **Commit**: YES — `feat(chess): add WebSocket client library with reconnect (P4.9)` + +- [ ] P4.10. **Client prediction + server reconciliation** + + **What to do**: `packages/chess/src/net/prediction.ts` — on user drag-drop, client locally applies move optimistically to engine session; sends intent to server; on `game.delta`, reconciles (replaces predicted state with authoritative state). On `error` response, rolls back. + + **Must NOT do**: drift — always re-hash local state against server snapshot on receipt; mismatch → resync from server full state + **Recommended Agent Profile**: `deep` + **Parallelization**: YES — Wave P4.3 + **Blocks**: P4.12 + **Blocked By**: P4.9 + + **Acceptance Criteria**: + - [ ] `bun test packages/chess/src/net/prediction.test.ts` green + - [ ] Simulated latency (100ms artificial delay) doesn't cause desync + + **QA Scenarios**: + ``` + Scenario: Optimistic prediction matches authoritative result + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/net/prediction.test.ts 2>&1 | tee /tmp/p410.log + Expected: all pass + Evidence: .sisyphus/evidence/task-P4.10-pred.log + Scenario: Rejected prediction rolls back (failure path) + Tool: Bash + Steps: 1. Run: bun test packages/chess/src/net/prediction.test.ts -t "rejected intent rolls back" 2>&1 + Expected: pass + Evidence: .sisyphus/evidence/task-P4.10-rollback.log + ``` + + **Commit**: YES — `feat(chess): add client prediction + server reconciliation (P4.10)` + +- [ ] P4.11. **Room lobby UI (create/join screens)** + + **What to do**: `packages/chess/src/ui/Lobby.tsx` — home route with two buttons: "Create Room" (shows generated code, share link) and "Join Room" (input for code). After join, redirect to `/game` with active session. + + **Recommended Agent Profile**: `visual-engineering`; Skills: [`interface-design`] + **Parallelization**: YES — Wave P4.3 + **Blocks**: P4.12 + **Blocked By**: P4.9 + + **Acceptance Criteria**: + - [ ] Playwright: create room in ctx A, join in ctx B, both see game + - [ ] Invalid code shows error + + **QA Scenarios**: + ``` + Scenario: Two contexts join same room + Tool: Playwright + Steps: + 1. Context A navigates /; clicks [data-action="create-room"]; notes [data-testid="room-code"] value (CODE) + 2. Context B navigates /; types CODE in [data-testid="room-code-input"]; clicks [data-action="join-room"] + 3. Both reach /game; both see starting position + Expected: both boards render + Evidence: .sisyphus/evidence/task-P4.11-create.png, .sisyphus/evidence/task-P4.11-join.png + + Scenario: Invalid code errors (failure path) + Tool: Playwright + Steps: + 1. Navigate /; type "XXXXXX"; click join + 2. Assert [data-testid="lobby-error"] visible + Evidence: .sisyphus/evidence/task-P4.11-bad.png + ``` + + **Commit**: YES — `feat(chess): add lobby UI for create/join rooms (P4.11)` + +- [ ] P4.12. **E2E multiplayer scenario (Phase 4 gate)** + + **What to do**: `packages/chess/e2e/multiplayer.spec.ts` — launches server + client (via Playwright webServer config); two contexts create/join room, play 10-move game alternating sides; ctx A disconnects at move 6, reconnects at move 7; game completes to checkmate; assert both clients see identical final state. + + **Must NOT do**: use fixed sleeps; use selector waits + **Recommended Agent Profile**: `unspecified-high`; Skills: [`playwright`] + **Parallelization**: NO — Wave P4.4 (gate) + **Blocks**: Final Wave + **Blocked By**: P4.1-P4.11 + + **Acceptance Criteria**: + - [ ] `bun x playwright test packages/chess/e2e/multiplayer.spec.ts` green + - [ ] Phase 4 tag: `git tag v0.4.0-phase4` + + **QA Scenarios**: + ``` + Scenario: Two-browser full multiplayer game with mid-game reconnect + Tool: Playwright (see spec) + Expected: spec passes; video captured + Evidence: .sisyphus/evidence/task-P4.12-mp.webm, .sisyphus/evidence/task-P4.12-trace.zip + + Scenario: Phase 4 tag present + Tool: Bash + Steps: 1. Run: git tag v0.4.0-phase4 && git tag | grep v0.4.0-phase4 + Expected: present + Evidence: .sisyphus/evidence/task-P4.12-tag.log + ``` + + **Commit**: YES — `test(root): E2E multiplayer with reconnect; tag Phase 4 (P4.12)`; post-commit: `git tag v0.4.0-phase4` + +--- + +## Final Verification Wave (MANDATORY — after ALL implementation tasks) + +> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before marking work complete. +> **Do NOT auto-proceed after verification. Wait for user's explicit approval.** +> **Never mark F1-F4 as checked before getting user's okay.** Rejection or user feedback → fix → re-run → present again → wait for okay. + +- [ ] F1. **Plan Compliance Audit** — `oracle` + Read this plan end-to-end. For each "Must Have": verify implementation exists (read file, run command, inspect built artifact). For each "Must NOT Have": search codebase for forbidden patterns (e.g., `grep -r "as any" packages/rete/src`), reject with file:line if found. Check evidence files exist in `.sisyphus/evidence/`. Verify all 5 phase tags exist (`git tag | grep phase`). Compare deliverables against plan. + Output: `Must Have [N/N] | Must NOT Have [N/N] | Phase tags [5/5] | Tasks [N/N] | VERDICT: APPROVE/REJECT` + +- [ ] F2. **Code Quality Review** — `unspecified-high` + Run `bun run typecheck` + `bun run lint` + `bun run test:coverage` + `bun run size-limit`. Review all changed files for: `as any` / `@ts-ignore` / `@ts-expect-error`, empty catches, `console.log` in prod code, commented-out code, unused imports, `Date.now()`/`Math.random()` in engine RHS paths, raw `Set` iteration in engine hot paths. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp/obj). Audit bundle sizes against budgets (engine < 50KB min+gz, chess < 200KB min+gz). + Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail, coverage X%/Y%/Z%] | Bundle [engine Xkb / chess Ykb] | Files [N clean/N issues] | VERDICT` + +- [ ] F3. **Real Manual QA via Playwright + Scripted Clients** — `unspecified-high` (+ `playwright` skill) + Start from clean state: `rm -rf node_modules && bun install && bun run build`. Launch chess server. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: play a full FIDE game; toggle 3 presets between games; play a custom-rules game; save via localStorage; reload browser; verify state persisted; export JSON; import into fresh browser; play a multiplayer game across two browser contexts with reconnect mid-game. Test edge cases: illegal move rejected, rate-limit trip, protocol version mismatch hard-disconnect, 60s reconnect boundary. Save to `.sisyphus/evidence/final-qa/`. + Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [ ] F4. **Scope Fidelity Check** — `deep` + For each task: read "What to do", read actual diff (`git log` / `git diff` on that task's commits). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance in diff. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes. Verify commit messages follow Conventional Commits with scope (`feat(rete):`, `feat(chess):`, `feat(server):`). + Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | Commit format [N/N] | VERDICT` + +--- + +## Commit Strategy + +- **Conventional Commits** enforced: `type(scope): description` where `scope ∈ {rete, chess, server, root}` +- **Types**: `feat`, `fix`, `test`, `refactor`, `chore`, `docs`, `perf`, `build`, `ci` +- **Atomic commits**: one logical change per commit. TDD tasks commit test+impl together. +- **Every commit**: passes `bun run check` (tsc + eslint + vitest) — enforced via pre-commit hook AND CI required-status-check +- **Phase boundaries tagged**: `v0.1.0-phase1`, `v0.2.0-phase2`, `v0.3.0-phase3`, `v0.4.0-phase4`, `v1.0.0` (final) +- **No WIP commits on main**; feature work in feature branches (if branching used) or linearly via rebase on main +- **No squash-merge across phases**; each phase is a merge train + +Per-task commit details live in each TODO's `Commit:` block. + +--- + +## Success Criteria + +### Verification Commands (run from repo root) + +```bash +bun install # → 0 errors +bun run typecheck # → 0 errors +bun run lint # → 0 errors +bun run test # → all green +bun run test:coverage # → engine ≥90%, chess ≥70%, server ≥80% +bun run build # → dist/ populated in all 3 packages +bun run size-limit # → engine < 50KB, chess < 200KB +bun run playwright test # → all E2E pass +bun run scripts/replay-determinism.ts fixtures/game-*.log # → hashes match for every fixture +bun run start:server & # server up +sleep 2 +bun run test:integration # WebSocket handshake, move exchange, reconnect +kill %1 +gh run list --limit 1 --json conclusion -q '.[0].conclusion' # → "success" +git tag --list # → contains v0.1.0-phase1 … v1.0.0 +``` + +### Final Checklist +- [ ] All "Must Have" present (verified by F1) +- [ ] All "Must NOT Have" absent (verified by F1 and F2) +- [ ] All phase tags present (v0.1.0-phase1 … v1.0.0) +- [ ] Engine coverage ≥90% / chess ≥70% / server ≥80% +- [ ] Bundle sizes within budget (engine <50KB, chess <200KB) +- [ ] Playwright scenarios all green +- [ ] Server integration tests all green +- [ ] Replay-determinism hash match 100% +- [ ] CI green on latest commit +- [ ] User has given explicit approval after F1-F4 presentation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e477c8e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Paratype Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2baf09e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# @paratype + +A Doorenbos-style Rete II rules engine for TypeScript games, with an authoritative WebSocket chess demo. + +## Packages + +- [`packages/rete`](packages/rete) — Rete II engine core +- [`packages/chess`](packages/chess) — Browser chess demo (React + Vite) +- [`packages/server`](packages/server) — Authoritative Bun WebSocket server + +## Docs + +- [SPEC.md](packages/rete/SPEC.md) — Engine specification +- [PHASES.md](docs/PHASES.md) — Development phases & perf budgets +- [RULES.md](packages/chess/RULES.md) — Chess rule presets +- [PROTOCOL.md](packages/server/PROTOCOL.md) — WebSocket message protocol + +## Getting Started + +```bash +bun install && bun run check +``` diff --git a/docs/PHASES.md b/docs/PHASES.md new file mode 100644 index 0000000..89839ee --- /dev/null +++ b/docs/PHASES.md @@ -0,0 +1,194 @@ +# Project Phases + +## Phase 0 — Specification Lock +In-scope deliverables: +- `packages/rete/SPEC.md` — engine semantics (10 sections) +- `docs/PHASES.md` — this document +- `packages/chess/RULES.md` — 15 preset custom rules with compat matrix +- `packages/server/PROTOCOL.md` — WebSocket protocol v1 (8+ message types) +- Monorepo scaffold: Bun workspaces, tsconfig base, eslint flat config, vitest workspace, playwright config, lefthook pre-commit hook, GitHub Actions CI +- Three package skeletons: `packages/rete`, `packages/chess`, `packages/server` + +Acceptance gate (executable): +```bash +test -f packages/rete/SPEC.md && test -f docs/PHASES.md && test -f packages/chess/RULES.md && test -f packages/server/PROTOCOL.md +bun install && bun run check # exits 0 +gh run list --limit 1 --json conclusion -q '.[0].conclusion' # "success" +``` + +Must NOT have in this phase: any engine implementation, any chess rules, any UI, any server logic. + +## Phase 1 — Pararules Parity +In-scope deliverables: +- `packages/rete/src/schema.ts` — typed EAV schema + Fact type +- `packages/rete/src/wm.ts` — WorkingMemory with deterministic iteration +- `packages/rete/src/alpha.ts` — AlphaNetwork + AlphaMemory with inverted-index dispatch +- `packages/rete/src/session.ts` — Session lifecycle (init, add, insert, retract, fireRules) +- `packages/rete/src/builder.ts` — Typed rule builder + `v()` variable helper +- `packages/rete/src/registry.ts` — HandlerRegistry + PredicateRegistry +- `packages/rete/src/serialize.ts` — JSON round-trip serialization (RULE_SCHEMA_V1) +- `packages/rete/src/beta.ts` — BetaMemory + Token propagation +- `packages/rete/src/join.ts` — JoinNode with variable-binding equality tests +- `packages/rete/src/condition.ts` — FilterNode with registered predicates +- `packages/rete/src/query.ts` — query() / queryAll() API +- `packages/rete/src/derived.ts` — thenFinally + truth maintenance +- `packages/rete/src/cycle.ts` — RecursionLimitExceededError + recursion counter +- `packages/rete/src/conflict.ts` — deterministic activation ordering +- `packages/rete/tests/golden/` — pararules golden-file test port (5-10 tests) +- tsup build output: `packages/rete/dist/` with ESM + CJS + .d.ts + +Acceptance gate: +```bash +bun test packages/rete # all green +bun run test:coverage -- packages/rete # ≥90% line coverage +bun run check # exits 0 +git tag v0.1.0-phase1 # after gate passes +``` + +Must NOT have: negation nodes, NCC, existential, aggregation (those are Phase 2). + +## Phase 2 — Rete II + Chess Engine +In-scope deliverables: +- `packages/rete/src/negation.ts` — NegationNode (NOT) +- `packages/rete/src/existential.ts` — ExistentialNode (EXISTS) +- `packages/rete/src/ncc.ts` — NccNode (not-count-condition) +- `packages/rete/src/aggregate.ts` — AggregationNode (count/sum/collect/min/max) +- `packages/chess/src/schema.ts` — chess attribute schema (piece types, squares, colors) +- `packages/chess/src/coord.ts` — square coordinate helpers +- `packages/chess/src/starting-position.ts` — FIDE starting position fact generator +- `packages/chess/src/rules/` — all FIDE chess rules as Rete productions: + - `primitives.ts`, `pawn.ts`, `knight.ts`, `sliding.ts`, `king.ts`, `turn.ts`, `capture.ts` + - `castling.ts`, `enpassant.ts`, `promotion.ts`, `check.ts` + - `checkmate.ts`, `stalemate.ts`, `draws.ts`, `insufficient.ts` +- `packages/chess/src/pgn.ts` — minimal SAN parser (no external dep) +- `packages/chess/tests/fide-games/` — 5 classic game PGN fixtures + replay tests + +Acceptance gate: +```bash +bun test packages/rete # Rete II nodes green +bun test packages/chess/tests/fide-games # 5 games replay to completion +bun run check # exits 0 +git tag v0.2.0-phase2 +``` + +Must NOT have: time-travel, UI, localStorage, server, presets, Playwright. + +## Phase 3 — Time-Travel + Presets + UI +In-scope deliverables: +- `packages/rete/src/eventlog.ts` — append-only event log with monotonic seq +- `packages/rete/src/snapshot.ts` — Immer snapshots every N ticks +- `packages/rete/src/replay.ts` — replay engine + stateHash() determinism verifier +- `scripts/replay-determinism.ts` — CI replay verifier script +- `packages/chess/src/presets/` — all 15 preset rules with registry +- `packages/chess/src/ui/` — React + Vite chess app: + - `Board.tsx`, `Rules.tsx`, `SavePanel.tsx`, `ImportExport.tsx` +- `packages/chess/src/persist/` — autosave.ts + io.ts +- `packages/chess/e2e/full-flow.spec.ts` — end-to-end UI scenario + +Acceptance gate: +```bash +bun run playwright test packages/chess/e2e/full-flow.spec.ts # green +bun run scripts/replay-determinism.ts # all hashes match +bun run check # exits 0 +git tag v0.3.0-phase3 +``` + +Must NOT have: WebSocket server, multiplayer, networked rooms. + +## Phase 4 — Authoritative Multiplayer +In-scope deliverables: +- `packages/server/src/index.ts` — Bun HTTP+WS server +- `packages/server/src/protocol.ts` — zod message schemas +- `packages/server/src/rooms.ts` — RoomRegistry (6-char codes) +- `packages/server/src/middleware.ts` — rate limiting, origin allow-list, 64KB cap +- `packages/server/src/game-session.ts` — authoritative engine session per room +- `packages/server/src/broadcast.ts` — move validation + fact-delta broadcast +- `packages/server/src/reconnect.ts` — 60s grace reconnection +- `packages/server/src/logging.ts` — pino + Prometheus metrics +- `packages/chess/src/net/client.ts` — WS client with reconnect + seq ack +- `packages/chess/src/net/prediction.ts` — client prediction + reconciliation +- `packages/chess/src/ui/Lobby.tsx` — room create/join UI +- `packages/chess/e2e/multiplayer.spec.ts` — two-browser multiplayer scenario +- `scripts/ws-client.ts` — scripted WS client for integration tests + +Acceptance gate: +```bash +bun run playwright test packages/chess/e2e/multiplayer.spec.ts # green +bun run test:integration # WebSocket handshake + move exchange + reconnect +bun run check # exits 0 +git tag v0.4.0-phase4 +``` + +Must NOT have: spectators, AI opponent, server-side persistence across restart, mid-game rule toggle, accounts. + +## Non-Goals (v1) +The following are explicitly out of scope for v1. Any agent that adds these will be considered scope-creep: +- Chess AI (Stockfish, minimax, MCTS, opening books) +- Puzzles, tutorials, analysis mode, engine evaluation +- ELO rating, matchmaking, tournaments, leaderboards +- Social features: chat, emotes, friend lists, profiles, avatars +- Rule marketplace, remote rule repository, user-authored JS rule upload +- Mid-game rule toggle (toggle only allowed between games) +- Spectators (2-player rooms only in v1) +- Server-side game persistence across server restart +- Mobile-native clients (responsive web only, no React Native / Capacitor) +- User accounts, OAuth, email/password, session tokens, analytics +- Telemetry, crash reporting, A/B testing, feature flags +- Internationalization (i18n), accessibility beyond keyboard-playable chess +- Additional games on the engine (no "build another game as proof") +- Visual rule editor / node graph editor (preset toggle only) +- Pararules Nim macro equivalents via runtime code-gen or eval +- External Rete library dependency (greenfield) +- chess.js or any hardcoded FIDE chess library +- Stockfish.wasm or any external chess engine +- Persistent user data beyond localStorage + +## Performance Budgets +These are contractual. CI enforces bundle size. Unit tests enforce microsecond budgets. + +| Metric | Budget | Enforcement | +|--------|--------|-------------| +| `session.insert(fact)` | < 0.5ms @ 10,000 facts | Vitest benchmark | +| `session.fireRules()` | < 5ms for chess ruleset (64 rules, ~100 facts) | Vitest benchmark | +| Replay 1,000 events | < 500ms | `scripts/replay-determinism.ts` | +| Alpha dispatch 10,000 inserts | < 50ms | Alpha network benchmark test | +| 3-condition join on 100 entities | < 10ms | Join benchmark test | +| 1,000 Immer snapshots memory | < 10MB (structural sharing) | Snapshot memory test | +| Engine bundle (`@paratype/rete`) | < 50KB min+gzip | size-limit in CI | +| Chess bundle (`@paratype/chess`) | < 200KB min+gzip | size-limit in CI | +| Server broadcast p99 latency | < 50ms per delta | Server integration test | + +## Demo Scenarios +One scripted demo per phase that proves the phase deliverable works end-to-end. + +**Phase 0 Demo**: Specs and scaffold exist. +```bash +bun install && bun run check # exits 0 with zero errors +cat packages/rete/SPEC.md | grep '^## ' | wc -l # outputs 10 +cat packages/chess/RULES.md | grep '^### ' | wc -l # outputs 15 +cat packages/server/PROTOCOL.md | grep '^### Message:' | wc -l # outputs ≥8 +``` + +**Phase 1 Demo**: Run pararules golden test suite. +```bash +bun test packages/rete/tests/golden # all green +bun run test:coverage -- packages/rete # "All files | XX% | ... " with ≥90% line +``` + +**Phase 2 Demo**: Replay Kasparov vs Topalov 1999 (27 moves to spectacular win). +```bash +bun test packages/chess/tests/fide-games/kasparov-topalov-1999.test.ts +# Output: "Kasparov vs Topalov: all 27 moves legal, checkmate detected ✓" +``` + +**Phase 3 Demo**: Full UI flow. +```bash +bun run dev --filter @paratype/chess & +bun x playwright test packages/chess/e2e/full-flow.spec.ts --headed # watch in browser +``` + +**Phase 4 Demo**: Two-browser multiplayer with reconnect. +```bash +bun run start:server & +bun x playwright test packages/chess/e2e/multiplayer.spec.ts --headed # watch two windows play +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..787880f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["**/dist/**", "**/node_modules/**", "**/*.js", "**/*.mjs"], + }, + ...tseslint.configs.recommended, + { + rules: { + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + }, + // Engine RHS purity: ban impure globals in rete/src + { + files: ["packages/rete/src/**/*.ts"], + rules: { + "no-restricted-globals": [ + "error", + { name: "Date", message: "RHS purity: use session.now() instead" }, + { name: "performance", message: "RHS purity: forbidden in engine" }, + { name: "setTimeout", message: "RHS purity: forbidden in engine" }, + { name: "setInterval", message: "RHS purity: forbidden in engine" }, + { name: "clearTimeout", message: "RHS purity: forbidden in engine" }, + { name: "clearInterval", message: "RHS purity: forbidden in engine" }, + { name: "fetch", message: "RHS purity: forbidden in engine" }, + { name: "console", message: "RHS purity: use debug flag instead" }, + ], + "no-restricted-syntax": [ + "error", + { + selector: "MemberExpression[object.name='Math'][property.name='random']", + message: "RHS purity: Math.random() forbidden in engine", + }, + ], + }, + } +); diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..19ef800 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,4 @@ +pre-commit: + commands: + check: + run: bun run check diff --git a/package.json b/package.json new file mode 100644 index 0000000..b707928 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@paratype/monorepo", + "private": true, + "version": "0.0.0", + "type": "module", + "workspaces": ["packages/*"], + "scripts": { + "check": "bun run typecheck && bun run lint && bun run test", + "typecheck": "tsc -b", + "lint": "eslint .", + "test": "vitest run --passWithNoTests", + "test:coverage": "vitest run --coverage --passWithNoTests", + "build": "bun run --filter '*' build", + "size-limit": "echo 'size-limit: TODO wire after build'" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^3.0.0", + "eslint": "^9.0.0", + "lefthook": "^1.10.0", + "typescript": "^5.7.0", + "typescript-eslint": "^8.0.0", + "vitest": "^3.0.0" + } +} diff --git a/packages/chess/README.md b/packages/chess/README.md new file mode 100644 index 0000000..ffddd0f --- /dev/null +++ b/packages/chess/README.md @@ -0,0 +1 @@ +# @paratype/chess — custom-rules chess demo diff --git a/packages/chess/RULES.md b/packages/chess/RULES.md new file mode 100644 index 0000000..0ad6777 --- /dev/null +++ b/packages/chess/RULES.md @@ -0,0 +1,371 @@ +# Preset Custom Rules + +This document is the authoritative specification for the 15 preset custom chess rules shipped with the `@rules/chess` package (v1). Each rule is implementable as a set of Rete II productions that either *add to* or *override* the base FIDE productions. No rule here requires user-authored JavaScript — all behaviour must be expressible via the declarative production + WME vocabulary defined in the engine spec. + +This document drives implementation tasks **P3.4 – P3.8**. + +## Conventions + +- **ID**: kebab-case, globally unique, used as the registry key in `packages/chess/src/presets/`. +- **Mode**: + - `additive` — the rule adds *new* productions without retracting any base production. + - `override` — the rule retracts (or short-circuits via higher salience) one or more base FIDE productions and installs replacements. + - `hybrid` — both additive and override effects. +- **Requires**: other preset IDs that must also be active (dependencies form a DAG — no cycles). +- **Incompatible With**: preset IDs that produce contradictory WMEs or retract productions the other relies on. +- **HP meta-rule**: the `piece-hp` preset extends the chess schema (`packages/chess/src/presets/piece-hp.ts`) with an `hp: number` attribute on every `Piece` WME, and replaces the FIDE capture production. Any rule that reads or writes HP must declare `Requires: piece-hp`. + +## Overview + +| # | ID | Name | Category | +|---|----|------|----------| +| 1 | `pawns-move-backward` | Backward-Marching Pawns | Movement Modifier | +| 2 | `double-pawn-sprint` | Perpetual Sprint | Movement Modifier | +| 3 | `pawn-diagonal-no-capture` | Slanting Pawns | Movement Modifier | +| 4 | `knights-leap-twice` | Double-Leap Knights | Movement Modifier | +| 5 | `bishops-ignore-color` | Colour-Blind Bishops | Movement Modifier | +| 6 | `rook-warp` | Rook Warp | Board Geometry | +| 7 | `wrap-board` | Cylindrical Board | Board Geometry | +| 8 | `queen-splits` | Queen Fission | Piece Ability | +| 9 | `king-heals` | Regenerating King | Piece Ability | +| 10 | `explosive-rook` | Detonating Rook | Piece Ability | +| 11 | `capture-to-win` | First Blood | Win/Loss Condition | +| 12 | `last-piece-standing` | Annihilation | Win/Loss Condition | +| 13 | `piece-hp` | Hit Points | Persistent State | +| 14 | `knight-immunity` | Bishop-Proof Knights | Piece Ability | +| 15 | `poisoned-squares` | Poisoned Centre | Persistent State | + +## Rule Definitions + +### Backward-Marching Pawns + +**ID**: `pawns-move-backward` +**Category**: Movement Modifier +**Description**: Pawns may, in addition to their normal forward moves, move exactly one square straight backward to an empty square. Backward moves may **not** capture and do **not** enable *en passant*. A pawn that has moved backward is still eligible for promotion once it later reaches the opposite last rank. +**Base Rule Affected**: `fide.pawn-single-push` (additive — adds a new production `preset.pawn-backward-push`, does not retract the forward push). +**Mode**: additive +**Requires**: none +**Incompatible With**: `double-pawn-sprint` + +**Test Scenarios**: +1. White pawn on e4, e3 empty → `e4→e3` is a legal move. +2. White pawn on e4, enemy piece on d3 → `e4→d3` is **not** legal (no backward captures). +3. White pawn on e4, white piece on e3 → `e4→e3` is **not** legal (destination occupied). + +**Edge Cases**: +- Backward move does **not** create an *en passant* target square (the FIDE `EnPassantTarget` WME is only asserted by the double-push production). +- A pawn that moves backward to its home rank may *not* then perform a double forward sprint on the following turn (home-rank privilege is tracked by a `HasMoved` WME, which is asserted on the pawn's first move of any kind). +- Backward moves do not count toward the fifty-move rule's "pawn move" reset — they **do** reset it, same as any pawn move. + +--- + +### Perpetual Sprint + +**ID**: `double-pawn-sprint` +**Category**: Movement Modifier +**Description**: A pawn may advance two squares straight forward from *any* rank, not only its home rank, provided both intervening squares are empty. The resulting move still asserts an *en passant* target on the skipped square for exactly one opponent turn. +**Base Rule Affected**: `fide.pawn-double-push` (override — removes the `HasMoved = false` guard). +**Mode**: override +**Requires**: none +**Incompatible With**: `pawns-move-backward` + +**Test Scenarios**: +1. White pawn on e4, e5 and e6 empty → `e4→e6` is legal and asserts `EnPassantTarget(e5)`. +2. Black pawn on d5 after white plays `e4→e6` → Black `d5×e6` is **not** en-passant-legal; but `d5×e5` **is** legal en passant (standard FIDE consequence). +3. White pawn on e7, e8 is the promotion rank → `e7→e8` single push with promotion is legal; `e7→e9` is illegal (off board, not a "two-square" candidate). + +**Edge Cases**: +- The *en passant* window remains one ply, exactly as in FIDE. +- A double sprint that jumps *over* a piece is illegal — both intervening squares must be empty (the slide-check production is retained). +- This rule is incompatible with `pawns-move-backward` because the two-square move is ambiguous when combined with an unrestricted backward step (e.g. e4→e2 vs e4→e6 would both be legal, breaking the pawn's identity as "forward-only"). + +--- + +### Slanting Pawns + +**ID**: `pawn-diagonal-no-capture` +**Category**: Movement Modifier +**Description**: Pawns may move one square diagonally forward to an *empty* square without capturing, in addition to all normal pawn moves. Diagonal moves onto occupied enemy squares still capture exactly as in FIDE; the new behaviour is *only* the non-capturing diagonal slide. +**Base Rule Affected**: `fide.pawn-capture` (additive — adds a new production `preset.pawn-diagonal-quiet`). +**Mode**: additive +**Requires**: none +**Incompatible With**: none + +**Test Scenarios**: +1. White pawn on e4, d5 and f5 empty → both `e4→d5` and `e4→f5` are legal quiet moves. +2. White pawn on e4, d5 occupied by a black piece → `e4→d5` is a legal capture (normal FIDE behaviour, unchanged). +3. White pawn on e4, d5 occupied by a *white* piece → `e4→d5` is illegal (cannot move onto a friendly piece — neither quiet nor capture). + +**Edge Cases**: +- A quiet diagonal move does **not** create an *en passant* target. +- Diagonal quiet moves may still deliver check / checkmate and may still promote if they land on the last rank. +- *En passant* itself is unchanged: a black pawn on d5 may still capture white's e-pawn en passant after `e2→e4`, even though the diagonal-quiet variant offers `e5→d6` or `e5→f6` as non-capturing alternatives. + +--- + +### Double-Leap Knights + +**ID**: `knights-leap-twice` +**Category**: Movement Modifier +**Description**: A knight makes two consecutive L-shaped leaps in a single turn. The knight must land on a square reachable by the composition of two knight moves, and the *intermediate* square must be empty (it is "flown over" but the knight briefly stops there for legality purposes). The knight may optionally stop after the first leap — any single-leap destination is still legal. +**Base Rule Affected**: `fide.knight-move` (hybrid — retains the single-leap production and adds `preset.knight-double-leap`). +**Mode**: hybrid +**Requires**: none +**Incompatible With**: none + +**Test Scenarios**: +1. White knight on b1, all squares empty → `b1→a3→c4` is legal (lands on c4); `b1→c4` alone is **not** a legal single-leap destination, so the double-leap is required. +2. White knight on b1, a3 occupied by a friendly pawn → `b1→?→c4` via a3 is illegal (intermediate square must be empty); other two-leap paths to the same final square (e.g. `b1→c3→a4`) must be evaluated independently. +3. White knight on b1, enemy piece on c3 → `b1→c3` single-leap capture is legal; `b1→c3→?` two-leap is illegal because the intermediate square must be empty, not occupied by an enemy. + +**Edge Cases**: +- The intermediate square is **not** captured: a knight cannot "capture on the way through". Only the final landing square's occupant (if enemy) is captured. +- Double-leap may deliver check from squares ordinary knight moves cannot reach; the checkmate-detection production uses the full two-leap move generator when this rule is active. +- Double-leap does **not** allow the knight to end on its starting square (a→b→a paths are pruned as null moves). + +--- + +### Colour-Blind Bishops + +**ID**: `bishops-ignore-color` +**Category**: Movement Modifier +**Description**: A bishop may, in addition to its normal diagonal slides, make a single one-square orthogonal step (N/S/E/W) to an empty or enemy-occupied square. This lets a bishop change diagonal colour over multiple turns and effectively reach every square on the board. +**Base Rule Affected**: `fide.bishop-move` (additive — adds `preset.bishop-orthogonal-step`). +**Mode**: additive +**Requires**: none +**Incompatible With**: none + +**Test Scenarios**: +1. White bishop on c1, d1 empty → `c1→d1` is a legal one-square orthogonal step. +2. White bishop on c1, b1 occupied by white knight → `c1→b1` is illegal (friendly blocker). +3. White bishop on c1, b2 occupied by an enemy pawn → `c1×b2` is legal as the normal diagonal capture; `c1→c2` is legal as a quiet orthogonal step if c2 is empty. + +**Edge Cases**: +- The orthogonal step is exactly one square — bishops do **not** gain full rook mobility. A bishop on c1 cannot reach c3 in one move unless via its native diagonal. +- Orthogonal steps count as bishop moves for the fifty-move rule (not pawn moves, not captures unless capturing). +- A bishop that steps orthogonally onto a square of the opposite colour is now permanently on the new colour until it steps again — the schema does not tag bishops by colour. + +--- + +### Rook Warp + +**ID**: `rook-warp` +**Category**: Board Geometry +**Description**: After a rook completes a normal slide, it may optionally "warp" — that is, continue the slide as if the far edge of the board wrapped to the near edge, landing on the first square of the opposite end of the same rank or file that is empty or contains an enemy piece. The warp is a single atomic move, chosen at move-generation time. +**Base Rule Affected**: `fide.rook-move` (hybrid — retains base slide and adds `preset.rook-warp-slide`). +**Mode**: hybrid +**Requires**: none +**Incompatible With**: `wrap-board` + +**Test Scenarios**: +1. White rook on a1, entire 1st rank empty → `a1→h1` is legal normal slide; `a1⇒h1` via warp is also legal but produces the same destination, so only the normal move is listed. +2. White rook on a4, all squares a5..a8 empty, a1..a3 empty, h4 occupied by enemy → slide `a4→a8` is legal normal; warp `a4⇒h4` is a legal capture (the slide from a4 up to a8, then wrapping down from a1 to h4). +3. White rook on d4, d5..d8 empty, d1..d3 empty, but d1 becomes the warp continuation point occupied by a friendly piece → warp is blocked at the friendly piece, so the warp destination is the last empty square before it. +4. White rook on d4 with enemy on d8, friendly on d1 → normal slide captures d8; warp path (d5..d8..wraps..d1..d3) is blocked immediately at d1 after wrapping around, so no warp capture beyond d8 is possible. + +**Edge Cases**: +- Castling is unchanged. A rook that has warped once still has `HasMoved = true` and so cannot castle. +- A rook cannot warp *through* its own king; the warp path is interrupted by any friendly piece and by any enemy piece (capturing the first enemy encountered). +- Warp and `wrap-board` are incompatible because `wrap-board` makes *every* piece's lateral movement wrap, which collapses the rook-specific warp semantics into ambiguity about when a move is a "rook warp" vs an ordinary wrap-slide. + +--- + +### Cylindrical Board + +**ID**: `wrap-board` +**Category**: Board Geometry +**Description**: The board is topologically a vertical cylinder: the a-file and h-file are adjacent. A piece whose lateral movement would leave the board on the a-side re-enters on the h-file at the same rank, and vice versa. Vertical movement (ranks 1 and 8) does **not** wrap. +**Base Rule Affected**: All sliding-piece productions (`fide.rook-move`, `fide.bishop-move`, `fide.queen-move`) and `fide.king-move`, `fide.knight-move`, `fide.pawn-capture` — each is overridden with a wrap-aware successor generator. +**Mode**: override +**Requires**: none +**Incompatible With**: `rook-warp` + +**Test Scenarios**: +1. White rook on a4, all squares empty → `a4→h4` is legal via wrap (westward one step); `a4→b4, c4, …` also legal normally. +2. White bishop on a1, diagonal empty → `a1→h2` is legal (one diagonal step westward wraps to h2). +3. White pawn on a5 (black to move with a black pawn on h5) → Black pawn on h5 *may* capture `h5×a6` diagonally through the wrap? **No** — pawn captures wrap: `h5×a6` is legal only if an enemy piece is on a6 (normal capture rule applied modulo wrap). If the white piece is on a6, the capture is legal. + +**Edge Cases**: +- Castling: the king's two-square hop does not wrap — a king on e1 cannot castle "around the board" to d1 via wrap. Castling targets remain fixed squares (g1/c1 for White). +- Check detection: a rook on a4 attacks h4 through the wrap; the king-safety production must consider wrap-attacks, so `wrap-board` overrides check detection too. +- A pawn's *en passant* target square is computed modulo wrap — if a black pawn double-sprints from h7 to h5, it can be captured en passant by a white pawn on a5 (moving `a5×h6`) because the wrap makes a5 and h5 laterally adjacent. + +--- + +### Queen Fission + +**ID**: `queen-splits` +**Category**: Piece Ability +**Description**: When a queen captures an enemy piece, after the capture resolves the queen is retracted and replaced by a rook and a bishop of the same colour. The rook is placed on the capture square; the bishop is placed on the nearest empty orthogonally- or diagonally-adjacent square (searched in clockwise order N, NE, E, SE, S, SW, W, NW). If no adjacent square is empty, the bishop is forfeit and only the rook remains. +**Base Rule Affected**: `fide.queen-capture` (override — the queen is not placed on the target square; instead the fission production fires). +**Mode**: override +**Requires**: none +**Incompatible With**: none + +**Test Scenarios**: +1. White queen on d1, black pawn on d4, d3/d5/c4/e4/c3/e3/c5/e5 all empty → queen captures d4; result: white rook on d4, white bishop on d5 (N is first clockwise empty). +2. White queen on d1 captures black rook on d4; every adjacent square of d4 is occupied by friendly pieces → result: white rook on d4, bishop forfeit (not placed). +3. White queen on h1 captures black pawn on h2 (corner capture); adjacency for h2 is only g1, g2, g3, h1, h3 → bishop is placed on the first empty in clockwise order starting N from h2 (i.e. h3 if empty). +4. White queen gives checkmate by capturing → fission still resolves; if removing the queen and placing a rook on the square no longer delivers checkmate, the game continues (fission is part of the move and is evaluated before legality of the resulting position). + +**Edge Cases**: +- If the rook placed on the capture square is pinned (i.e. removing the queen leaves the king in check), the queen's capture was illegal to begin with — the production must back-check using the post-fission board, not the mid-capture board. +- Promotion interaction: a pawn that promotes to a queen and immediately captures (promotion-capture) fissions on the same move: the promotion square receives a rook and the adjacent square receives a bishop. +- Fissioned pieces do **not** retain castling rights; they are newly-materialised. + +--- + +### Regenerating King + +**ID**: `king-heals` +**Category**: Piece Ability +**Description**: At the end of a player's turn, if that player's king is **not** in check, the king regenerates 1 HP, up to a maximum of 3. The king starts the game with 1 HP. The king can only be eliminated when its HP reaches 0; a capture (or damage from other rules) decrements HP by 1. +**Base Rule Affected**: `fide.check-resolution` and `fide.king-capture` (hybrid — adds a new end-of-turn production and modifies capture semantics so that the king is a multi-HP piece). +**Mode**: hybrid +**Requires**: `piece-hp` +**Incompatible With**: none + +**Test Scenarios**: +1. White king starts the game with HP=1. White plays a turn, ends not in check → at the end of White's turn, king HP becomes 2. +2. White king at HP=3, White ends turn not in check → king HP stays at 3 (cap). +3. White king at HP=2, Black delivers a legal "capture" on the king (only possible because `piece-hp` is active and king cannot be checkmated in the FIDE sense) → king HP becomes 1, game continues. White's next turn ends not in check → HP becomes 2. + +**Edge Cases**: +- Being "in check" means *at the end of the turn*, not during it. A king briefly in check mid-move (impossible under FIDE but possible if combined with rules that allow illegal-intermediate positions) does not prevent healing. +- Healing fires *once* per turn-end, regardless of how many pieces threaten the king. If the king is in check the heal production is simply not eligible. +- `king-heals` does **not** disable checkmate detection on its own — checkmate still ends the game when the king has HP=1 and cannot escape; this rule merely gives the king additional hits. + +--- + +### Detonating Rook + +**ID**: `explosive-rook` +**Category**: Piece Ability +**Description**: When a rook makes a capture, it *also* removes every other piece (friendly or enemy) on the same rank or same file within a Chebyshev-style distance of 2 squares from the capture square, measured along rank and file only. The capturing rook itself survives and remains on the capture square. +**Base Rule Affected**: `fide.rook-capture` (override — the post-capture production asserts additional retracts). +**Mode**: override +**Requires**: none +**Incompatible With**: `piece-hp` + +**Test Scenarios**: +1. White rook captures on d4; black pieces on d3, d5, d6, c4, e4, f4, d2, d7 → after detonation, removed: d3, d5, d6 (d6 is distance 2), c4, e4, f4 (f4 is distance 2), d2 (distance 2). d7 survives (distance 3). +2. White rook captures on d4; own king on d5 → own king is blown up. This is legal (self-detonation is permitted); however if it is the *only* king the game ends with that side losing. +3. White rook captures on a1 corner; pieces on a2, a3, b1, c1 → all four are in range (distances 1, 2, 1, 2) and are all removed; a4 and d1 (distance 3) survive. + +**Edge Cases**: +- Detonation does **not** chain: if a captured piece is itself a rook, its capture does not re-trigger. Only the original capturing rook detonates. +- A rook that captures en passant — impossible in FIDE (only pawns do), and not introduced by any other preset — is not a special case. +- Incompatibility with `piece-hp`: when HP is active, captures are non-lethal damage; the detonation semantics ("removes pieces") conflict with "deals 1 HP damage". Rather than redefining, the v1 preset set treats these two as mutually exclusive. + +--- + +### First Blood + +**ID**: `capture-to-win` +**Category**: Win/Loss Condition +**Description**: The first player to make a capture — of *any* enemy piece, including pawns — wins the game immediately. Checkmate is disabled; stalemate still results in a draw if no capture is ever available. +**Base Rule Affected**: `fide.checkmate-win`, `fide.stalemate-draw` (override — disables checkmate, retains stalemate; adds `preset.first-capture-win`). +**Mode**: override +**Requires**: none +**Incompatible With**: `last-piece-standing` + +**Test Scenarios**: +1. Starting position, White plays `1.e4 d5 2.exd5` → White wins immediately on move 2 (first capture). +2. Game reaches a position with no legal captures for either side and the side to move has no other legal moves → stalemate, draw. +3. White is in check; the only legal response is a capture of the checking piece → White plays the capture and wins (check is not a loss condition under this rule — only capture is a win condition; if White cannot respond, stalemate-by-no-moves rules determine the draw). + +**Edge Cases**: +- The king is not special: capturing a pawn wins just as capturing a queen does. Players therefore play extremely cautiously, avoiding any offer of exchange. +- Promotions do not count as captures unless they *are* capture-promotions (a pawn capturing diagonally onto the last rank). +- *En passant* counts as a capture. +- A move that would give up the player's own piece to be captured by the opponent is not itself a capture — only the capturing move triggers the win. + +--- + +### Annihilation + +**ID**: `last-piece-standing` +**Category**: Win/Loss Condition +**Description**: A player wins when the opponent has **zero** remaining pieces. The king has no special status: it may be captured like any other piece, and is not subject to check or checkmate. Stalemate is replaced by the losing condition "no legal moves" = loss. +**Base Rule Affected**: `fide.check-detection`, `fide.checkmate-win`, `fide.stalemate-draw`, `fide.king-capture-illegal` (override — all four are retracted and replaced by a single annihilation-win production plus a "no legal moves = loss" production). +**Mode**: override +**Requires**: none +**Incompatible With**: `capture-to-win` + +**Test Scenarios**: +1. White has only a king on e1; Black captures it with a queen → Black wins. No "check" warning fires in any prior position. +2. White has king + rook vs Black king. White sacrifices the rook to force Black's king into a position with no legal moves → Black loses by "no legal moves" rule. +3. White has king only; Black has king only → the game is drawn by the threefold-repetition or fifty-move rule eventually; no "insufficient material" automatic draw because kings are normal pieces here. + +**Edge Cases**: +- A player may legally move *into* a "check" — there is no such thing as check. This interacts with pinning: pins do not exist either, since they rely on king-safety semantics. +- A side with a king and one other piece that cannot legally move loses. This is different from FIDE stalemate (draw). +- Incompatible with `capture-to-win` because the two rules trigger on opposite conditions (first capture vs last piece) and cannot both be active. + +--- + +### Hit Points + +**ID**: `piece-hp` +**Category**: Persistent State +**Description**: Every piece has an integer `hp` attribute. All non-king pieces start at `hp = 2`; the king starts at `hp = 1` (this can be modified by `king-heals`). When a piece is "captured" by another piece's move, the attacker's move resolves as in FIDE (the attacker ends on the target square), but instead of retracting the target, the target's `hp` is decremented by 1. Only when `hp` reaches 0 is the target retracted. If after decrement the target still has `hp > 0`, the attacker and defender "stack" on the same square for resolution purposes — in practice this means the defender is *pushed* to the nearest empty adjacent square (clockwise from N), or retracted if no adjacent empty square exists. +**Base Rule Affected**: `fide.capture` (override — replaces all capture productions with `preset.hp-damage`) and the schema itself (adds the `hp` attribute to the `Piece` WME). +**Mode**: override +**Requires**: none +**Incompatible With**: `explosive-rook` + +**Test Scenarios**: +1. White rook captures black pawn (pawn hp=2) → pawn hp becomes 1, white rook ends on target square, black pawn is pushed to the clockwise-nearest empty adjacent square (e.g. N first). +2. White rook captures black pawn already at hp=1 → pawn hp becomes 0, pawn retracted, white rook ends on target square (standard FIDE appearance). +3. White queen captures black knight (hp=2) but every adjacent square to the target is occupied → knight has no push destination and is retracted instead (damage-over-push fallback). + +**Edge Cases**: +- *En passant* capture damages the captured pawn on the pawn's actual square (not the en-passant target square). The captured pawn is pushed from its original square if it survives. +- Promotion: a promoting pawn that captures deals 1 damage as normal. The pawn promotes on the target square regardless of whether the target survives. +- This rule is incompatible with `explosive-rook` (see that rule's notes). +- The schema extension (`hp: number`) is defined in `packages/chess/src/presets/piece-hp.ts` and is the single source of truth for HP-related attribute writes; all dependent rules (`king-heals`, `poisoned-squares`) read/write through this extension. + +--- + +### Bishop-Proof Knights + +**ID**: `knight-immunity` +**Category**: Piece Ability +**Description**: A bishop may never capture a knight. Any bishop move that would land on a square occupied by an enemy knight is illegal. The bishop may still pass threats through (for check/pin purposes a bishop still "attacks" the square), but it cannot complete a capture on it. +**Base Rule Affected**: `fide.bishop-capture` (override — adds a guard: target must not be `Piece(type=Knight, color=opponent)`). +**Mode**: override +**Requires**: none +**Incompatible With**: none + +**Test Scenarios**: +1. White bishop on c1, black knight on h6 along the diagonal, all intermediate empty → `c1×h6` is illegal; bishop may still move to any empty square along the diagonal up to but not including h6 (i.e., ending on g5 is legal, h6 is not). +2. Black bishop gives check to white king via a diagonal that passes through no knight → check is legal and must be resolved normally. +3. Position where the *only* way to block a bishop-check is to capture the bishop with a knight → this is still legal; knight immunity protects knights from bishops, not bishops from knights. + +**Edge Cases**: +- For check and checkmate detection purposes, a bishop still *threatens* the knight's square (so a knight cannot "shield" its king from a bishop's ray — the bishop's attack passes through the knight as if it weren't there for threat-detection, but the actual capture move is illegal). Implementation: the bishop's threat-ray is computed ignoring immune pieces; the bishop's move-generation excludes them. +- Promotion to bishop: a pawn that promotes to a bishop is subject to the same restriction — it cannot capture knights on its promotion move if the promotion is a capture onto a knight. +- This rule does **not** restrict queens (which are not bishops) from capturing knights, even along diagonals. + +--- + +### Poisoned Centre + +**ID**: `poisoned-squares` +**Category**: Persistent State +**Description**: The four central squares — d4, d5, e4, e5 — are permanently poisoned. Any piece that ends its owner's turn on a poisoned square loses 1 HP. A piece that occupies and then leaves a poisoned square on the same turn is unaffected. Pieces with 0 HP are retracted at end-of-turn evaluation. +**Base Rule Affected**: End-of-turn phase (additive — adds `preset.poison-damage` production that fires in the post-move resolution phase before turn-pass). +**Mode**: additive +**Requires**: `piece-hp` +**Incompatible With**: none + +**Test Scenarios**: +1. White rook (hp=2) ends the turn on e4 → rook hp becomes 1 at end-of-turn. +2. White knight (hp=1) ends the turn on d5 → knight hp becomes 0 and is retracted at end-of-turn. +3. White rook on e4 moves to e8 during its turn → no poison damage (rook did not *end* its turn on a poisoned square). + +**Edge Cases**: +- The king (hp=1 or higher if `king-heals` is active) takes poison damage too. This can create "king on d4 = dies" positions; combined with `king-heals`, a king on a poisoned square at end-of-turn takes 1 damage *and* heals 1 only if not in check — net zero if not in check, net −1 if in check. +- If a pawn on d5 promotes and the promoted piece ends on d5, the promoted piece suffers poison (promotion does not grant immunity). +- Poison fires exactly once per turn per occupant: a piece cannot take 2 damage for being on a poisoned square for two half-moves, because end-of-turn evaluation is per-player-turn. +- Order of resolution: poison damage resolves *before* `king-heals`, so a king that enters a poisoned square while in check will lose HP from poison and not heal (since it is in check), and may be retracted if hp reaches 0. diff --git a/packages/chess/package.json b/packages/chess/package.json new file mode 100644 index 0000000..68159a2 --- /dev/null +++ b/packages/chess/package.json @@ -0,0 +1,21 @@ +{ + "name": "@paratype/chess", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paratype/rete": "workspace:*" + }, + "devDependencies": { + "vite": "^6.0.0", + "@vitejs/plugin-react": "^4.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } +} diff --git a/packages/chess/src/index.ts b/packages/chess/src/index.ts new file mode 100644 index 0000000..ad2049b --- /dev/null +++ b/packages/chess/src/index.ts @@ -0,0 +1,2 @@ +// @paratype/chess — browser chess game +export {}; diff --git a/packages/chess/tsconfig.json b/packages/chess/tsconfig.json new file mode 100644 index 0000000..b01e4f4 --- /dev/null +++ b/packages/chess/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*", "e2e/**/*"], + "references": [{ "path": "../rete" }] +} diff --git a/packages/chess/vitest.config.ts b/packages/chess/vitest.config.ts new file mode 100644 index 0000000..9a44e84 --- /dev/null +++ b/packages/chess/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "chess", + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + environment: "happy-dom", + }, +}); diff --git a/packages/rete/README.md b/packages/rete/README.md new file mode 100644 index 0000000..697a2c7 --- /dev/null +++ b/packages/rete/README.md @@ -0,0 +1 @@ +# @paratype/rete — Rete II rules engine for TypeScript games diff --git a/packages/rete/SPEC.md b/packages/rete/SPEC.md new file mode 100644 index 0000000..afdebf1 --- /dev/null +++ b/packages/rete/SPEC.md @@ -0,0 +1,208 @@ +# `@paratype/rete` — Engine Semantics Specification + +This document is the canonical, normative specification of `@paratype/rete`, a Doorenbos-style Rete II rules engine implemented in TypeScript. All implementation, tests, and downstream consumers MUST conform to the semantics defined here. Where the specification disagrees with the implementation, the specification wins and the implementation is a bug. + +This is a Phase 0 spec lock. Subsequent phases (engine implementation, chess domain, multiplayer server) depend on the contracts established here. + +## Fact Model + +Working memory is a strict EAV (Entity-Attribute-Value) store. Every fact is a triple of the form `(id: EntityId, attr: AttrKey, value: AttrValue)`, where: + +- `id` is a branded numeric entity identifier (see ID Authority). +- `attr` is a string key drawn from a schema-declared set of attribute names. +- `value` is the schema-declared TypeScript type for that attribute. + +Each `(id, attr)` pair stores exactly one value. Inserting a fact for an `(id, attr)` pair that already has a value is an **update**: the previous value is replaced, and the engine treats this as a retract-then-insert for the purposes of match invalidation and re-firing (see Match Refraction). + +Facts are typed: each `attr` key maps to a specific TypeScript type declared in a session-level schema `S`. The schema is the single source of truth for attribute typing and is consumed by both the public API surface (for type inference at insert/query call sites) and by JSON rule validation (see JSON Rule Schema). + +Internally, working memory is stored as `Map>`. The outer map is keyed by entity id, the inner map is keyed by attribute name, and the value is the schema-typed payload (erased to `unknown` at the storage layer and recovered via the schema at API boundaries). The canonical fact shape exposed by the public API is `{ id: EntityId, attr: AttrKey, value: T }`, where `T` is the schema-declared type for that attr. + +This shape is the only fact shape the engine recognises. Composite shapes (objects, tuples) are not facts; they must be decomposed into multiple EAV triples sharing the same `id` before insertion. + +## ID Authority + +`EntityId` is a branded numeric type: + +``` +type EntityId = number & { readonly __brand: 'EntityId' } +``` + +The brand exists at the type level only; at runtime an `EntityId` is a plain `number`. The brand prevents accidental interchange between `EntityId` and ordinary `number` values at compile time. + +In a standalone session (no network), IDs are minted by the Session itself via an auto-incrementing counter exposed as `session.nextId(): EntityId`. The counter starts at 1 and increments by 1 on each call. Each call returns a unique, never-reused id for the lifetime of the session. + +In the multiplayer server context, ID minting is reserved exclusively to the server. Clients MUST NOT call `session.nextId()` directly. Instead, clients send intents that reference positions, squares, or other domain coordinates, and the server translates these intents into fact mutations using server-minted ids. The client-side session receives facts with server-assigned ids and references them opaquely thereafter. + +This split guarantees network-deterministic replay: every replay of the same event log on every peer produces the same id assignments and therefore the same working-memory contents and the same rule firing order. + +Derived facts produced by the engine itself (see Truth Maintenance) use a separate negative-id space and are minted by the engine, not by the user or the server. + +## Conflict Resolution + +When multiple rule activations are pending simultaneously inside a single `fireRules()` call, they are ordered deterministically before any RHS executes. The ordering is a pure function over the activation set and is identical on every machine and every run. + +The ordering keys, in priority order: + +1. **Salience descending.** Each rule has an integer `salience` (default `0`). Activations whose rule has higher salience fire first. +2. **Specificity descending.** Among activations of equal salience, those whose rule has more conditions (longer `what` / `conditions` array) fire first. Specificity is the cardinality of the rule's condition list at registration time. +3. **Insertion order ascending.** Among activations of equal salience and specificity, those whose rule was added to the session earlier (lower `addedAt` index assigned at registration time) fire first. + +There is no randomness. There is no lexicographic ordering on rule names, fact ids, or any other identifier. The only tie-breaker beyond the three keys above is the order in which rules were added to the session, which is itself deterministic given a deterministic registration order. + +The conflict resolution function is invoked once per `fireRules()` iteration: pending activations are collected, sorted by `(−salience, −specificity, +addedAt)`, and fired in that order. New activations produced by an RHS are added to the next iteration's pending set; they do not interleave with the current iteration's already-sorted firing order. + +Tests MUST verify this exact ordering by constructing scenarios with deliberate ties at each level. + +## Match Refraction + +Each unique match fires its rule's RHS exactly once. A "unique match" is identified by the tuple of **fact identities** — not values — bound to the rule's variables at the time of activation. Concretely, a match is keyed by the ordered tuple of `EntityId` values bound to the rule's pattern variables, in the order those variables appear in the rule's condition list. + +A match re-fires only when one of the following occurs: + +- A fact whose value is bound to one of the rule's variables changes. Updating an `(id, attr)` pair is treated as a retract followed by an insert with the same `(id, attr)`; this invalidates the previous match (removing it from the fired-set) and creates a new match (which is then eligible to fire). +- A previously absent matching fact is inserted, producing a new match for which no entry exists in the fired-set. + +This is CLIPS-style refraction. The engine maintains a `Set`, where `MatchKey` is a stable serialisation of the bound entity ids in the canonical variable order for that rule. Before invoking an RHS, the engine checks whether the candidate match's key is already in the fired-set; if so, the activation is suppressed. + +Retracting a fact removes every match that depended on that fact from the fired-set, so that if the same pattern later becomes true again the rule re-fires. This is what makes truth maintenance and re-derivation correct (see Truth Maintenance). + +The fired-set is per-rule. Two different rules matching the same fact tuple have independent fired-set entries. + +## Iteration Order + +All iteration over working-memory facts in hot paths — alpha dispatch, beta join, query result assembly — uses sorted arrays with documented, deterministic sort keys. Iteration over a raw `Set`, `Map`, or any unordered structure is forbidden in any code path that affects rule firing order or query result order. Insertion-order iteration of `Set` and `Map` is also forbidden in these paths because it makes firing order depend on insertion history rather than on fact identity, defeating replay determinism. + +The canonical sort keys, in priority order: + +1. `id` ascending (numeric comparison on the underlying `number`). +2. `attr` ascending (string lexicographic, by JavaScript default string comparison). + +Any collection of facts returned by `queryAll()` or by the session-level introspection method `session.allFacts()` is sorted by `[id, attr]` before being returned to the caller. Internal beta-memory tokens — partial matches consisting of multiple bound facts — are sorted by their constituent fact ids in the variable order defined by the rule's condition list, so that any deterministic enumeration of tokens produces the same sequence on every run. + +Code review and the `no-impure-rhs` lint family (extended with iteration-order checks) MUST flag any use of `Set` or `Map` iteration in firing-order-sensitive paths. + +## Truth Maintenance + +Derived facts — facts produced as the conclusion of a `thenFinally`-style production rather than asserted by the user — are logically dependent on the matches that produced them. The engine maintains this dependency explicitly: each derived fact stores the `Set` of matches that currently support it. + +When any supporting match is removed from the fired-set (because a source fact was retracted, or because a condition no longer holds after an update), the derived fact's support set shrinks. When the support set becomes empty, the engine automatically retracts the derived fact. This retraction propagates: if other rules matched against the now-retracted derived fact, their matches are also invalidated, and any further derived facts they supported are likewise re-evaluated. + +Derived facts use **negative** `EntityId` values, minted by the engine as `-(counter)` from a counter independent of the user-facing positive-id counter. This separation guarantees that derived ids cannot collide with user-asserted ids and makes derived facts trivially identifiable in logs and diagnostics. + +Derived facts are excluded from the serialised event log. Replay re-derives them from the user-asserted facts and the rule set, which preserves both correctness (no stale derivations) and log compactness. + +A `thenFinally` production whose match becomes true again after a previous derived fact was retracted will re-derive the fact (with a new negative id), because match refraction's invalidation rule applies to the derived production's match in the same way as for any other rule. + +## Cycle Detection + +`session.fireRules(opts?: { recursionLimit?: number })` accepts a configurable recursion limit. The default limit is **64**. + +The engine tracks "depth" as the number of times `fireRules` has recursively triggered itself. Recursion occurs in two situations: + +- In `autoFire: true` mode, when a rule's RHS calls `session.insert()` or `session.retract()`, the engine immediately re-enters `fireRules` to propagate the resulting activations. +- In any mode, when a rule's RHS explicitly calls `session.fireRules()`. + +When depth exceeds `recursionLimit`, the engine throws `RecursionLimitExceededError`. The error contains: + +- `message`: a human-readable description naming the limit and the depth reached. +- `depth`: the integer depth at which the limit was breached. +- `activationTrace`: an array of the last N rule names that fired, in order, where N is at most 10. This trace is the most recent suffix of the firing history and is intended for diagnosing the cycle. + +Setting `recursionLimit: 0` disables the limit entirely. This is a deliberate escape hatch for advanced users who need unbounded fixpoint computations; it is dangerous because infinite loops will hang the host thread, and its use is a smell that warrants review. + +In `autoFire: false` mode, the depth counter resets to zero at the start of each explicit `fireRules()` call, so successive top-level `fireRules()` invocations do not accumulate depth across calls. Within a single `fireRules()` call, depth accumulates across all recursive re-entries until the call returns. + +## RHS Purity Contract + +A Rule Right-Hand Side (RHS) handler — the function registered via `HandlerRegistry.register(name, fn)` and referenced by name from the JSON rule schema — MUST be pure with respect to the engine's notion of determinism. Specifically, an RHS MUST NOT call any of the following: + +- `Date.now()`, `new Date()`, `performance.now()`, or any other source of wall-clock or monotonic time. +- `Math.random()`, or any other source of non-deterministic randomness. +- `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, or any other timer API. +- `fetch`, `XMLHttpRequest`, `WebSocket`, or any other network I/O. +- `console.log`, `console.warn`, `console.error`, `console.info`, `console.debug`, or any other console API. +- Any DOM API, including `document`, `window`, and event-listener registration. + +The only session mutations an RHS is permitted to perform are: `session.insert()`, `session.retract()`, and `session.nextId()` (the last only in standalone, non-multiplayer contexts). + +Enforcement is layered: + +1. **ESLint.** A custom rule `no-impure-rhs`, defined in `packages/rete/eslint-rules/`, applies to files matching `packages/rete/src/**/rhs/**` and `packages/chess/src/**/handlers/**`. It bans the prohibited globals via `no-restricted-globals` and forbids importing time, random, timer, network, console, and DOM modules. Lint failures block CI. +2. **Dev-mode runtime guard.** When `NODE_ENV !== 'production'`, the engine wraps each RHS invocation in a scope where `Date`, `Math.random`, `setTimeout`, `setInterval`, `fetch`, and `console` are replaced with stubs that throw on access. This catches violations the lint rule misses, including indirect calls through helper functions or third-party libraries pulled into RHS modules. + +The combination — static lint plus dynamic guard — gives high confidence that no RHS observed in production was permitted to be impure during development or CI. + +Time, randomness, and I/O belong outside the engine: time is provided by the host as a fact (`(world, tick, n)`), randomness is provided as pre-rolled facts produced by a seeded RNG outside the rule firing path, and network/console output is the responsibility of the calling layer. + +## JSON Rule Schema + +Rules are serialisable to and from JSON using a handler-registry pattern. There is no function-to-string conversion, no `eval`, no `new Function`, and no arbitrary JavaScript embedded in JSON. All executable behaviour — predicates and RHS handlers — is referenced by name and resolved against a registry at deserialisation time. + +The v1 JSON rule schema: + +``` +{ + "name": "string (unique rule name within the session)", + "salience": "number (optional, default 0)", + "conditions": [ + { + "type": "alpha | negation | existential | ncc | aggregation", + "id": "string | number | null (null = wildcard)", + "attr": "string (attribute key)", + "binding": "string (variable name to bind value to, or null)", + "idBinding": "string (variable name to bind entity id to, or null)" + } + ], + "filters": [ + { + "predicate": "string (registered predicate name)", + "args": ["JsonValue (static arguments)"] + } + ], + "handler": "string (registered handler name)", + "handlerArgs": ["JsonValue (static arguments passed to handler alongside match)"] +} +``` + +Field semantics: + +- `name` MUST be unique within the session; duplicate registration is an error. +- `salience` is consumed by Conflict Resolution; omission is equivalent to `0`. +- `conditions[].type` selects the Rete II node type that handles the condition; `alpha` is the ordinary positive condition, the others correspond to the Phase 2 node types listed in Rete II Reference Target. +- `conditions[].id` is either a literal entity id (number), a variable reference (string starting with `?`), or `null` for wildcard. +- `conditions[].binding` and `conditions[].idBinding` declare variables introduced by this condition; downstream conditions and filters refer to them by name. +- `filters[].predicate` MUST be a name registered in the session's predicate registry; the predicate receives the bound variable values plus `args` and returns a boolean. +- `handler` MUST be a name registered in the session's `HandlerRegistry`; the handler receives the bound match plus `handlerArgs` and may perform the permitted session mutations subject to RHS Purity Contract. + +Deserialisation validates every registry reference. If `handler` names a function not present in the `HandlerRegistry`, the engine throws `UnknownHandlerError`. If any `filters[].predicate` names a function not present in the predicate registry, the engine throws `UnknownPredicateError`. Both errors include the offending name and the rule name in their message. + +The schema is exported as `RULE_SCHEMA_V1`, a Zod schema, for runtime structural validation prior to registry resolution. Schema-level errors (missing fields, wrong types, unknown `type` values) are reported with Zod's standard issue paths. + +This handler-registry design makes rules safely portable across processes, persistable to disk, and shippable over the network without ever transmitting executable code. + +## Rete II Reference Target + +The canonical reference for the Rete II algorithm implemented by this engine is the **Doorenbos** PhD thesis: + +> Doorenbos, R. B. (1995). *Production Matching for Large Learning Systems*. PhD Thesis, Carnegie Mellon University. CMU-CS-95-113. + +All node types, memory structures, and algorithmic decisions in this engine trace back to that thesis. Where this specification diverges (notably in Conflict Resolution, ID Authority, and the JSON serialisation surface), the divergence is documented above and is intentional. + +The following node types from the Doorenbos thesis are in scope for v1 of this engine: + +| Node Type | Phase | Description | +|-----------|-------|-------------| +| `AlphaNode` | Phase 1 | Indexes facts by `(id?, attr)` pattern; feeds an `AlphaMemory`. | +| `AlphaMemory` | Phase 1 | Stores facts matching one alpha pattern. | +| `BetaMemory` | Phase 1 | Stores partial matches (tokens) produced by left activations. | +| `JoinNode` | Phase 1 | Combines left tokens with right alpha facts; performs variable binding and equality checks. | +| `FilterNode` | Phase 1 | Applies registered predicates to tokens; the analog of CLIPS-style `cond` / test nodes. | +| `ProductionNode` | Phase 1 | Terminal node; triggers an RHS handler on each full match (subject to refraction). | +| `DerivedFactProduction` | Phase 1 | The `thenFinally` variant of a production; retracts its derived fact when the supporting match is removed. | +| `NegationNode` | Phase 2 | Passes a token iff zero facts match the negated pattern (NOT). | +| `ExistentialNode` | Phase 2 | Passes a token iff at least one fact matches the pattern (EXISTS). | +| `NccNode` | Phase 2 | Passes a token iff no combination of N conditions matches (negated conjunctive condition). | +| `AggregationNode` | Phase 2 | Computes `count` / `sum` / `min` / `max` / `collect` over matching facts; binds the result to a variable. | + +Out of scope for v1: RETE/UL (unlinking), right-unlinking optimisation, sequential mode, and any conflict-set priority queue beyond the three-key sort defined in Conflict Resolution. These may be revisited in later versions if profiling demonstrates a need; until then, the simpler implementation is preferred. diff --git a/packages/rete/package.json b/packages/rete/package.json new file mode 100644 index 0000000..bc44691 --- /dev/null +++ b/packages/rete/package.json @@ -0,0 +1,22 @@ +{ + "name": "@paratype/rete", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --dts --clean", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.0.0" + } +} diff --git a/packages/rete/src/index.ts b/packages/rete/src/index.ts new file mode 100644 index 0000000..5e2a1b7 --- /dev/null +++ b/packages/rete/src/index.ts @@ -0,0 +1,3 @@ +// @paratype/rete — Doorenbos-style Rete II rules engine +// Phase 1 implementation begins here +export {}; diff --git a/packages/rete/tsconfig.json b/packages/rete/tsconfig.json new file mode 100644 index 0000000..c49dd21 --- /dev/null +++ b/packages/rete/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true + }, + "include": ["src/**/*"] +} diff --git a/packages/rete/vitest.config.ts b/packages/rete/vitest.config.ts new file mode 100644 index 0000000..1f98f96 --- /dev/null +++ b/packages/rete/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "rete", + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + }, +}); diff --git a/packages/server/PROTOCOL.md b/packages/server/PROTOCOL.md new file mode 100644 index 0000000..fe45e73 --- /dev/null +++ b/packages/server/PROTOCOL.md @@ -0,0 +1,291 @@ +# Chess Server WebSocket Protocol v1 + +## Overview + +All WebSocket messages are JSON. Every message includes: + +- `v: 1` — protocol version; mismatch triggers hard disconnect +- `seq: number` — monotonic sequence number (client and server maintain separate counters; client acks server seq) +- `ts: number` — unix milliseconds timestamp of message creation + +Maximum message size: 64KB. Exceeding this limit causes hard disconnect with `error` code `MSG_TOO_LARGE`. + +The server is AUTHORITATIVE. Clients send INTENTS (what they want to do). The server validates, applies the intent to the engine session, and broadcasts FACTS (what changed) to all room members. + +## Connection + +WebSocket upgrade URL: `ws://{host}:{port}/ws` + +Required query params on upgrade: none (auth happens via first message after connect). + +Origin allow-list: Configurable via `ALLOWED_ORIGINS` env var (comma-separated). Requests from unlisted origins receive HTTP 403 before upgrade. Default: `http://localhost:5173` (dev). + +## Message Envelope + +Every message has this wrapper: + +```json +{ + "v": 1, + "seq": 42, + "ts": 1745000000000, + "type": "room.create", + "token": "optional-room-token", + "payload": {} +} +``` + +`token` is required on all messages AFTER the first `room.create` or `room.join`. The first message from a client need not include token. + +## Auth & Rooms + +- Room codes: 6 characters, uppercase `[A-Z0-9]`, randomly generated +- Room token: UUID v4, returned on `room.create`, required on subsequent messages +- Rooms are in-memory only (lost on server restart) +- Maximum 2 players per room +- 60-second reconnection grace window after disconnect + +### Message: room.create + +Direction: Client → Server +Purpose: Create a new game room. Server responds with room code and auth token. + +Request payload: + +```json +{ + "rulesetIds": ["pawns-move-backward", "piece-hp"] +} +``` + +- `rulesetIds`: optional array of preset rule IDs to activate for this game + +Response (Server → Client, type `room.created`): + +```json +{ + "v": 1, "seq": 1, "ts": 1745000000000, + "type": "room.created", + "payload": { + "code": "ABC123", + "token": "550e8400-e29b-41d4-a716-446655440000", + "color": "white" + } +} +``` + +Error cases: + +- Server at capacity (too many rooms): `error` code `SERVER_FULL` + +### Message: room.join + +Direction: Client → Server +Purpose: Join an existing room as the second player. + +Request payload: + +```json +{ + "code": "ABC123" +} +``` + +Response (Server → Client, type `room.joined`): + +```json +{ + "v": 1, "seq": 1, "ts": 1745000000000, + "type": "room.joined", + "payload": { + "code": "ABC123", + "token": "661f9500-f30c-52e5-b827-557766550111", + "color": "black", + "activeRules": ["pawns-move-backward"] + } +} +``` + +When second player joins, server broadcasts `game.state` to BOTH players (initial board state). + +Error cases: + +- Room not found: `error` code `ROOM_NOT_FOUND` +- Room full (2 players already): `error` code `ROOM_FULL` +- Wrong protocol version: disconnect + `error` code `VERSION_MISMATCH` + +### Message: room.leave + +Direction: Client → Server +Purpose: Voluntarily leave a room / concede. + +Request payload: `{}` + +Server broadcasts `game.end` with `reason: "player_left"` to remaining player. + +### Message: game.move + +Direction: Client → Server +Purpose: Express a move intent. Server validates and applies if legal. + +Request payload: + +```json +{ + "from": "e2", + "to": "e4", + "promoteTo": "queen" +} +``` + +- `from`, `to`: algebraic square notation (a1–h8) +- `promoteTo`: optional, only relevant for pawn promotion; one of `"queen"`, `"rook"`, `"bishop"`, `"knight"` + +On valid move: Server applies to engine, broadcasts `game.delta` to both players. +On invalid move: Server sends `error` (code `ILLEGAL_MOVE`) to mover only; no broadcast. + +Error cases: + +- Not your turn: `error` code `NOT_YOUR_TURN` +- Illegal move: `error` code `ILLEGAL_MOVE` +- Game already over: `error` code `GAME_OVER` + +### Message: game.state + +Direction: Server → Client +Purpose: Full board state snapshot. Sent on room join and on reconnect. + +Payload: + +```json +{ + "fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + "facts": [ + { "id": 1, "attr": "PieceType", "value": "pawn" }, + { "id": 1, "attr": "Color", "value": "white" }, + { "id": 1, "attr": "Position", "value": 12 } + ], + "activeRules": ["pawns-move-backward"], + "turn": "black", + "lastSeq": 42, + "moveHistory": ["e2-e4"] +} +``` + +- `fen`: standard FEN string for UI rendering convenience +- `facts`: all non-derived working-memory facts +- `lastSeq`: highest server seq the client should ack from (used for reconnect delta) + +### Message: game.delta + +Direction: Server → Client +Purpose: Incremental fact changes after each move. Sent to BOTH players after valid move. + +Payload: + +```json +{ + "inserted": [ + { "id": 1, "attr": "Position", "value": 28 } + ], + "retracted": [ + { "id": 1, "attr": "Position", "value": 12 } + ], + "moveNotation": "e2e4", + "turn": "black", + "gameOver": null +} +``` + +- `inserted`: facts added to working memory this tick +- `retracted`: facts removed this tick (identified by id+attr) +- `gameOver`: null if game continues; `{ winner: "white"|"black"|"draw", reason: "checkmate"|"stalemate"|"50-move"|"threefold"|"insufficient"|"player_left" }` when game ends + +### Message: game.end + +Direction: Server → Client +Purpose: Explicit game-over notification (also included in `game.delta` `gameOver` field, but sent separately for clarity). + +Payload: + +```json +{ + "winner": "white", + "reason": "checkmate", + "finalFen": "rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 0 3" +} +``` + +### Message: error + +Direction: Server → Client +Purpose: Report validation failures, protocol errors, rate limit violations. + +Payload: + +```json +{ + "code": "ILLEGAL_MOVE", + "message": "Move e2-e5 is not legal for white pawn on e2", + "fatal": false +} +``` + +- `fatal: true` means the connection will be closed immediately after this message (e.g., rate limit, version mismatch, bad auth) +- `fatal: false` means the connection stays open; client may retry + +## Reconnection Flow + +1. Client disconnects (network drop, tab sleep, etc.) +2. Server starts a 60-second grace timer for that client's slot in the room +3. During grace period: other player continues playing (server holds their moves but doesn't process if no opponent — or processes against a "ghost" state) +4. Client reconnects: sends `room.join` with same code and original token +5. Server validates token matches the disconnected slot +6. Server sends `game.state` (full snapshot) followed by all `game.delta` messages since the client's last acked `seq` +7. Client reconciles local state with the snapshot +8. After 60 seconds: server broadcasts `game.end` with `reason: "player_left"` to the remaining player; room is destroyed + +Reconnect message flow: + +``` +Client Server + |-- room.join (code, token) -->| + |<-- game.state (full snap) ---| + |<-- game.delta (seq 43..50) --| (missed deltas since last ack) + | [game resumes] | +``` + +## Rate Limiting + +Token-bucket algorithm per WebSocket connection: + +- Capacity: 20 messages (burst) +- Refill rate: 100 messages per second +- When bucket is empty: connection is closed with `error` code `RATE_LIMIT`, `fatal: true` + +Messages counted: all messages from client including heartbeats. + +## Security + +- **Origin**: Only connections from `ALLOWED_ORIGINS` (env var) are accepted +- **Message size**: Max 64KB; exceeded → disconnect + `error` code `MSG_TOO_LARGE` +- **JSON validation**: Every message validated against Zod schema; malformed JSON → disconnect +- **Token**: Room tokens are UUID v4; expire when room is destroyed; not reusable across rooms +- **No user auth**: v1 has no accounts; rooms are ephemeral; tokens are room-scoped only +- **No server-side persistence**: Game state lives in-memory; server restart destroys all rooms + +## Error Codes + +| Code | Fatal | Description | +|------|-------|-------------| +| `ILLEGAL_MOVE` | No | Move not in legal move set | +| `NOT_YOUR_TURN` | No | Move attempted when it's opponent's turn | +| `GAME_OVER` | No | Move attempted after game ended | +| `ROOM_NOT_FOUND` | No | Room code doesn't exist or expired | +| `ROOM_FULL` | No | Room already has 2 players | +| `SERVER_FULL` | No | Server at maximum room capacity | +| `VERSION_MISMATCH` | Yes | `v` field doesn't match server's protocol version | +| `RATE_LIMIT` | Yes | Message rate exceeded 100/sec | +| `MSG_TOO_LARGE` | Yes | Message exceeds 64KB | +| `BAD_TOKEN` | Yes | Token missing or invalid for room | +| `INVALID_MESSAGE` | Yes | JSON parse failure or schema validation failure | diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..ae3d9c8 --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1 @@ +# @paratype/chess-server — authoritative WebSocket server diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..11fca93 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,15 @@ +{ + "name": "@paratype/chess-server", + "version": "0.1.0", + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "build": "echo 'server runs via bun directly'", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paratype/rete": "workspace:*", + "pino": "^9.0.0", + "zod": "^3.23.0" + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..f9e0f4f --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,2 @@ +// @paratype/chess-server — authoritative Bun WebSocket server +export {}; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..04c5039 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"], + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "references": [{ "path": "../rete" }] +} diff --git a/packages/server/vitest.config.ts b/packages/server/vitest.config.ts new file mode 100644 index 0000000..356699c --- /dev/null +++ b/packages/server/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "server", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..ce984f2 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: "**/e2e/**/*.spec.ts", + use: { + baseURL: "http://localhost:5173", + trace: "on-first-retry", + video: "on-first-retry", + }, + webServer: { + command: "bun run --filter @paratype/chess dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + timeout: 30000, + }, + reporter: [["html", { open: "never" }], ["list"]], +}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..24e1d67 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "strict": true, + "noImplicitAny": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "skipLibCheck": true, + "esModuleInterop": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b7b60b2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "files": [], + "references": [ + { "path": "packages/rete" }, + { "path": "packages/chess" }, + { "path": "packages/server" } + ] +} diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000..f676c22 --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,7 @@ +import { defineWorkspace } from "vitest/config"; + +export default defineWorkspace([ + "packages/rete/vitest.config.ts", + "packages/chess/vitest.config.ts", + "packages/server/vitest.config.ts", +]);