Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/hooks/check-commit-format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ SUBJECT="$(printf '%s\n' "$MSG" | head -1)"
# --- Validation ----------------------------------------------------------

# 1. Subject grammar.
SCOPE_ALLOW='knowrag|api|ui|mcp|ingest|repo|platform|stacks|state|ops|ci|docs|release'
SCOPE_ALLOW='knowrag|reliquary|cardshed|api|ui|mcp|ingest|repo|platform|stacks|state|ops|ci|docs|release'
# Description: starts lowercase, may contain any non-newline (incl. internal
# periods like SemVer `v0.1.0`), but must NOT end with a trailing period
# immediately before the ` (#N)` issue ref.
Expand Down
166 changes: 166 additions & 0 deletions @lab/ll-CARDSHED/.claude/rules/core-determinism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Core Determinism — MANDATORY for `apps/core/`

> **Load-bearing.** The rules engine is the *truth machine*. If purity breaks, replay breaks, anti-cheat breaks, bot simulation breaks, and online clients desync. This rule encodes the PRP 2 success criteria as a permanent guardrail.

This rule applies to **every file under `@lab/ll-CARDSHED/apps/core/src/core/`**. It does NOT apply to `apps/ui/`, `apps/server/`, `scripts/`, or tests.

---

## 1. Forbidden in `src/core/**`

The following are **banned**, enforced by ESLint:

- `Math.random()` — randomness goes through `prng.mulberry32(seed)`.
- `Date.now()`, `new Date()`, `performance.now()` — the engine has no concept of wall-clock time. Timestamps belong to PRP 3's event emitter.
- `crypto.getRandomValues()` — same reason as `Math.random`; the seed source lives outside core.
- `setTimeout`, `setInterval`, `requestAnimationFrame` — the engine is synchronous.
- `console.*` — emit events via the `GameEvent` union, not log lines.
- `fetch`, `XMLHttpRequest`, `WebSocket`, `fs`, `child_process`, `process.env` — no I/O.
- Module-level mutable state — every reducer derives next-state from inputs only.

The only exception: `prng.ts` itself uses `Math.imul` (a math operation, not randomness). It does NOT call `Math.random`.

---

## 2. Required patterns

### 2.1 Errors are values

Validation failures MUST return `{ ok: false, error: { code, message, details? } }`. Throws are reserved for **invariant violations** — programmer bugs that should crash the process so the test suite catches them.

```ts
// CORRECT — user-facing validation
return { ok: false, error: { code: "ATTACK_INVALID_SIZE", message: "..." } };

// CORRECT — invariant (cannot happen if the engine is correct)
if (!c) throw new Error("INVARIANT: deck exhausted during initial deal");

// WRONG — throwing on user input
if (cards.length !== 1 && cards.length !== 3 && cards.length !== 5) {
throw new Error("Invalid attack size"); // ❌ should return ok:false
}
```

### 2.2 Pure reducers

Every state-mutating function MUST return a fresh `MatchState`. The input is treated as read-only.

```ts
// CORRECT
function submitAttack(state: MatchState, ...): ActionResult {
const next = structuredClone(state); // or .slice() per array
// ...mutate next, never state
return { ok: true, state: next, events };
}

// WRONG
function submitAttack(state: MatchState, ...): ActionResult {
state.round.phase = "AwaitingDefense"; // ❌ mutates input
return { ok: true, state, events };
}
```

### 2.3 Seeded randomness only

`shuffleDeck(deck, seed)` is the **only** randomness entry-point. The `seed` parameter is REQUIRED (per PRP 3 NEW #6). Same seed in → byte-identical permutation out, on every JS runtime.

```ts
// CORRECT
const shuffled = shuffleDeck(deck, matchState.rngSeed);

// WRONG
const shuffled = deck.slice().sort(() => Math.random() - 0.5); // ❌ ban
```

### 2.4 Hidden information is law

`createPublicView(state, viewerId?)` MUST NOT leak opponent hand contents or deck order. `createPrivateView(state, viewerId)` reveals only the viewer's own hand.

```ts
// CORRECT
players: state.players.map((p) => ({
...,
hand: { ownerId: p.id, count: p.hand.length, publiclyKnown: [] },
}))

// WRONG
players: state.players.map((p) => ({ ..., hand: p.hand })) // ❌ leaks
```

### 2.5 Card.id is opaque

Consumers MUST use `card.suit` and `card.rank` for game logic. Never parse `card.id` to extract them. The id format is presentational and may change.

```ts
// CORRECT
if (card.suit === trump && card.rank > attack.rank) return true;

// WRONG
const [suit, rank] = card.id.split("-"); // ❌ never
```

---

## 3. Conservation invariant

Across every legal state transition, exactly 52 cards must be accounted for:

```
sum(player.hand.length) + deck.length + discard.length + pendingAttackCardCount(round.pendingAttack) === 52
```

The property test `conservation.property.test.ts` enforces this across ≥200 random-legal-action sequences. **Any change that breaks this invariant is a bug — never widen the property to make red green.**

---

## 4. Win-check ordering

`checkWin(state, playerId)` MUST run **after** `drawToMinimum` in every refill path. A player at 0 cards with a non-empty deck will be refilled to 5 and is NOT a winner.

`checkWin` MUST run on BOTH the full-defence and partial-defence branches of `stopDefending` (per PRP 2 #3) — a defender CAN win on the trailing edge of a round.

---

## 5. No reshuffles

Once `deck.length === 0`, draws stop. The discard pile NEVER goes back into the deck. This is Rules v2.0 and is non-negotiable.

---

## 6. When adding new functions

1. Add the type signature to the Rules Engine API in `src/core/index.ts` exports.
2. Write the test FIRST under `src/core/__tests__/<function>.test.ts`.
3. Implement the function with `structuredClone` for any state derivation.
4. Confirm the function returns `ActionResult` (or a pure value type) — never `void`, never `throw` on user input.
5. Re-run the property test `conservation.property.test.ts` to confirm the invariant still holds.
6. Re-run `sim-smoke.ts` (1000 games) to confirm no regressions.

---

## 7. Validation

These commands MUST pass on every PR that touches `apps/core/`:

```bash
cd @lab/ll-CARDSHED/apps/core
npm run typecheck # 0 errors
npm run lint # 0 errors, 0 warnings (the no-Math.random rule must be active)
npm test # all 82+ tests pass
npm run sim-smoke # 1000 games, <30s, 0 conservation violations
```

If any of these go red, the engine is broken until they're green — no exceptions, no skip-failing-test commits.

---

## 8. Why this rule exists

The deterministic core is what lets us:

- **Replay any match** from `(matchSeed, actions[])` — bug reproduction, balance analysis, anti-cheat audit.
- **Run millions of bot-vs-bot games** for balance metrics — only possible because the engine has zero I/O.
- **Mechanically port to Rust** for the online server (PRP 1 §2.3) — the TS implementation is the canonical spec; non-deterministic JS hides in plain sight when ported.
- **Trust client-side legal-action highlighting** — the client and server agree because both run the same engine.

One `Math.random()` slip in this layer breaks all four. The rule is mechanical because the consequence is invisible until it bites.
148 changes: 148 additions & 0 deletions @lab/ll-CARDSHED/.claude/rules/ui-design-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# UI Design Pipeline — MANDATORY for ll-CARDSHED

> **EPIC RULESET — load-bearing once `apps/ui/` exists.** The interface IS the game-feel layer. There is no hand-rolled UI escape hatch.

This rule strengthens the root `.claude/rules/ui-design.md`. Where they conflict, **this file wins** inside `@lab/ll-CARDSHED/`.

> Note: this rule has **no effect** until `apps/ui/` lands at PRP 3 M1. The bootstrap stub has no UI surface. From M1 onward, every UI change goes through the pipeline below.

---

## 1. The Pipeline (every UI change goes through it)

```
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ IDEA │ → │ STITCH │ → │ AGENT-BROWSER │
│ (PRP 3 §B │ │ (skill + MCP │ │ (Vercel-based │
│ screen spec) │ generation) │ │ validation) │
└──────────────┘ └──────────────────┘ └────────┬────────┘
▼ if complex eval
┌─────────────────────┐
│ PLAYWRIGHT MCP + │
│ SKILLS (deep e2e) │
└─────────────────────┘
```

You do **not** skip a stage. You do **not** reorder them. You do **not** invent UI without a Stitch artifact upstream.

---

## 2. STAGE 1 — STITCH (generation / transformation)

**Use one of these — nothing else:**

| Tool | When |
|------|------|
| `Skill: stitch-design` | Default entry-point for any new or modified screen. Wraps prompt-enhancement, design-system synthesis, and screen generation/edit. |
| `Skill: enhance-prompt` | When the input idea is vague — runs first, then hands off to `stitch-design`. |
| `Skill: design-md` | When the design system itself needs synthesis into `.stitch/DESIGN.md`. |
| `Skill: stitch-loop` | When iterating on multiple screens with consistent system (baton-pass loop). |
| `mcp__stitch__*` | Direct MCP calls when the skill abstraction is wrong fit (rare). |

**Mandatory inputs every Stitch invocation:**

1. The screen spec from `PRPs/cardshed-03-experience-prp.md` §B.
2. The relevant engine API contract (`apps/core/src/core/rules.ts` + `types.ts`) — the UI must not invent state the engine doesn't expose.
3. The pre-Stitch wireframe (PRP 3 §B per-screen ASCII layout).
4. The current `.stitch/DESIGN.md` (so output stays coherent).

**Output destination:**
- Screen mockups → `docs/SCREENS/<screen-slug>.md` (+ exported assets if Stitch returns them).
- Design system updates → `.stitch/DESIGN.md` (only `stitch-design` / `design-md` write here).

---

## 3. STAGE 2 — AGENT-BROWSER (validation / visualisation)

Default validator. Use for: dogfooding, screenshot capture, exploratory QA, click-through flows, simple regression smoke.

**Why agent-browser over Playwright MCP for the default case?** Per project memory: on Ubuntu 26.04, Playwright MCP can't install Chromium reliably; agent-browser ships a bundled browser. It also runs in Vercel-sandbox microVMs for clean-room runs.

**Use the `agent-browser` skill** for every UI-change verification. Output goes under `dogfood-output/<UTC-timestamp>/` as `report.md` + screenshots.

**Mandatory verification gates after every Stitch generation:**
- ✅ Screen renders without console errors.
- ✅ Screenshot captured at the canonical viewport (desktop 1440×900 minimum; add mobile when the storyboard demands it).
- ✅ At least one game-relevant action exercised (submit an attack, beat a card, stop defending) and its result captured.

---

## 4. STAGE 3 — PLAYWRIGHT MCP + SKILLS (complex eval)

Reach for Playwright **only** when the case demands what agent-browser can't cleanly do:

- Multi-tab / multi-context scenarios (hot-seat pass-and-play simulation across two viewports).
- Network interception / mock injection (testing the WebSocket reconnection path at PRP 3 M14+).
- Trace recording for performance analysis (card-flip animation budget).
- Programmatic accessibility-tree assertions.
- Long-running e2e suites with reporters.

**Tools:** `mcp__plugin_playwright_playwright__*` and the `webapp-testing` skill.

**Do NOT** use Playwright when agent-browser suffices — the heavier setup makes change loops slow.

---

## 5. Hard prohibitions

- ❌ **Hand-writing screens** — every screen has a Stitch origin in `docs/SCREENS/`.
- ❌ **Inventing design tokens** — read `.stitch/DESIGN.md` and apply via `mcp__stitch__apply_design_system`. If a token doesn't exist, regenerate the design system first.
- ❌ **Claiming a UI change is "done" without `agent-browser` evidence** under `dogfood-output/`. Type-check + unit-test passing ≠ UI works.
- ❌ **Re-implementing rules in the UI.** The UI must consume `@cardshed/core` for every legality check, legal-action enumeration, and view projection. If a UI helper duplicates a rule function, delete the helper and call the core.
- ❌ **Mixing UI frameworks.** React + Tailwind v4 + Radix + framer-motion + Zustand + TanStack Query + Immer. No Material UI, no shadcn-without-Stitch-mapping, no PixiJS (cards are DOM, not canvas).
- ❌ **Mocking the UI surface in screenshot tests** — capture from the running container.

---

## 6. Decision flow (when uncertain)

```
UI task arrives
├─ Vague request ("make the table feel less cramped")
│ → enhance-prompt → stitch-design
├─ New screen needed
│ → stitch-design (with all 4 mandatory inputs)
├─ Design system feels incoherent across screens
│ → design-md → mcp__stitch__create_design_system → apply across all screens
├─ Many screens to evolve together
│ → stitch-loop
├─ Verify a change works in browser
│ → agent-browser → capture screenshot → save under dogfood-output/
├─ Need multi-tab / network mock / a11y tree / trace
│ → playwright MCP + webapp-testing skill
└─ Just code, no visual change?
→ still verify with agent-browser; UI plumbing without proof = unverified
```

---

## 7. Acceptance checklist (every UI PR — applies from PRP 3 M1)

- [ ] Screen spec referenced from `PRPs/cardshed-03-experience-prp.md` §B
- [ ] Stitch mockup under `docs/SCREENS/<screen>.md` with provenance (which prompt, which design-system version)
- [ ] Screen built against the current `.stitch/DESIGN.md`
- [ ] UI consumes `@cardshed/core` for every rule (no rule re-implementation)
- [ ] `agent-browser` run under `dogfood-output/<timestamp>/` with `report.md` + screenshots
- [ ] If complex behaviour: Playwright trace under `dogfood-output/<timestamp>/traces/`
- [ ] No new design tokens introduced outside `.stitch/DESIGN.md`
- [ ] Lint, type-check, unit-tests green (core + ui both)

---

## 8. Why this rule exists

Three failure modes we explicitly prevent:

1. **Bespoke-UI drift** — once a developer hand-rolls one screen, the design system fractures. Every screen Stitch-touched stays coherent.
2. **Unverified-frontend claims** — "tests pass" doesn't mean the screen works. Real-browser evidence under `dogfood-output/` is the only proof we accept.
3. **Rule duplication** — the UI is *tempted* to re-implement "is this card legal?" for ergonomic reasons. That temptation is how clients and servers desync. The engine API is the single source of truth.

This is policy, not preference. If you find a real reason the pipeline can't apply, document it in `docs/DECISIONS/` and propose a rule amendment — don't bypass.
16 changes: 16 additions & 0 deletions @lab/ll-CARDSHED/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ─────────────────────────────────────────────────────────────
# ll-CARDSHED — environment schema (committed, placeholders only)
# Copy to .env, fill values, NEVER commit .env.
#
# At bootstrap the stack ships a stub container only — these
# vars become load-bearing at PRP 3 M1 when apps/ui/ lands.
# ─────────────────────────────────────────────────────────────

# ── Service ports (host-side) ────────────────────────────────
UI_PORT=4343

# ── Logging ──────────────────────────────────────────────────
LOG_LEVEL=info

# ── UI build-time (PRP 3 M1) ─────────────────────────────────
VITE_GAME_TITLE=CARD SHED
23 changes: 23 additions & 0 deletions @lab/ll-CARDSHED/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Runtime secrets — never commit (use .env.sops when ready)
.env

# Container volumes
data/

# Dogfood / playwright artifacts (keep .gitkeep + curated reports)
dogfood-output/*/
!dogfood-output/.gitkeep
!dogfood-output/*/report.md

# Stitch generated screen exports (only DESIGN.md is committed)
.stitch/screens/
.stitch/projects/
.stitch/*.tmp

# Vite / build outputs (PRP 3 M1+)
apps/ui/dist/
apps/ui/.vite/

# Editor noise
.DS_Store
*.swp
Loading
Loading