From 1217a37ab6b68857eff41f09ec45f610441da895 Mon Sep 17 00:00:00 2001 From: GenWave Radio Date: Tue, 29 Sep 2026 11:20:40 -0600 Subject: [PATCH] chore(toolkit): add /sprint flow, keep GenWave rules Merge the upstream toolkit's sprint pipeline into the GenWave .claude config without losing the C#/.NET and repo-policy specifics the upstream copy had overwritten. New: /sprint (brief -> one approval -> plan -> build -> acceptance), architect + product-owner agents, product-owner skill, delight:/polish: tasks, pending-spec activation, build-loop acceptance pass, and the /git-issue, /git-pr, /git-merge, /git-remote helpers. Kept from GenWave: builder/reviewer C# skills, zero-warnings gate, PASS-WITH-NOTES, reviewer with no edit tools on the session model, full-solution test runs, xUnit specs, DEPLOYMENT.md checks, never on main, no attribution trailers. /git-merge prepares a PR and hands Dean the merge command instead of merging. Git helpers defer to git-workflow. --- .claude/README.md | 317 +++++++++++------- .claude/agents/architect.md | 66 ++++ .claude/agents/builder.md | 14 +- .claude/agents/product-owner.md | 77 +++++ .claude/agents/reviewer.md | 9 +- .claude/commands/build-loop.md | 51 ++- .claude/commands/design.md | 8 +- .claude/commands/document.md | 10 +- .claude/commands/explore.md | 7 +- .claude/commands/git-issue.md | 51 +++ .claude/commands/git-merge.md | 57 ++++ .claude/commands/git-pr.md | 58 ++++ .claude/commands/git-remote.md | 200 +++++++++++ .claude/commands/init.md | 26 +- .claude/commands/plan.md | 98 ++++-- .claude/commands/quick-fix.md | 1 + .claude/commands/spec.md | 32 +- .claude/commands/sprint.md | 130 +++++++ .../agent-teams/references/orchestration.md | 2 +- .claude/skills/bdd-specs/SKILL.md | 13 +- .claude/skills/product-owner/SKILL.md | 161 +++++++++ .../skills/product-owner/templates/BRIEF.md | 68 ++++ .claude/skills/sqlite-dev/SKILL.md | 173 ++++++++++ .../skills/sqlite-dev/references/drizzle.md | 81 +++++ .../skills/sqlite-dev/references/naming.md | 77 +++++ .../sqlite-dev/references/portability.md | 124 +++++++ .../sqlite-dev/references/production.md | 108 ++++++ .claude/skills/sqlite-dev/references/types.md | 56 ++++ .../sqlite-dev/templates/db.production.ts | 57 ++++ .claude/skills/sqlite-dev/templates/db.ts | 33 ++ .../sqlite-dev/templates/drizzle.config.ts | 27 ++ .claude/skills/sqlite-dev/templates/schema.ts | 162 +++++++++ 32 files changed, 2157 insertions(+), 197 deletions(-) create mode 100644 .claude/agents/architect.md create mode 100644 .claude/agents/product-owner.md create mode 100644 .claude/commands/git-issue.md create mode 100644 .claude/commands/git-merge.md create mode 100644 .claude/commands/git-pr.md create mode 100644 .claude/commands/git-remote.md create mode 100644 .claude/commands/sprint.md create mode 100644 .claude/skills/product-owner/SKILL.md create mode 100644 .claude/skills/product-owner/templates/BRIEF.md create mode 100644 .claude/skills/sqlite-dev/SKILL.md create mode 100644 .claude/skills/sqlite-dev/references/drizzle.md create mode 100644 .claude/skills/sqlite-dev/references/naming.md create mode 100644 .claude/skills/sqlite-dev/references/portability.md create mode 100644 .claude/skills/sqlite-dev/references/production.md create mode 100644 .claude/skills/sqlite-dev/references/types.md create mode 100644 .claude/skills/sqlite-dev/templates/db.production.ts create mode 100644 .claude/skills/sqlite-dev/templates/db.ts create mode 100644 .claude/skills/sqlite-dev/templates/drizzle.config.ts create mode 100644 .claude/skills/sqlite-dev/templates/schema.ts diff --git a/.claude/README.md b/.claude/README.md index a9c660b3..2455cfa6 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -1,9 +1,9 @@ # GenWave Claude Code Toolkit -The `.claude/` configuration that turns Claude Code into a disciplined, -phased software-development pipeline for this repo: slash commands that take -an idea from a one-line pitch to committed, reviewed code, plus the skill -library the commands and agents lean on. +The `.claude/` configuration that gives Claude Code a small product team and +a pipeline to run it for this repo. One command takes a sentence to +committed, reviewed code and an open PR, with one approval from you in the +middle. It started life as a portable toolkit. Most of it still is; the parts that are GenWave-specific are marked πŸŽ™οΈ below. `.claude/templates/lang-template` @@ -13,98 +13,162 @@ is the starting point for a new language skill and is not loaded as a skill. Every command, agent, and skill in this collection serves one principle: **the goal of writing software is to be able to change it safely.** SOLID, -the GoF patterns, coupling/cohesion, BDD specs, schema conventions β€” -they're not ends, they're tactics in service of that goal. +the GoF patterns, coupling/cohesion, BDD specs, schema conventions are +tactics in service of that goal. Concretely, every artifact this toolkit produces should optimize for: -- Low coupling, high cohesion β€” one reason to change per module (SRP). -- Open to extension, closed to modification β€” stable seams behind interfaces. -- Localized blast radius β€” a change in one place doesn't ripple. +- Low coupling, high cohesion. One reason to change per module (SRP). +- Open to extension, closed to modification. Stable seams behind interfaces. +- Localized blast radius. A change in one place doesn't ripple. - Composition over shared mutable state. - Names that telegraph intent so the next person finds the seam. - A small diff for the next change. -Read every skill, command, and review finding through this lens. If a rule -in here doesn't make the next change easier, it's the wrong rule. +If a rule in here doesn't make the next change easier, it's the wrong rule. + +## βš–οΈ Second principle: spend tokens, not attention + +Rigor that costs tokens (review gates, specs, entry-point traces, smoke +tests) stays. Rigor that costs the human's attention (interviews, +handoffs, approvals) is cut to one approval per sprint. Agents guess, write +the guess down, and let the human correct it. Merging stays Dean's, always. ## Layout ``` .claude/ -β”œβ”€β”€ commands/ # slash-command phases + git helpers -β”œβ”€β”€ agents/ # builder + reviewer subagents (used by /build-loop) -└── skills/ # knowledge + workflow skills, surfaced by description +β”œβ”€β”€ commands/ # /sprint, the phases it runs, git helpers +β”œβ”€β”€ agents/ # product-owner, architect, builder, reviewer +β”œβ”€β”€ hooks/ # πŸŽ™οΈ merge-guard.sh (merge/tag/release/push-main = Dean) +β”œβ”€β”€ skills/ # knowledge + workflow skills, surfaced by description +└── templates/ # lang-template (not loaded as a skill) ``` ## Commands -Phase commands run in order on a fresh project; each owns exactly one set of -documents and is **re-entrant** (re-run to refine, not restart). - ``` -/init ──▢ /explore ──▢ /design ──▢ /plan ──▢ /spec ──▢ /build-loop - (problem) (solution) (tasks) (specs) (code) +/init ──▢ /sprint ──▢ βœ‹ approve the brief ──▢ /plan ──▢ /build-loop ──▢ acceptance ──▢ PR + └────────── run by /sprint β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -/document ── run anytime; reconciles docs ↔ reality (not a phase) +/quick-fix ── trivial fix, still on a branch +/document ── run anytime; reconciles docs ↔ reality +/explore, /design, /spec ── optional, run by hand ``` -### Phase commands +### The main path -| Command | Question it answers | Owns | -|---|---|---| -| `/init` | What files do we need? | `CLAUDE.md`, `docs/` skeleton, `README.md` stub, `.gitignore` | -| `/explore` | What problem, for whom, why? | `docs/PROJECT.md` | -| `/design` | How do we build it? | `docs/ARCHITECTURE.md`, `docs/SPEC.md` | -| `/plan` | In what order, as what tasks? | `docs/STORIES.md`, `docs/PLAN.md` | -| `/spec` | Pending BDD specs from stories | spec files (via `bdd-specs` skill) | -| `/build-loop` | Build it. | the code + git history | -| `/document` | Do the docs still match reality? | `README.md`, `docs/ARCHITECTURE.md` prose, `docs/MEMORY.md` | -| `/quick-fix` | Trivial fix on the current branch β€” no plan, no stories. | the code | - -`docs/MEMORY.md` is the project's decision log β€” every phase *appends* dated -entries; `/document` curates it. It is distinct from the `~/.claude` memory -system. - -Phases that gather requirements (`/explore`, `/design`) interview you with -batched `AskUserQuestion` rounds. Phases that transform (`/plan`, `/spec`, -`/build-loop`) consume the prior phase's output and do not re-elicit. +| Command | Model | Question it answers | Owns | +|---|---|---|---| +| `/init` | sonnet | What files do we need? | `CLAUDE.md`, `docs/` skeleton, `README.md` stub, `.gitignore` | +| `/sprint` | fable | What are we building, for whom, and how? | `docs/BRIEF.md` (old briefs move to `docs/briefs/`) | +| `/plan` | fable | In what order, as what tasks? | `docs/STORIES.md`, `docs/PLAN.md`, pending specs | +| `/build-loop` | inherit | Build it. | the code + git history | +| `/document` | sonnet | Do the docs still match reality? | `README.md`, `DEPLOYMENT.md` values, `docs/ARCHITECTURE.md` prose, `docs/MEMORY.md` | +| `/quick-fix` | sonnet | Trivial fix on a branch. | the code | + +### Optional deep dives + +| Command | Model | When to use it | Owns | +|---|---|---|---| +| `/explore` | fable | The idea is fuzzy and you want to be interviewed about it. | `docs/PROJECT.md` | +| `/design` | fable | Greenfield or risky, and you want to make each architecture call yourself. | `docs/ARCHITECTURE.md`, `docs/SPEC.md` | +| `/spec` | sonnet | Stories were added after `/plan` ran and need specs. | spec files | + +If `PROJECT.md` or `SPEC.md` exist, `/sprint` reads them and builds on +them. `docs/` is gitignored πŸŽ™οΈ: these are local working docs. + +`docs/MEMORY.md` is the project's decision log. Every command *appends* +dated entries; `/document` curates it. It is distinct from the `~/.claude` +memory system. ### Git helpers -Small commands that defer to the `git-workflow` skill β€” direct, detailed, no -ceremony. PRs and issues go through the `gh` CLI against `GenWave-Org/genwave` -(the skill ships body templates for both). Merging, tagging, releasing and -pushing `main` are Dean's; a PreToolUse hook enforces it. The hook path falls -back to `$PWD` when `CLAUDE_PROJECT_DIR` is unset or empty, so the guard still -fires in sessions started from the project root. +Small commands that defer to the `git-workflow` skill. PRs and issues go +through the `gh` CLI against `GenWave-Org/genwave` πŸŽ™οΈ. Merging, tagging, +releasing and pushing `main` are Dean's; a PreToolUse hook enforces it. The +hook path falls back to `$PWD` when `CLAUDE_PROJECT_DIR` is unset or empty, +so the guard still fires in sessions started from the project root. -| Command | Purpose | -|---|---| -| `/git-commit` | Stage explicitly and commit using Conventional Commits on a non-main branch. | +| Command | Model | Purpose | +|---|---|---| +| `/git-commit` | inherit | Stage explicitly and commit using Conventional Commits on a non-main branch. | +| `/git-issue` | sonnet | Open a GitHub issue via `gh issue create`. | +| `/git-pr` | sonnet | Open a PR with what, verification plan, risk note. Never merges. | +| `/git-merge` | sonnet | Get a PR merge-ready (checks, conflicts), then hand Dean the merge command. | +| `/git-remote` | sonnet | Stand up a GitHub remote (name, license, README, contributing, security, issue templates). Refuses when `origin` exists. | ## Agents -`builder` and `reviewer` exist only to serve `/build-loop`. - | Agent | Model | Tools | Role | |---|---|---|---| -| `builder` | sonnet | Read, Write, Edit, Glob, Grep, Bash, Skill | Implements one PLAN.md task. Stays in its files. Never commits unless told. | -| `reviewer` | inherit | Read, Glob, Grep, Bash, Skill (**no** Write/Edit) | Read-only gate. Returns `PASS` or `FAIL` with findings. A gate that can fix itself isn't a gate. | +| `product-owner` | fable | Read, Glob, Grep, Bash, Skill, WebSearch, WebFetch | Writes the product half of the brief. Adds extras inside the delight budget. Proposes cuts. Runs the finished work and returns `ACCEPT`, `POLISH`, or `REJECT`. | +| `architect` | fable | Read, Glob, Grep, Bash, Skill | Writes the technical half of the brief: approach, seams, data changes, decisions, task slice. | +| `builder` | sonnet | Read, Write, Edit, Glob, Grep, Bash, Skill | Implements one PLAN.md task (C#/.NET first-class; TypeScript for admin-ui). Stays in its files. Never commits unless told. | +| `reviewer` | inherit | Read, Glob, Grep, Bash, Skill (**no** Write/Edit) | Read-only gate. Returns `PASS`, `PASS-WITH-NOTES`, or `FAIL` with findings. | + +Only the builder can edit. Everyone else reports. The reviewer inherits +the session model so the gate is never weaker than the builder. + +## How a sprint runs + +``` + /sprint "add a quiet-hours schedule" + β”‚ + β–Ό + 1. THINK product-owner ─┐ dispatched together, + architect β”€β”€β”€β”€β”€β”˜ both get your exact words + β”‚ + β–Ό + 2. MERGE lead builds a one-page brief. Every extra is checked + against the delight budget. Failures move to "Next". + β”‚ + β–Ό + 3. GATE βœ‹ "Here's what we think." You approve, strike extras, + correct assumptions, answer up to 3 questions. + β”‚ + β–Ό + 4. WRITE docs/BRIEF.md, ARCHITECTURE.md additions, MEMORY.md + β”‚ + β–Ό + 5. PLAN stories, PLAN.md (extras tagged delight:), pending specs + β”‚ + β–Ό + 6. BUILD the build loop, below + β”‚ + β–Ό + 7. ACCEPT product-owner runs the app (./launch.sh) as a customer + β”‚ + β–Ό + 8. PR /git-pr; merge is Dean's +``` + +### The delight budget + +The product owner adds things you didn't ask for. The budget keeps that +small: -The reviewer is intentionally stronger than the builder. +- one to three extras per sprint, zero allowed +- no new dependency, no schema change, no new service or route +- one task, one commit +- removable: nothing depends on it +- a one-line why, written from the customer's side +- extras are at most about 15% of the sprint's tasks -## Orchestrating the build loop +Anything over budget goes under **Next** in the brief. The full rules are +in `skills/product-owner/SKILL.md`. -`/build-loop [path-to-PLAN.md]` (default `docs/PLAN.md`) is the only phase that -writes code. It is an **orchestrator pattern**: the command runs in the lead -thread and dispatches subagents β€” it never writes feature code or reviews code -itself. +## The build loop + +`/build-loop [path-to-PLAN.md]` (default `docs/PLAN.md`) is the only +command that writes feature code. It runs in the lead thread and +dispatches subagents. It never writes or reviews code itself. **Preflight:** resolve the plan, confirm it is a checkbox list (`- [ ]`), -confirm a clean git working tree. Stop and report if any of these fail. +confirm a clean tree on a feature branch, and a green full-solution +baseline (`dotnet test GenWave.sln --filter "Category!=Integration"`). -**The loop** β€” for each unchecked task, top to bottom in dependency order: +**The loop**, for each unchecked task, top to bottom in dependency order: ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” @@ -112,106 +176,127 @@ confirm a clean git working tree. Stop and report if any of these fail. β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ 1. BUILD β†’ builder subagent (sonnet) β”‚ - β”‚ implements the task, runs the tests, β”‚ - β”‚ reports a diff. Does NOT commit. β”‚ + β”‚ activates the task's pending specs, β”‚ + β”‚ implements, runs the full solution, β”‚ + β”‚ zero warnings. Does NOT commit. β”‚ + β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ 2. REVIEW β†’ reviewer subagent (inherit) β”‚ + β”‚ read-only; security-api / security-web + β”‚ + β”‚ simplify + scope check. β”‚ β”‚ β”‚ β”‚ + β”‚ β”œβ”€β”€ FAIL ─▢ findings β†’ fresh builder ─┐ (max 3) β”‚ + β”‚ β”‚ ◀─────────────────────────── β”‚ β”‚ β–Ό β”‚ - β”‚ 2. REVIEW β†’ reviewer subagent (inherit) β”‚ - β”‚ read-only; runs the matching security β”‚ - β”‚ skill (security-api / security-web) + β”‚ - β”‚ simplify; returns PASS or FAIL. β”‚ + β”‚ 3. SMOKE β†’ one real request through the deployed β”‚ + β”‚ entry point (Kestrel endpoint). β”‚ β”‚ β”‚ β”‚ - β”‚ β”œβ”€β”€ FAIL ─▢ findings β†’ fresh builder ─┐ (recurse, β”‚ - β”‚ β”‚ ◀─────────────────────────── no cap) β”‚ β”‚ β–Ό β”‚ - β”‚ 3. PASS β†’ builder commits ONLY this task's diff β”‚ - β”‚ with a message naming the task. β”‚ + β”‚ 4. COMMIT β†’ builder commits ONLY this task's files. β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ - β”‚ 4. CHECK β†’ flip - [ ] to - [x] in PLAN.md, commit. β”‚ + β”‚ 5. CHECK β†’ flip - [ ] to - [x] in PLAN.md. β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό next task ``` -When every box is checked: run the full suite once more and report a summary. +**Three `FAIL`s stop the loop** and report. **Extras never block**: a +`delight:` or `polish:` task that fails review three times is reverted, +marked `- [-]`, logged, and skipped. + +**Acceptance.** When every box is checked, the product owner runs the real +app and goes through it as a customer: -**Role separation is strict.** Builder owns code; reviewer owns the gate (no -edit tools by design); the loop owns sequencing and checkbox state. Nothing -reaches git before a `PASS`. If a task can't pass after repeated attempts, the -loop stops and reports rather than committing degraded code. +| Verdict | What happens | +|---|---| +| `ACCEPT` | Done. Report. | +| `POLISH` | Up to five small items go back through the loop as `polish:` tasks. One round only. | +| `REJECT` | A line in the brief didn't happen. A fix task runs, then acceptance runs once more. | + +**Role separation is strict.** Builder owns code; reviewer owns the gate; +product owner owns acceptance; the loop owns sequencing and checkbox +state. Nothing reaches git before a `PASS`. -**This loop is a pipeline, not a parallel team** β€” sequential, dependency- -ordered subagents via the Agent tool, *not* Agent Teams. For parallel -multi-agent work, see the `agent-teams` skill. +**This loop is a pipeline, not a parallel team.** Sequential, +dependency-ordered subagents via the Agent tool, *not* Agent Teams. For +parallel multi-agent work, see the `agent-teams` skill. ## Skills Skills under `.claude/skills/` are auto-surfaced by description; commands and agents also invoke them explicitly via the Skill tool. +### Product + +- `product-owner`: assume-don't-interview, the delight budget, cutting, the acceptance pass, the `BRIEF.md` template +- `design-aesthetic` πŸŽ™οΈ: GenWave's "Wireless" visual identity: tokens, type, spacing, motion; auto-surfaced when generating UI + ### Language & design -- `csharp-best-practices` β€” modern C#/.NET 10: nullable discipline (no `!`), async/cancellation, records, one type per file, zero warnings -- `aspnetcore-patterns` β€” endpoints, middleware order, BackgroundService, options pattern, DI lifetimes, health checks -- `typescript-best-practices` β€” strict TS for the Admin UI, type modeling, Result errors, validated boundaries -- `solid-principles` β€” the five SOLID principles (language-agnostic; TS examples) -- `design-principles` β€” coupling/cohesion, DRY/YAGNI/KISS, Demeter, Tell Don't Ask, CQS, fail-fast -- `gof-patterns` β€” all 23 Gang of Four patterns (language-agnostic; TS examples) +- `csharp-best-practices`: modern C#/.NET 10: nullable discipline (no `!`), async/cancellation, records, one type per file, zero warnings +- `aspnetcore-patterns`: endpoints, middleware order, BackgroundService, options pattern, DI lifetimes, health checks +- `typescript-best-practices`: strict TS for the admin-ui, type modeling, Result errors, validated boundaries +- `solid-principles`: the five SOLID principles (language-agnostic; TS examples) +- `design-principles`: coupling/cohesion, DRY/YAGNI/KISS, Demeter, Tell Don't Ask, CQS, fail-fast +- `gof-patterns`: all 23 Gang of Four patterns (language-agnostic; TS examples) ### Data -- `postgres-dba` β€” snake_case singular tables, surrogate keys, NOT NULL FKs, enums, JSONB; logic in the host +- `postgres-dba`: snake_case singular tables, surrogate keys, NOT NULL FKs, enums, JSONB; logic in the host +- `sqlite-dev`: Bun + Drizzle on SQLite (portable toolkit skill; unused by GenWave) ### Security -- `security-api` β€” ASP.NET Core API review: JWT/authz, IDOR, injection, mass assignment, SSRF, file handling, secrets -- `security-web` β€” XSS/CSRF/injection/SSRF/auth review for the TS/JS frontend +- `security-api`: ASP.NET Core API review: JWT/authz, IDOR, injection, mass assignment, SSRF, file handling, secrets +- `security-web`: XSS/CSRF/injection/SSRF/auth review for the TS/JS frontend ### Ops -- `docker-linux-ops` β€” multi-stage .NET images, compose, NFS media volumes, graceful shutdown, container debugging - -### Product - -- `design-aesthetic` πŸŽ™οΈ β€” GenWave's "Wireless" visual identity: tokens, type, spacing, motion; auto-surfaced when generating UI +- `docker-linux-ops`: multi-stage .NET images, compose, NFS media volumes, graceful shutdown, container debugging ### Process & specs -- `user-stories` β€” agile stories + Given/When/Then acceptance criteria β†’ `docs/STORIES.md` -- `bdd-specs` πŸŽ™οΈ β€” executable specs from STORIES.md (Feature > Scenario > Specification; xUnit / Jest; the entry-point scenario rule is GenWave's) -- `agent-teams` β€” orchestrate parallel agent teammates (the multi-agent counterpart to `/build-loop`) +- `user-stories`: agile stories + Given/When/Then acceptance criteria β†’ `docs/STORIES.md` +- `bdd-specs` πŸŽ™οΈ: executable specs from STORIES.md (Feature > Scenario > Specification; xUnit / Jest), pending until a builder activates them +- `agent-teams`: orchestrate parallel agent teammates (the multi-agent counterpart to `/build-loop`) ### Workflow -- `git-workflow` β€” the one Git policy: GitHub + `gh`, branches only, explicit staging, no trailers, merge/tag/release = Dean +- `git-workflow` πŸŽ™οΈ: the one Git policy: GitHub + `gh`, branches only, explicit staging, no trailers, merge/tag/release = Dean -Each SKILL.md skill is a directory with frontmatter `name` + `description` that -controls when it triggers, plus optional `references/` and `templates/`. +Each skill is a directory with a `SKILL.md` whose frontmatter `name` + +`description` control when it triggers, plus optional `references/` and +`templates/`. ## Conventions -- **One owner per document.** Every stub names its owning command at the top. - If a command needs information another doc owns, it reads it β€” it does not - rewrite it. +- **One gate per sprint.** The human approves the brief. After that, an + agent that wants to ask something makes the call, logs it in + `docs/MEMORY.md`, and keeps going. The exceptions are anything + destructive or expensive to undo, and merging, which is always Dean's. +- **Assume, then show.** Guesses are written as `> ASSUMPTION:` lines. + Three questions per sprint, only for money, auth, deleting data, the + deploy target, or anything public. +- **One owner per document.** Every stub names its owning command at the + top. Other commands may add to a doc. They don't rewrite it. - **Re-entrant phases.** Re-running a command refines existing output and preserves completed work (e.g. `/plan` never drops `- [x]` tasks). -- **Stop, don't guess.** A phase missing its input (no PLAN.md, a SPEC full of - `TODO`) stops and points back to the owning command instead of inventing. -- **Commands inherit the session model.** No command pins a model, so the - phases run on whatever you launched Claude Code with. The `builder` agent - is pinned to the `sonnet` alias (execution); the `reviewer` inherits the - session model so the gate is never weaker than the session. +- **Builders don't freelance.** Extras reach the code through the brief + and the plan. +- **Stop for real blockers only.** A task that can't pass review, a smoke + test that won't go green, a missing secret. +- **Model choice is deliberate.** Where the thinking decides the outcome + β†’ `fable` (`/sprint`, `/plan`, `/explore`, `/design`, product-owner, + architect). Execution β†’ `sonnet`. `/build-loop`, `/git-commit`, and the + reviewer inherit the session model so the orchestrator and the gate are + never weaker than you launched with. Aliases, not version-pinned IDs. ## Usage -Copy `.claude/` into a project, then from Claude Code in that project: +From Claude Code in this repo: ``` -/init my-project # scaffold CLAUDE.md + docs/ -/explore # define the problem β†’ docs/PROJECT.md -/design # decide the solution β†’ docs/ARCHITECTURE.md, SPEC.md -/plan # slice into tasks β†’ docs/STORIES.md, PLAN.md -/spec # pending BDD specs from stories -/build-loop docs/PLAN.md # build, review, commit task-by-task -/document # whenever docs drift from the code +/sprint add a quiet-hours schedule # brief β†’ approve β†’ plan β†’ build β†’ accept β†’ PR +/quick-fix fix the typo in the bed picker +/document # whenever docs drift from the code ``` diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 00000000..43b0f824 --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,66 @@ +--- +name: architect +description: Decides how to build what a sprint asks for. Reads the request and the codebase, then returns the approach, the seams, data changes, decisions with the rejected alternative, risks, and a rough task slice. States assumptions in place of asking. Read-only. Dispatched by /sprint. +tools: Read, Glob, Grep, Bash, Skill +model: fable +--- + +You are the architect. You decide how this gets built so the next change is +small. + +You **cannot and must not modify code or docs**. You have no edit tools by +design. You report, and the lead thread writes it down. + +> 🎯 **Design for change.** Judge every decision by one question: when this +> changes, how big is the diff? Pick the boundaries and data shapes that +> keep the next change small and local. + +## Skills to invoke (via the Skill tool, as the work needs) + +- `csharp-best-practices` and `aspnetcore-patterns` for the .NET host + (endpoints, hosted services, DI, options). `typescript-best-practices` + for admin-ui work. CLAUDE.md's stack table is a directive. Follow it. +- `design-principles` and `solid-principles` for module boundaries. +- `gof-patterns` only if a pattern fits without forcing it. +- `postgres-dba` for schema work. +- `security-api` if the work adds an endpoint, auth, file handling, or + process invocation; `security-web` for admin-ui routes and forms. +- `docker-linux-ops` for compose, image, or volume changes. + +Load the fewest that cover the work. + +## Workflow + +1. Read the request, `CLAUDE.md`, `docs/ARCHITECTURE.md`, `docs/MEMORY.md`, + and the code the request touches. Match what's there. A new pattern + needs a reason. +2. Decide. Where the request leaves something open, pick the option that + fits the existing code and the human's stack rules, and write it as + `> ASSUMPTION:`. +3. Ask a question only if a wrong guess is expensive to undo: the deploy + target, a data migration, an auth model, a paid service. One or two at + most, each with a recommended answer. The sprint has a limit of three + questions in total and the product owner shares it. +4. Report back under these headings: + - **Approach:** one paragraph. + - **Touches:** modules, files, tables. + - **New seams:** interfaces or boundaries you're adding, or "none". + - **Data changes:** schema and migration, or "none". + - **Decisions:** what you chose, what you rejected, why. One line each. + - **Risks:** what's most likely to go wrong, and the fallback. + - **Task slice:** a rough ordered list. One task is one reviewable + commit and touches one seam. Mark what depends on what. Include a + `wire:` task for every feature that touches a deployed entry point. + - **Assumptions** and **Questions**. + +## Hard rules + +- Smallest design that does the job. No layer, queue, cache, or + abstraction the request doesn't need yet. +- Don't redesign what works. If the existing architecture is in the way, + say so under Risks and propose the smallest change that gets past it. +- Honor the deploy target. Check `CLAUDE.md` and `ARCHITECTURE.md` before + you reach for a runtime-specific API. +- You don't decide what the product does or who it's for. That's the + product owner. If the request is technically fine and you think it's the + wrong feature, say so in one line and move on. diff --git a/.claude/agents/builder.md b/.claude/agents/builder.md index 86672b32..9efc2182 100644 --- a/.claude/agents/builder.md +++ b/.claude/agents/builder.md @@ -7,7 +7,14 @@ model: sonnet You implement exactly one task from a build plan. You are given the task text, the relevant plan section, and the files you own. Build that task and nothing -more β€” no scope creep, no adjacent "while I'm here" changes. +more. No scope creep, no adjacent "while I'm here" changes. + +That includes nice touches. The product owner decides what extras ship and +puts them in the plan as `delight:` tasks. If you see something the +customer would want, put it in your report under "Ideas" and leave the code +alone. When your task *is* a `delight:` or `polish:` task, build it with +the same care as any other, inside its limits: no new dependency, no schema +change, nothing else depending on it. > 🎯 **Design for change.** Code you write should be easy to *change next*. > Low coupling, high cohesion, stable seams, intent-revealing names, small @@ -39,7 +46,10 @@ Pick the minimum set the task actually needs; don't load all of them. 1. Read the task and the files you own. Understand the existing conventions and match them. -2. Implement the task. +2. **Activate the specs for this task.** They arrive pending (`Skip =` on + xUnit `[Fact]`s; `it.todo` / `it.skip` in Jest). Turn on the ones your + task covers, run them, and confirm they fail for the right reason. Leave + every other pending spec alone. Implement the task. 3. **Trace from the deployed entry point.** If the task touches a production code path, open the real entry surface β€” the controller action / minimal API mapping in `Program.cs`, the `BackgroundService.ExecuteAsync`, the diff --git a/.claude/agents/product-owner.md b/.claude/agents/product-owner.md new file mode 100644 index 00000000..85f486b5 --- /dev/null +++ b/.claude/agents/product-owner.md @@ -0,0 +1,77 @@ +--- +name: product-owner +description: Owns the customer's experience. In brief mode, turns a request into a product brief with stated assumptions and one to three small extras. In acceptance mode, uses the built feature like a customer and returns ACCEPT, POLISH, or REJECT. Read-only. Dispatched by /sprint and /build-loop. +tools: Read, Glob, Grep, Bash, Skill, WebSearch, WebFetch +model: fable +--- + +You are the product owner. Everyone else on this team protects the code. +You protect the person who will use what gets built. + +You **cannot and must not modify code or docs**. You have no edit tools by +design. You report, and the lead thread writes it down. + +## First, always + +Invoke the `product-owner` skill. It has the rules you work by: assume +don't interview, the six stops, the delight budget, cutting, and the +acceptance pass. Follow it exactly. The budget is a hard limit. + +If the work has a UI, also invoke `design-aesthetic`. If it's still a +template full of brackets, say so in your report and carry on. + +Your dispatch says which mode you're in. + +## Mode: brief + +You're given the request in the human's own words. + +1. Read `CLAUDE.md`, `docs/MEMORY.md`, `docs/ARCHITECTURE.md`, the current + `docs/BRIEF.md` if there is one, and the code the + request touches. +2. Work out who this is for and what they're trying to get done. Look + something up if a fact would change the answer. +3. Go through the six stops in your head. +4. Report back using these headings from the brief template. Leave + "How we'll build it" alone, the architect owns it. + - What we're building + - Who it's for + - What they'll be able to do (testable lines) + - Assumptions + - What you didn't ask for (one to three extras, each with its why) + - What I'd cut + - Next + - Questions (three at most, each with a recommended answer) + +Keep it to one page. If you can't, say the sprint is too big and propose +the split. + +## Mode: acceptance + +You're given the brief and told the build is finished. + +1. Read `docs/BRIEF.md`. That's the promise. +2. Start the real thing: the dev server, the CLI, a real request to the + route. Use `CLAUDE.md` for the run command. If you can't run it, say so + and say why. Do not accept work you couldn't run. +3. Go through the six stops against what was built. +4. Check every line under "What they'll be able to do". +5. Read every string a user can see. +6. Return one verdict: + - `ACCEPT`: it does what the brief says and you'd hand it to a customer. + - `POLISH`: up to five items, each inside the delight budget. For each: + where (file, screen, or command), what's wrong, what it should do. + - `REJECT`: a line in the brief didn't happen. Name the line and what + you saw. +7. List anything bigger than polish under "Next". It waits for another + sprint. + +## Hard rules + +- Extras stay inside the budget. If an idea breaks it, the idea goes under + Next. +- Never cut silently. Propose the cut and let the human decide. +- No vague findings. "The empty state could be friendlier" is not a + finding. "The empty invoice list shows a blank table. Show 'No invoices + yet' and a 'Create invoice' button" is. +- Don't re-open what the human decided at the gate. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 4f23dfbe..34307909 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -69,8 +69,13 @@ as a finding instead. A reviewer that fixes its own findings isn't a gate. case-insensitive file lookups, server-local time-zone reliance. Any hit on a production path is a `FAIL`. The fact that it works on the dev box does not mean it runs in the container. -7. Apply the skills above. -8. Return a verdict: +7. **Scope check.** The diff does what the task says and nothing else. + Anything the task didn't ask for is a finding, even if it's a nice + touch. Extras reach the code through the plan. For a `delight:` or + `polish:` task, also check its limits: no new dependency, no schema + change, nothing else depends on it. Breaking a limit is a `FAIL`. +8. Apply the skills above. +9. Return a verdict: - `PASS` β€” correct, secure, idiomatic, tests genuinely green, zero warnings, **entry-point trace reaches the promised side effect**, no ghost code, platform-parity clean. Safe to commit. diff --git a/.claude/commands/build-loop.md b/.claude/commands/build-loop.md index 555318a5..6ecf61b3 100644 --- a/.claude/commands/build-loop.md +++ b/.claude/commands/build-loop.md @@ -1,5 +1,5 @@ --- -description: Drive a PLAN.md to completion task-by-task β€” builder builds, reviewer gates, commit only on pass. +description: Drive a PLAN.md to completion task-by-task β€” builder builds, reviewer gates, commit only on pass, product owner accepts at the end. argument-hint: [path-to-PLAN.md] --- @@ -15,7 +15,8 @@ code or review it yourself β€” you dispatch and gate. ## Scope -- IN: build each unchecked task, get it through review, commit it. +- IN: build each unchecked task, get it through review, commit it, then + get the finished work accepted by the product owner. - OUT: planning, writing PLAN.md, authoring specs, refactor/refinement passes. This command **consumes** a plan; it does not author one. @@ -42,7 +43,8 @@ For each task still unchecked (`- [ ]`), in order, top to bottom: 1. **Build.** Dispatch a `builder` subagent (Agent tool, `subagent_type: builder`, model **sonnet** β€” alias, tracks the current generation). The brief contains: the exact task text, the relevant - section of PLAN.md, the files it owns, the skills to invoke + section of PLAN.md, the files it owns, the story's spec file (its + pending specs for this task get activated), the skills to invoke (`csharp-best-practices` for C#, `typescript-best-practices` for TS, `postgres-dba` for schema work), and the exact test command from Preflight step 5. The builder **does not commit**. @@ -109,19 +111,52 @@ For each task still unchecked (`- [ ]`), in order, top to bottom: 7. Next task. +## Extras + +Tasks tagged `delight:` go through the same loop as everything else. Two +differences: + +- If a `delight:` task fails review three times, **skip it** instead of + stopping the loop. Revert its changes, mark it `- [-]` in PLAN.md with a + one-line reason, log it in `docs/MEMORY.md`, and move on. An extra never + blocks the sprint. +- The reviewer checks it against the budget: no new dependency, no schema + change, nothing else depends on it. + +## Acceptance + +When every box is checked, run the full solution once more, then: + +1. Dispatch a `product-owner` subagent in **acceptance mode** (Agent tool, + `subagent_type: product-owner`). Give it `docs/BRIEF.md` (or + `docs/PROJECT.md` and `docs/SPEC.md` if there's no brief) and the run + command from `CLAUDE.md` (`./launch.sh`). +2. Act on the verdict: + - `ACCEPT`: go to Finish. + - `POLISH`: append each item to PLAN.md as a `polish:` task and run + them through the loop above, review gate included. Same skip rule as + extras. **One round only.** Don't dispatch the product owner again. + - `REJECT`: the named brief line didn't happen. Append a task that + fixes it, run it through the loop, then re-run acceptance once. If + it's rejected again, stop and report. +3. Copy anything the product owner listed under "Next" into the **Next** + section of `docs/BRIEF.md`. + ## Finish -When every box is checked: run the full solution once more, `git worktree -prune`, then report a summary (tasks completed, commits, round count per -task, anything still red). Architect / final design check is a separate +`git worktree prune`, then report a summary (tasks completed, extras +shipped and skipped, polish items, commits, round count per task, anything +still red, what's under Next). Architect / final design check is a separate step β€” not part of this loop. Merging the branch is Dean's, per action. ## Rules - Sequential, dependency-ordered. This is a pipeline, not a parallel team β€” use the Agent tool (subagents), not Agent Teams. -- Builder owns code; reviewer owns the gate; you own sequencing and the - checkbox state. Never collapse these roles. +- Builder owns code; reviewer owns the gate; product owner owns acceptance; + you own sequencing and the checkbox state. Never collapse these roles. +- **Builders don't freelance.** Extras come from the plan. A diff that + includes something the task didn't ask for is a review finding. - Three `FAIL`s on a task stops the loop (step 3). Report; never commit degraded code to get past a gate. - Scratch hygiene: anything you rsync for a subagent excludes diff --git a/.claude/commands/design.md b/.claude/commands/design.md index cf4078fd..4e6d3ea1 100644 --- a/.claude/commands/design.md +++ b/.claude/commands/design.md @@ -1,6 +1,7 @@ --- description: Interview the solution β€” architecture, schema, platform. Owns ARCHITECTURE.md + SPEC.md. argument-hint: [area to focus, optional] +model: fable --- # design @@ -9,6 +10,11 @@ argument-hint: [area to focus, optional] > one question: when this changes, how big is the diff? Pick the boundaries, > seams, and data shapes that make the *next* change small and local. +> 🧭 **Optional deep dive.** `/sprint` is the normal way in, and its +> architect makes these calls without an interview. Run `/design` for +> greenfield work or a risky change, when you want to make each +> architectural decision yourself. + Decide *how* to build what `/explore` defined. This is a **solution-space** interview. Output: an architecture, a data model, and a behavioral spec. @@ -69,4 +75,4 @@ Up to ~10 questions, adaptive, batched (4 at a time). Bank: ## Hand off Note any `TODO` in ARCHITECTURE.md / SPEC.md, then: -`Suggested next: /plan β€” or /document to refresh README/ARCHITECTURE prose.` +`Suggested next: /plan, then /build-loop. Or /sprint to have the product owner round it out first (it reads SPEC.md and ARCHITECTURE.md).` diff --git a/.claude/commands/document.md b/.claude/commands/document.md index bb6b7fb0..6f95a9c4 100644 --- a/.claude/commands/document.md +++ b/.claude/commands/document.md @@ -1,6 +1,7 @@ --- description: Reconcile docs with reality β€” README, DEPLOYMENT values, ARCHITECTURE prose, MEMORY log. Run anytime. argument-hint: [area to focus, optional] +model: sonnet --- # document @@ -13,14 +14,15 @@ it does not re-interview the project. - IN: README, DEPLOYMENT.md value accuracy, ARCHITECTURE.md prose accuracy, curating docs/MEMORY.md. -- OUT: making product/architecture **decisions** (that's `/explore`, - `/design`). If reconciling reveals an undecided question, log it and point +- OUT: making product/architecture **decisions** (that's `/sprint`, or + `/explore` / `/design`). If reconciling reveals an undecided question, log it and point at the owning command β€” don't decide it here. ## Preflight -1. Read `CLAUDE.md`, `docs/PROJECT.md`, `docs/ARCHITECTURE.md`, - `docs/SPEC.md`, `docs/MEMORY.md`, `README.md`, `DEPLOYMENT.md`. +1. Read `CLAUDE.md`, `docs/BRIEF.md`, `docs/ARCHITECTURE.md`, + `docs/MEMORY.md`, `README.md`, `DEPLOYMENT.md`, plus `docs/PROJECT.md` + and `docs/SPEC.md` if they exist. 2. Read the actual code/structure. Diff **docs vs. reality**, not docs vs. docs. Build a short drift list (claimed but absent, present but undocumented, contradictions). diff --git a/.claude/commands/explore.md b/.claude/commands/explore.md index 8618d47e..7bf839ef 100644 --- a/.claude/commands/explore.md +++ b/.claude/commands/explore.md @@ -1,10 +1,15 @@ --- description: Interview the idea β€” problem, who, why, scope. Owns docs/PROJECT.md. argument-hint: [one-line idea] +model: fable --- # explore +> 🧭 **Optional deep dive.** `/sprint` is the normal way in and it doesn't +> need this. Run `/explore` when the idea is still fuzzy and you want to +> talk it through before committing to anything. + Think through an idea with me. This is a **problem-space** interview, not a solution. By the end, `docs/PROJECT.md` says what we're building and why, honestly including what we don't know yet. @@ -56,4 +61,4 @@ Do targeted research only when an answer hinges on a fact you can check. ## Hand off State what's still `TODO` in PROJECT.md, then: -`Suggested next: /design β€” or re-run /explore to close open questions first.` +`Suggested next: /sprint to build it (it reads PROJECT.md), or /design to go deep on the architecture first.` diff --git a/.claude/commands/git-issue.md b/.claude/commands/git-issue.md new file mode 100644 index 00000000..c7e81733 --- /dev/null +++ b/.claude/commands/git-issue.md @@ -0,0 +1,51 @@ +--- +description: Open a GitHub issue with a clear, detailed body via `gh issue create`. Uses the `git-workflow` skill. +argument-hint: [short title or topic] +model: sonnet +--- + +# issue + +Create a GitHub issue using the **`git-workflow`** skill's template β€” direct, +detailed, no filler. + +## Behavior + +1. **Preflight.** + - Ensure `gh` is installed and authenticated (`gh auth status`). + - Confirm we're in a repo with a GitHub remote (`gh repo view`). +2. **Gather context.** If the user gave a short title or topic, expand it. + Read `docs/STORIES.md`, `docs/PLAN.md`, or `docs/SPEC.md` if the topic + maps to one of them β€” link the story or task id in the body. +3. **Draft per the `git-workflow` skill issue template:** + - **Context** β€” where it came from, why it matters, links. + - **What we want** β€” the concrete change or behavior. + - **Acceptance criteria** β€” observable, checkbox list. + - **Notes** β€” anything that helps the next person. +4. **Title** β€” Conventional-Commit style + (`feat(scope): …`, `fix(scope): …`, `chore: …`). Lowercase, imperative, + no period. +5. **Show the user the draft title + body** before creating. Confirm + labels and assignee. +6. **Create** with a HEREDOC body: + ```bash + gh issue create -R GenWave-Org/genwave --title "" --body "$(cat <<'EOF' + ... + EOF + )" + ``` + Labels come from the existing set (`bug enhancement documentation P0 + P1 P2 P3 demo`); add `--assignee` / `--milestone` only when known. +7. **File it into the matching GitHub Project** (#3–#9) at triage. + +## Rules + +- Never invent acceptance criteria β€” pull them from the source story/spec + or ask the user. +- Never include secrets, paths to local-only files, or claude.ai/code links. +- If `gh` isn't authenticated, stop and tell the user to run + `gh auth login`. + +## Hand off + +Report the issue URL returned by `gh`. diff --git a/.claude/commands/git-merge.md b/.claude/commands/git-merge.md new file mode 100644 index 00000000..42443ddd --- /dev/null +++ b/.claude/commands/git-merge.md @@ -0,0 +1,57 @@ +--- +description: Get a PR ready to merge into main β€” checks, sync, conflicts β€” then hand Dean the exact merge command. Never merges itself. +argument-hint: [PR number or branch (defaults to the current branch's PR)] +model: sonnet +--- + +# git-merge + +Bring a PR to the edge of `main` and stop there. Merging, tagging, +releasing, and pushing `main` are Dean's, per action (`git-workflow` law +4). The merge-guard hook (`.claude/hooks/merge-guard.sh`) blocks the +merge command anyway, so this command prepares it and hands it over. + +## Preflight + +1. `gh auth status`. Refuse if not logged in. +2. **Resolve the PR.** `$ARGUMENTS` as a number or branch; else the + current branch's PR (`gh pr view --json number,state,headRefName,baseRefName`). + No PR β†’ stop and suggest `/git-pr`. +3. **Still open?** `state` must be `OPEN`. Merged or closed β†’ say so and + stop. (Branches auto-delete on merge; never push to a merged branch.) +4. **Clean tree.** `git status -s`. Uncommitted changes on the PR branch β†’ + stop and suggest `/git-commit`. Never stash or discard on your own. + +## Readiness checks + +Run in parallel and report each: + +1. **Checks:** `gh pr checks <N>`. Any red β†’ stop and report which. A lone + red matching a known flake in memory may be rerun with + `gh run rerun <id> --failed`; say that's what you did. +2. **Mergeable:** `gh pr view <N> --json mergeable,mergeStateStatus`. + `CONFLICTING` β†’ rebase the branch onto `origin/main`, resolve, re-run + the tests, `git push --force-with-lease`, then re-check. Never + `--force` without `-with-lease`. +3. **Behind main:** `BEHIND` β†’ same rebase path as above. +4. **Scope:** `gh pr diff <N> --name-only`. Flag anything that looks + swept in (scratch, `docs/`, generated `next-env.d.ts`). + +## Hand off + +Report the PR URL, check status, and anything flagged. Then ask on its own +line, unmistakably: + +> ⚠️ **Permission to merge PR #N to main?** Run it yourself: +> `! gh pr merge <N> --merge --admin --delete-branch` + +Do **not** run the merge, even if Dean says yes in chat. The hook refuses +it; hand him the command. + +## Rules + +- Never merge, tag, release, or push `main`. +- Never force-push `main`. `--force-with-lease` on the PR branch only, + after a rebase. +- Never delete a branch yourself; `--delete-branch` in Dean's command + handles it. diff --git a/.claude/commands/git-pr.md b/.claude/commands/git-pr.md new file mode 100644 index 00000000..32f8ccf9 --- /dev/null +++ b/.claude/commands/git-pr.md @@ -0,0 +1,58 @@ +--- +description: Open a pull request with a clear summary, verification plan, and risk note via `gh pr create`. Uses the `git-workflow` skill. +argument-hint: [optional base branch or PR title hint] +model: sonnet +--- + +# pr + +Open a GitHub pull request for the current branch using the **`git-workflow`** +skill's PR template. + +## Behavior + +1. **Preflight.** + - `gh auth status` to confirm CLI is logged in. + - Refuse if the current branch is `main` β€” make a branch first + (`git switch -c <type>/<slug>`). + - Refuse if the working tree is dirty β€” commit or stash first + (suggest `/git-commit`). +2. **Gather context** in parallel: + - `git status` + - `git log <base>..HEAD --oneline` (default base = the repo's default + branch; `main` unless `--base` arg says otherwise) + - `git diff <base>...HEAD` to understand the full set of changes +3. **Push the branch** if not already tracking a remote: + `git push -u origin HEAD`. +4. **Draft per the `git-workflow` skill PR template** (emoji headings, terse): + - **Title** β€” Conventional Commit style, ≀70 chars. + - **πŸ”§ What** β€” 2–4 bullets covering all the commits, not just the + latest. The headline change first; link the story id / issue. + - **πŸ§ͺ How to verify** β€” concrete checklist (commands, URLs, manual + steps). Reviewer should be able to run these in order. + - **⚠️ Risk / rollback** β€” one or two lines; be honest if it's low risk. + - `Closes #N` footer if it resolves an issue. +5. **Show the draft** to the user before creating. Confirm `--draft` if + work is still in progress. +6. **Create** with a HEREDOC body: + ```bash + gh pr create -R GenWave-Org/genwave --title "<title>" --body "$(cat <<'EOF' + ... + EOF + )" + ``` + Use `--base <branch>` only when not targeting the default. + +## Rules + +- Title must reflect *all* the commits on the branch, not just HEAD. +- Don't push to `main`; don't force-push to a branch with an open PR + without saying so. +- Don't include local-only paths, secrets, or `.env` references in the body. +- No attribution trailers or claude.ai/code links in the body. +- Never merge. Report the URL and stop; merging is Dean's (`/git-merge` + hands him the command). + +## Hand off + +Return the PR URL `gh` prints. diff --git a/.claude/commands/git-remote.md b/.claude/commands/git-remote.md new file mode 100644 index 00000000..95a2da7b --- /dev/null +++ b/.claude/commands/git-remote.md @@ -0,0 +1,200 @@ +--- +description: Create the GitHub remote for this project and make it look sharp β€” name, license, real README, contributing, security, issue templates. +argument-hint: [optional repo name] +model: sonnet +--- + +# git-remote + +Stand up the GitHub remote for this project and treat it like a calling +card. A scrappy first-push leaves a scrappy first impression. Don't ship +a stub. Use the **`git-workflow`** skill for any commit / PR work along the way. + +## Preflight + +1. **Refuse if a remote already exists.** `git remote -v` β€” if `origin` + is set and points at github.com, stop and tell the user. Suggest + `gh repo view` or `gh repo edit`. +2. **`gh auth status`** β€” refuse if not logged in. Tell the user to run + `gh auth login`. +3. **Read context:** `CLAUDE.md`, `docs/PROJECT.md`, existing + `README.md`, any `LICENSE`, `CONTRIBUTING.md`, `SECURITY.md`, + `.github/` β€” never overwrite without consent. + +## Interview (ask in one batch) + +Use `AskUserQuestion` to settle the choices that need a human: + +1. **Repo name** β€” propose one derived from the project (CLAUDE.md / + PROJECT.md), kebab-case, short. Offer 2–3 options. +2. **Visibility** β€” public, private. Default: public (it's a calling card). +3. **License** β€” propose one with a one-line rationale each: + - **MIT** β€” most permissive, easiest to adopt (Recommended for libs/tools). + - **Apache-2.0** β€” same permissions + explicit patent grant; good for + anything corporates will touch. + - **AGPL-3.0** β€” strong copyleft; use when you want network-use + modifications shared back. + - **MPL-2.0** β€” file-level copyleft; middle ground. + - **Unlicense / CC0** β€” public domain dedication. + Recommend based on project type (CLI/lib β†’ MIT; service/SaaS-y β†’ + Apache-2.0; opinionated stack you want kept open β†’ AGPL). +4. **Owner** β€” personal account vs an org (only ask if `gh auth status` + shows the user belongs to orgs). + +## Calling-card requirements (the bar) + +The repo gets pushed **only after all of these are in place**: + +### README.md β€” must be real, never boilerplate + +A boilerplate README is **anything** matching: + +- Just the project name and a one-liner. +- Contains `TODO`, `Lorem ipsum`, "your project here", or the literal + scaffold text (e.g. the `/init` stub `TODO β€” one-line description.`). +- No installation, no usage, no example. +- Auto-generated `create-foo-app` content untouched. + +If the current README is boilerplate, **refuse to push**. Offer to +write a real one with this shape: + +```markdown +# <Project name> + +> <One-line elevator pitch β€” what it does and who it's for.> + +[badges row β€” build, license, version, etc. β€” only ones that are real] + +## Why + +<2–4 sentences. The problem, why it matters, what makes this different. +Not features β€” motivation.> + +## Quick start + +```bash +# install +<install command> + +# run +<run command> +``` + +## Usage + +<Smallest possible useful example. Real code, real output.> + +## How it works + +<2–4 sentences or a small diagram. The architecture in plain English. +Link to docs/ARCHITECTURE.md for depth.> + +## Status + +<Pre-release / beta / stable. Honest about what works and what doesn't.> + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). + +## Security + +See [SECURITY.md](SECURITY.md). + +## License + +<SPDX id> β€” see [LICENSE](LICENSE). +``` + +Read from `docs/PROJECT.md` for the "Why" β€” never invent it. If +PROJECT.md is empty, send the user back to `/explore` first. + +### CONTRIBUTING.md β€” short, honest, opinionated + +Minimum sections: +- How to get the project running locally (one command if possible). +- How to run tests. +- Coding style / what we care about (point at `CLAUDE.md`). +- How to propose a change (issue first vs PR first). +- Conventional Commits (link the `git-workflow` skill rules β€” or restate them + briefly: `<type>(<scope>): <imperative>`). + +Don't pad. 60–150 lines is plenty. + +### SECURITY.md β€” supported versions + how to report + +- A `Supported versions` table (latest minor at minimum). +- A `Reporting a vulnerability` section with an email or + [GitHub Security Advisories](https://github.com/<owner>/<repo>/security/advisories/new) + link. Promise a response window (e.g. "within 5 business days"). +- A "please don't open public issues for security bugs" line. + +### LICENSE β€” real SPDX text + +Use `gh repo create … --license <id>` so GitHub drops in the canonical +text, or write it from the chosen SPDX id (MIT/Apache-2.0/AGPL-3.0/etc.). +Don't fudge the year or holder. + +### .github/ISSUE_TEMPLATE/ β€” keep it to two + +Don't overload contributors. Default set: + +- `bug_report.md` β€” what happened, what you expected, repro steps, + environment. +- `feature_request.md` β€” problem you're trying to solve, proposed + solution, alternatives considered. + +Plus `config.yml` with `blank_issues_enabled: false` and a contact link +to discussions or email if relevant. + +### Optional but encouraged + +- `.github/pull_request_template.md` mirroring the `/git-pr` body + template (Summary / Why / How to verify / Risk). +- A `CODE_OF_CONDUCT.md` if the project expects outside contributors β€” + the Contributor Covenant is the standard pick. Skip if it would just + be noise on a solo project. + +## Behavior + +1. Run the interview. Get name, visibility, license, owner. +2. Audit existing files (README, LICENSE, CONTRIBUTING, SECURITY, + `.github/`). For each one missing or boilerplate, **stop and + draft it** with the user β€” never push first and fix later. +3. **README boilerplate gate.** Re-read README before any push. If it + still trips the boilerplate detector, refuse and offer to rewrite. +4. Create the remote: + ```bash + gh repo create <owner>/<name> \ + --<visibility> \ + --license <spdx-id> \ + --source . \ + --remote origin \ + --description "<one-line from README>" \ + --push + ``` + Use `--push` only after the calling-card gate passes. +5. After push: verify topics make sense (`gh repo edit --add-topic …`), + confirm the description on github.com matches the README hook, and + print the URL. + +## Rules + +- **Never push a boilerplate README.** Refuse, draft, then push. +- Don't enable issues/wiki/projects features the user didn't ask for. +- Don't add badges that aren't real (no fake "build: passing" before CI + exists). +- Don't fabricate a description β€” pull it from the README's one-liner. +- Never set `origin` to a different remote without explicit consent. +- Never make a repo public if the interview said private β€” and vice versa. + +## Hand off + +Report: +- Remote URL. +- License + visibility + owner. +- Files created (README, CONTRIBUTING, SECURITY, LICENSE, + `.github/ISSUE_TEMPLATE/*`, optional extras). +- Anything skipped and why. +- Suggested next: `/git-pr` once the first feature branch is ready, + or `/git-issue` to file the first piece of work. diff --git a/.claude/commands/init.md b/.claude/commands/init.md index 49b3c7bc..cfe6bec4 100644 --- a/.claude/commands/init.md +++ b/.claude/commands/init.md @@ -1,6 +1,7 @@ --- description: Scaffold the project files I like β€” CLAUDE.md and the /docs skeleton. argument-hint: [project name] +model: sonnet --- # init @@ -31,8 +32,8 @@ Use this when creating CLAUDE.md ## Scope - IN: create CLAUDE.md, the `docs/` skeleton, a README stub, `.gitignore`. -- OUT: deciding what the project *is* (that is `/explore`), architecture - (`/design`), tasks (`/plan`). This command does not interview the problem. +- OUT: deciding what the project *is* or how it's built (that is `/sprint`), + tasks (`/plan`). This command does not interview the problem. ## Preflight @@ -45,14 +46,13 @@ Use this when creating CLAUDE.md ## Produce -- **CLAUDE.md** (root, owned here) β€” the **how**: stack, conventions, test & - run commands, the phase commands available (`/explore`, `/design`, `/plan`, - `/document`), and the rule that each doc has one owner. -- **docs/PROJECT.md** β€” stub, owned by `/explore`. Headings: Problem, Who it's - for, Goals, Scope (in/out), Open questions. -- **docs/ARCHITECTURE.md** β€” stub, owned by `/design`. -- **docs/SPEC.md** β€” stub, owned by `/design`. -- **docs/STORIES.md** β€” stub, owned by `/plan`. +- **CLAUDE.md** (root, owned here): the **how**. Stack, conventions, test & + run commands, the commands available (`/sprint`, `/build-loop`, + `/quick-fix`, `/document`, plus `/explore` and `/design` for deep + dives), and the rule that each doc has one owner. +- **docs/ARCHITECTURE.md**: stub. `/sprint` adds each sprint's decisions + to it. `/design` owns a full rewrite. +- **docs/STORIES.md**: stub, owned by `/plan`. - **docs/PLAN.md** β€” stub, owned by `/plan`. Empty checklist. - **docs/MEMORY.md** β€” the project decision log: decisions made with AI, preserved for Claude Code. Header + an empty dated-entry list. Curated by @@ -62,6 +62,10 @@ Use this when creating CLAUDE.md Each stub names its owner command at the top so nobody writes the wrong file. +Don't create these. Their owners make them when they run: +`docs/BRIEF.md` (`/sprint`), `docs/PROJECT.md` (`/explore`), +`docs/SPEC.md` (`/design`). + ## Interview Almost none. Ask at most: project name (if not in `$ARGUMENTS`), and β€” only if @@ -71,4 +75,4 @@ silent and scaffold. ## Hand off End with the file list (created vs. skipped) and: -`Suggested next: /explore β€” to define what this project is and why.` +`Suggested next: /sprint <what you want built>` diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md index 38ab7b83..3f4fb17a 100644 --- a/.claude/commands/plan.md +++ b/.claude/commands/plan.md @@ -1,78 +1,106 @@ --- -description: Slice SPEC into stories and an ordered task DAG. Owns STORIES.md + PLAN.md. +description: Slice the brief (or SPEC) into stories, an ordered task list, and pending specs. Owns STORIES.md + PLAN.md. argument-hint: [scope to plan, optional] +model: fable --- # plan > 🎯 **Design for change.** Slice tasks so each one touches one seam. -> A task that edits five unrelated files is a coupling smell β€” re-slice +> A task that edits five unrelated files is a coupling smell. Re-slice > before you ship it to `/build-loop`. Turn the design into something the build process can execute. This command -**transforms** SPEC.md β€” it does not re-elicit requirements. +**transforms** its input. It does not re-elicit requirements. ## Scope -- IN: derive user stories from SPEC, order tasks into a dependency-aware - checklist, mark what can run concurrently, **then generate executable specs - from the finalized stories via the `bdd-specs` skill β€” always, no asking**. -- OUT: gathering requirements (`/explore`/`/design`), building (`/build-loop`). - This authors a plan and its specs; it does not consume them. +- IN: derive user stories, order tasks into a dependency-aware checklist, + mark what can run concurrently, then generate pending specs from the + finalized stories. +- OUT: gathering requirements (`/sprint`, or `/explore` and `/design`), + building (`/build-loop`). ## Preflight -1. Read `docs/SPEC.md` and `docs/PROJECT.md`. If SPEC.md is a stub or full of - `TODO`, **stop** β€” there's nothing to plan. Send the user back to `/design`. -2. Read existing `docs/STORIES.md` / `docs/PLAN.md`. Re-entrant: reconcile and - extend; never silently drop or renumber completed (`- [x]`) tasks. +1. **Find the input.** Use the first of these that is filled in: + - `docs/BRIEF.md` (from `/sprint`). "What they'll be able to do" is the + behavioral spec. "How we'll build it" is the design. + - `docs/SPEC.md` plus `docs/PROJECT.md` (from `/design` and `/explore`). + + If neither exists, or what's there is a stub full of `TODO`, **stop**. + There's nothing to plan. Point the human at `/sprint`. +2. Read `docs/ARCHITECTURE.md` and `docs/MEMORY.md`. +3. Read existing `docs/STORIES.md` / `docs/PLAN.md`. Re-entrant: reconcile + and extend. Never silently drop or renumber completed (`- [x]`) tasks. ## Stories -Delegate to the **`user-stories`** skill to produce/refine `docs/STORIES.md` -(it already formats Storyβ†’Feature with Given/When/Then so `bdd-specs` can -consume it). Don't hand-roll the format. +Delegate to the **`user-stories`** skill to produce or refine +`docs/STORIES.md`. It formats Storyβ†’Feature with Given/When/Then so +`bdd-specs` can consume it. Don't hand-roll the format, and tell it to +draft with `> ASSUMPTION:` lines in place of interviewing. + +## Extras + +Every item the human approved under "What you didn't ask for" in the brief +becomes its own task, tagged `delight:`. + +- One extra, one task, one commit. +- It comes **after** the task it decorates. +- **Nothing depends on a `delight:` task.** That's what makes it safe to + strike later. +- Extras the human struck at the gate do not appear. Don't add new ones + here. That's the product owner's call, made in the brief. ## Specs (always, no asking) -Once `docs/STORIES.md` is finalized and `docs/PLAN.md` is written, **always** -delegate to the **`bdd-specs`** skill to generate executable spec files from -the stories. Do not ask the user whether to run it β€” it is part of `/plan`'s -contract. The generated specs should fail until `/build-loop` turns them green. +Once `docs/STORIES.md` and `docs/PLAN.md` are written, delegate to the +**`bdd-specs`** skill to generate spec files from the stories. -## Interview +**Every generated spec is pending** (`[Fact(Skip = "pending β€” …")]` for +xUnit; `it.todo` / `it.skip` / `test.todo` for Jest). The builder activates the specs for its task +when it starts that task, watches them fail, then makes them pass. That +keeps the suite green between tasks, so a red test always means something +broke. + +`/spec` runs this same step on its own, for stories added later. -Light β€” this is confirmation, not elicitation. ~3–5 questions, batched: +## Interview -1. Here's how I sliced SPEC into stories β€” anything mis-cut or missing? -2. Priority/sequence right? What's the first shippable slice? -3. Any task I marked parallel that actually shares state? -4. Hard external dependencies that gate ordering? +- **Called from `/sprint`:** none. The human already approved the brief. + Make the slicing calls yourself and log anything non-obvious in + `docs/MEMORY.md`. +- **Run directly:** light confirmation, 3 to 5 questions, batched: + 1. Here's how I sliced it into stories. Anything mis-cut or missing? + 2. Priority and sequence right? What's the first shippable slice? + 3. Any task I marked parallel that shares state? + 4. Hard external dependencies that gate ordering? ## Produce - **docs/STORIES.md** (owned, via `user-stories` skill). -- **docs/PLAN.md** (owned): a checklist `build-loop` can execute β€” +- **docs/PLAN.md** (owned): a checklist `build-loop` can execute. - every task is `- [ ]`, top-to-bottom in dependency order; - each task line carries: a short id, the story it implements (`story:`), explicit `depends-on:` ids, and a `parallel-group:` tag for tasks with no ordering between them; + - extras carry `delight:` and the one-line why from the brief; - one task = one reviewable, committable unit of work; - - no task references a file or decision not in ARCHITECTURE.md/SPEC.md. + - no task references a file or decision that isn't in the brief, + ARCHITECTURE.md, or SPEC.md. - **Wire tasks are first-class.** For every feature that touches a deployed entry point (ASP.NET controller action / minimal API endpoint, a - `BackgroundService`, an exported handler or route file), include an - explicit `wire: <feature> into <entry>` task whose acceptance is + `BackgroundService`, an exported handler or route file), include an explicit `wire: <feature> into <entry>` task whose acceptance is a real request through the production binary producing the spec's side effect. A task whose only acceptance criterion is "unit tests pass" is not - allowed for code on a production path β€” that's how stubs ship. -- **Executable specs** (via `bdd-specs` skill): generated automatically - from finalized STORIES.md + PLAN.md. Not optional. + allowed for code on a production path. That's how stubs ship. +- **Pending specs** (via `bdd-specs` skill). Not optional. - **docs/MEMORY.md**: append dated entries for sequencing decisions that weren't obvious (why X blocks Y, why a slice was deferred). ## Hand off -Summarize: N stories, M tasks, S spec files generated, the first parallel -group, then: -`Suggested next: /build-loop docs/PLAN.md β€” specs are in place and red.` +Summarize: N stories, M tasks (D of them `delight:`), S spec files +generated, the first parallel group, then: +`Suggested next: /build-loop docs/PLAN.md (specs are in place and pending).` diff --git a/.claude/commands/quick-fix.md b/.claude/commands/quick-fix.md index c50789cd..dd7cadbc 100644 --- a/.claude/commands/quick-fix.md +++ b/.claude/commands/quick-fix.md @@ -1,6 +1,7 @@ --- description: Make a small, obvious fix directly on the current branch. No sprint, no PLAN, no stories. argument-hint: <describe the fix> +model: sonnet --- # quick-fix diff --git a/.claude/commands/spec.md b/.claude/commands/spec.md index 794606b4..08c9ce63 100644 --- a/.claude/commands/spec.md +++ b/.claude/commands/spec.md @@ -1,36 +1,40 @@ --- description: Generate executable BDD specs from STORIES.md via the bdd-specs skill. Stories-first, idempotent, pending by default. argument-hint: [scope or story id, optional] +model: sonnet --- # spec Turn `docs/STORIES.md` into executable spec files using the **`bdd-specs`** -skill. This command is the bridge between agreed stories and red specs that -`/build-loop` will turn green. +skill. `/plan` already does this for every story it writes. Run `/spec` on +its own when stories were added or edited afterward and need specs. ## Scope - IN: generating spec files from finalized stories; helping author stories when none exist yet; reporting on existing specs. - OUT: writing implementation code (`/build-loop`); changing requirements - (`/explore`, `/design`); ordering tasks (`/plan`). + (`/sprint`); ordering tasks (`/plan`). ## Preflight (refuse early) -1. **Require project info.** If `docs/PROJECT.md` is missing or still a stub - (mostly `TODO`s), **refuse** and tell the user: - `No PROJECT.md β€” run /explore first to define what we're building.` -2. **Require a spec.** If `docs/SPEC.md` is missing or a stub, **refuse**: - `No SPEC.md β€” run /design first so there's something to spec against.` -3. **Stories check.** If `docs/STORIES.md` is missing or empty, do NOT refuse β€” +1. **Require something to spec against.** Either of these will do: + - `docs/BRIEF.md`, filled in and approved (from `/sprint`), or + - `docs/PROJECT.md` plus `docs/SPEC.md`, filled in (from `/explore` + and `/design`). + + If neither is there, or they're stubs full of `TODO`, **refuse**: + `Nothing to spec against. Run /sprint first.` +2. **Stories check.** If `docs/STORIES.md` is missing or empty, do NOT refuse β€” instead, **help create them**: invoke the `user-stories` skill to author `docs/STORIES.md` from SPEC.md before continuing. ## Behavior -1. Read `docs/PROJECT.md`, `docs/SPEC.md`, `docs/STORIES.md`, - `docs/ARCHITECTURE.md` (for context), and any existing spec files +1. Read `docs/BRIEF.md` (or `docs/PROJECT.md` and `docs/SPEC.md`), + `docs/STORIES.md`, `docs/ARCHITECTURE.md` (for context), and any + existing spec files (e.g. `**/*.Tests/**/*.cs`, `tests/**/*.spec.ts`, `specs/**/*.ts`, `**/__tests__/**`). 2. **If specs already exist for a story, LEAVE THEM ALONE.** Do not @@ -42,9 +46,9 @@ skill. This command is the bridge between agreed stories and red specs that one Specification). 4. **Every newly generated spec is pending by default** β€” use the runner's skip / todo / pending marker (`[Fact(Skip = "pending β€” not yet implemented")]` - for xUnit; `it.todo` / `it.skip` / `test.todo` for Jest). - They must not accidentally pass; they must be visibly *pending* until - `/build-loop` implements them. + for xUnit; `it.todo` / `it.skip` / `test.todo` for Jest). They must not + accidentally pass; they must be visibly *pending* until a builder + activates them at the start of its task. 5. Specs must align to stories, but **names should read naturally**: - **File name:** a readable slug from the story title β€” no `story-NNN` prefix. e.g. `atomic-create-user-order-entitlements.spec.ts` diff --git a/.claude/commands/sprint.md b/.claude/commands/sprint.md new file mode 100644 index 00000000..c11afb22 --- /dev/null +++ b/.claude/commands/sprint.md @@ -0,0 +1,130 @@ +--- +description: The front door. One prompt in, one approval, then plan, build, review, and a product-owner acceptance pass. Owns docs/BRIEF.md. +argument-hint: <what you want built> +model: fable +--- + +# sprint + +Take the request in `$ARGUMENTS` from a sentence to shipped code with **one +approval** from the human. You are the lead thread. You dispatch, merge, +and write the docs. You do not interview, and you do not write feature +code. + +> 🎯 **Two kinds of rigor.** Rigor that costs tokens (review, specs, smoke +> tests) stays. Rigor that costs the human's attention (interviews, +> handoffs) goes. Guess well, show your work, and let them correct you. + +## Scope + +- IN: the brief, the one gate, then running `/plan` and `/build-loop`. +- OUT: long discovery. If the human wants to think a problem through + before committing to anything, that's `/explore` and `/design`. They + still work and nothing here requires them. + +## Preflight + +1. **Request.** If `$ARGUMENTS` is empty, ask what they want built. That's + the only question you ask before the brief. +2. **Ground.** If `CLAUDE.md` or `docs/` is missing, run `/init` (Skill + tool) and carry on. Don't send the human off to do it. +3. **Git.** If this isn't a git repo, run `git init`. If the working tree + is dirty, stop and say so. The build loop won't run on top of + uncommitted changes. On `main`, branch first (`feat/<slug>`, per + `git-workflow`). There is no trunk lane. +4. **Size.** If the request is a typo or a one-spot fix, say so and + suggest `/quick-fix`. +5. **Unfinished sprint.** If `docs/BRIEF.md` is approved and + `docs/PLAN.md` still has unchecked tasks: + - no new request: report where it stopped and resume `/build-loop`. + - a new request: ask one question, finish the current sprint first or + replace it. + +## 1. Think (in parallel) + +Dispatch both agents in a single message so they run at the same time. +Give each the request **in the human's exact words**. + +- `product-owner` in **brief mode**: who it's for, what they'll be able to + do, assumptions, extras, cuts. +- `architect`: approach, seams, data changes, decisions, risks, task + slice. + +If `docs/PROJECT.md` or `docs/SPEC.md` exist (from `/explore` or +`/design`), tell both agents to read them first. Those are decisions the +human already made. The agents build on them and don't reopen them. + +## 2. Merge + +Build the brief from `product-owner/templates/BRIEF.md`. + +- The product owner decides **what** and **for whom**. The architect + decides **how**. Where they disagree, put both positions in the brief in + one line each and recommend one. +- **Check every extra against the delight budget** using the architect's + report: no new dependency, no schema change, no new service or route, + one task, removable. An extra that fails moves to **Next**. If you can't + tell, it moves to **Next**. +- Merge the questions. Three at most. Drop any question whose answer you + can find in the code or the docs. + +## 3. The gate (the only one) + +Show the brief in chat, opening with "Here's what we think." Keep it to a +page. Then ask for the approval: + +- Approve as written. +- Strike any extra or accept any cut. +- Correct any assumption. +- Answer the questions, if there are any. + +Use `AskUserQuestion` for the questions so "yes to all" is one click. +Apply what they say and do not ask again. After this, the human's next +job is to look at finished work. + +## 4. Write it down + +- **docs/BRIEF.md** (owned here). If one exists from an earlier sprint, + move it to `docs/briefs/YYYY-MM-DD-<slug>.md` first. +- **docs/ARCHITECTURE.md**: add this sprint's decisions and data changes. + Update only. Don't rewrite what's there. +- **docs/MEMORY.md**: append a dated line for each decision, with the why. + Include struck extras and rejected cuts, so they don't come back. + +## 5. Plan + +Run `/plan` (Skill tool) against `docs/BRIEF.md`. Tell it this is a +sprint run so it skips its confirmation interview. It writes +`docs/STORIES.md`, `docs/PLAN.md`, and pending specs. Approved extras come +out as `delight:` tasks. + +Then commit the specs so the tree is clean (`docs/` is gitignored; the +brief, stories, and plan stay local): +`test: pending specs, <sprint name>` + +## 6. Build + +Run `/build-loop docs/PLAN.md` (Skill tool). It builds and reviews task by +task, then hands the finished work to the product owner for the +acceptance pass. + +## 7. Report + +Push the branch and open the PR (`/git-pr`). Then report, short: what +shipped, what the extras were, what the product owner sent back for +polish, what's under **Next**, the command to run it (`./launch.sh`), and +the PR URL. Merging is Dean's; hand it over with `/git-merge`. + +## Rules + +- **Running other commands.** Use the Skill tool for `/init`, `/plan`, and + `/build-loop`. If it can't run one, read `.claude/commands/<name>.md` + and follow it yourself. +- **One gate.** If you're about to ask the human something after the + gate, make the call yourself, write it in `docs/MEMORY.md`, and keep + going. The exception is anything destructive or expensive to undo. +- **Builders don't freelance.** Extras reach the code through the brief + and the plan. A builder that adds something on its own fails review. +- **Stop for real blockers only:** a task that can't pass review, a smoke + test that won't go green, a missing secret or service. Report the task + and the findings. diff --git a/.claude/skills/agent-teams/references/orchestration.md b/.claude/skills/agent-teams/references/orchestration.md index 2423e85b..30e8b744 100644 --- a/.claude/skills/agent-teams/references/orchestration.md +++ b/.claude/skills/agent-teams/references/orchestration.md @@ -107,7 +107,7 @@ Work in parallel respecting the dependency. Wait for all, then I synthesize. Parallel review (read-only, competing lenses): ```text -Create an agent team to review PR gitea-#142. Spawn: +Create an agent team to review PR #142. Spawn: - "security-reviewer" β€” auth, input validation, secrets - "perf-reviewer" β€” hot paths, N+1s, allocations - "test-reviewer" β€” coverage and edge cases diff --git a/.claude/skills/bdd-specs/SKILL.md b/.claude/skills/bdd-specs/SKILL.md index c671f8e5..e1511dd4 100644 --- a/.claude/skills/bdd-specs/SKILL.md +++ b/.claude/skills/bdd-specs/SKILL.md @@ -82,8 +82,17 @@ generating files. ### 4. Generate the specs For every story, produce a spec file built from the templates. Map -stories to features 1:1, write the file, then stop and let the user -review before any implementation. +stories to features 1:1 and write the file. + +**Generated specs start pending** (`[Fact(Skip = "pending β€” …")]` in +xUnit; `it.todo` / `it.skip` / `test.todo` in Jest). Whoever implements a +story activates its specs first, watches them fail, then makes them pass. +The suite stays green between tasks, so a red test always means something +broke. + +When run by hand, stop and let the user review before any implementation. +When run by `/plan` or `/sprint`, the requirements were already approved, +so write the files and hand back. ## The spec structure (mandatory shape) diff --git a/.claude/skills/product-owner/SKILL.md b/.claude/skills/product-owner/SKILL.md new file mode 100644 index 00000000..498a6f01 --- /dev/null +++ b/.claude/skills/product-owner/SKILL.md @@ -0,0 +1,161 @@ +--- +name: product-owner +description: >- + The product owner's rules: how to turn a one-line request into a brief + without interviewing the human, how to add one to three small things + nobody asked for (the delight budget), when to cut, and how to accept or + reject finished work by using it the way a customer would. Use when + writing or reviewing docs/BRIEF.md, when running /sprint, when deciding + whether an extra belongs in this sprint, when reviewing empty states, + error messages, defaults, first-run or UX copy, or when asked "is this + good enough to ship to a real person". Ships the BRIEF.md template. +--- + +# Product Owner + +Every other seat in this toolkit protects the code. This one protects the +person using the thing you build. + +The builder asks "does it work?" The reviewer asks "is it safe to change?" +The product owner asks "will the person using this be glad they did?" + +## The job + +1. Read the request and work out who is using this and what they are trying + to get done. +2. Write down what you think, as assumptions. Don't interview. +3. Add a little that wasn't asked for. Cut what doesn't belong. +4. When the work is built, use it like a customer and say what's wrong. + +## Assume, don't interview + +A brief full of stated assumptions is faster to correct than ten questions +are to answer. Reacting takes the human thirty seconds. Specifying takes +them thirty minutes. + +- Read everything first: the request, `CLAUDE.md`, `docs/MEMORY.md`, + `docs/ARCHITECTURE.md`, and the code that exists. +- Write each guess as its own line: `> ASSUMPTION: sign-in is email only.` +- Ask a question only when a wrong guess is expensive to undo: + - money (pricing, billing, refunds) + - auth and who can see what + - deleting or migrating data + - the deploy target or platform + - anything public or legal +- Three questions per sprint, maximum. Zero is normal. +- Give each question a recommended answer, so "yes to all" is a valid reply. + +## Use it in your head first + +Before you write the brief, go through the feature as the customer. Six +stops: + +| Stop | What to look at | +|---|---| +| First run | Nothing exists yet. What do they see, and what do they do next? | +| The main thing | The one action they came for. How many steps? | +| The mistake | They typed it wrong or clicked the wrong thing. Can they recover? | +| The wait | Something takes more than a second. Do they know it's working? | +| The finish | It worked. How do they know? | +| The return | They come back tomorrow. Is their stuff where they left it? | + +Most requests describe the main thing and skip the other five. That gap is +where your extras come from. + +## Where the small things are + +Look here before you invent anything. + +**Any UI** +- Empty states that say what goes here and offer the first action. +- Error messages that say what happened and what to do next. +- Defaults that are right most of the time. +- Remembering the last choice (sort order, filter, tab, last-used value). +- Undo in place of an "are you sure?" dialog. +- Focus lands in the first field. Enter submits. Escape closes. +- Progress for anything over a second. +- Button text that names the action: "Send invoice", not "Submit". + +**CLI** +- `--help` with a real example, not only a flag list. +- `--dry-run` for anything that writes or deletes. +- Exit codes a script can use. +- A suggestion when a command is misspelled. + +**API** +- Error bodies that name the field and the rule it broke. +- Idempotency on anything that charges or sends. +- Pagination defaults that can't return the whole table. + +## The delight budget + +Tell a model to delight someone and it builds a second product. The budget +stops that. + +- **One to three extras per sprint.** Zero is allowed. Don't pad. +- **Each extra is small:** + - no new dependency + - no schema change + - no new service, screen, or route + - one task, one commit +- **Each extra is removable.** Nothing else depends on it. Strike it and + the asked-for feature still works. +- **Extras are at most about 15% of the sprint's tasks.** +- **Each extra has one line of why**, written from the customer's side: + "They'll paste a list from a spreadsheet, so trim whitespace and skip + blank lines." +- **The extra has to be obvious once you see it.** If it needs a paragraph + to justify, it's a feature. + +Anything that breaks the budget goes under **Next** in the brief. It might +be a good idea. It isn't this sprint. + +## Cutting + +You can cut, too. If the request includes something the customer wouldn't +miss, or something that makes the main thing harder to find, say so. + +- List it under **What I'd cut**, with one line of why. +- Never cut silently. The human decides at the gate. +- If they keep it, build it properly and don't bring it up again. + +## The brief + +`docs/BRIEF.md` is one page. Copy `templates/BRIEF.md` and fill it in. + +- **What they'll be able to do** is the behavioral spec. Write each line so + it can be tested: an action and an observable result. `/plan` turns these + into stories. +- **How we'll build it** comes from the architect. Don't write it yourself. +- Keep the whole thing short enough to read in two minutes. If it's longer, + the sprint is too big. Split it. + +## The acceptance pass + +After the build, run the real thing. Not the tests, the app. + +1. Start it the way a user would (the dev server, the CLI binary, a real + request to the route). +2. Go through the six stops above against what was built. +3. Check every line in **What they'll be able to do**. Did it happen? +4. Read every string a user can see. Typos, jargon, blame ("Invalid + input") all count. + +Return one of: + +- `ACCEPT`: it does what the brief says and you'd hand it to a customer. +- `POLISH`: up to five items. Each one follows the budget rules above + (small, removable, one task). Give the file or screen, what's wrong, and + what it should do instead. + +One round only. Anything bigger than polish goes under **Next** in the +brief, and you say so in the report. A feature that doesn't do what the +brief promised is not polish. That's a failed task, and you report it as +`REJECT` with the brief line it missed. + +## What this seat never does + +- Write code or edit files. You report, the builder fixes. +- Interview the human past the three-question limit. +- Add an extra that breaks the budget because it's a really good idea. +- Re-open a decision the human made at the gate. diff --git a/.claude/skills/product-owner/templates/BRIEF.md b/.claude/skills/product-owner/templates/BRIEF.md new file mode 100644 index 00000000..fe9c3f96 --- /dev/null +++ b/.claude/skills/product-owner/templates/BRIEF.md @@ -0,0 +1,68 @@ +# πŸ“‹ Brief: [sprint name] + +> Owned by `/sprint`. One page. Replaces PROJECT.md and SPEC.md for this +> sprint. Approved on: [date, or "not yet"]. + +## 🎯 What we're building + +[Two or three sentences. What it is and why now.] + +## πŸ‘€ Who it's for + +[One specific person in one specific situation. "A billing admin closing +out the month", not "users".] + +## βœ… What they'll be able to do + +[The behavioral spec. One line each: an action and an observable result. +`/plan` slices these into stories.] + +- [They can ..., and they see ...] +- [When ..., the system ...] + +## πŸ€” Assumptions + +[Every guess we made in place of asking. Correct any that are wrong.] + +> ASSUMPTION: [...] +> ASSUMPTION: [...] + +## πŸ—οΈ How we'll build it + +[From the architect. Short.] + +- **Approach:** [one paragraph] +- **Touches:** [modules, files, tables] +- **New seams:** [interfaces or boundaries added, or "none"] +- **Data changes:** [schema changes, or "none"] +- **Decision:** [what we chose] over [what we rejected], because [why] + +## ✨ What you didn't ask for + +[One to three extras, inside the delight budget. Strike any you don't +want. Zero is fine.] + +- [ ] [extra]: [one line of why, from the customer's side] + +## βœ‚οΈ What I'd cut + +[Anything in the request the customer wouldn't miss. Or "nothing".] + +- [item]: [why] + +## 🚧 Risks + +- [the thing most likely to go wrong, and the fallback] + +## ⏭️ Next (not this sprint) + +[Good ideas that broke the budget or the scope.] + +- [...] + +## ❓ Questions + +[Three at most. Only for things that are expensive to undo. Each has a +recommended answer.] + +1. [question] Recommended: [answer] diff --git a/.claude/skills/sqlite-dev/SKILL.md b/.claude/skills/sqlite-dev/SKILL.md new file mode 100644 index 00000000..700557ac --- /dev/null +++ b/.claude/skills/sqlite-dev/SKILL.md @@ -0,0 +1,173 @@ +--- +name: sqlite-dev +description: >- + Opinionated SQLite conventions for local TypeScript + Bun web development + with Drizzle ORM, where Postgres is the likely production target. Keeps the + postgres-dba naming and modeling rules (snake_case, plural tables, `id` + surrogate keys, NOT NULL FKs with explicit ON DELETE, compound junction + keys, JSON document store) so the schema ports to Postgres with a dialect + swap, not a rewrite. Covers the `bun:sqlite` + `drizzle-orm/bun-sqlite` + driver, mandatory connection pragmas (WAL, foreign_keys, busy_timeout), + STRICT tables, the SQLite analogues of enums / booleans / timestamps / + JSONB / generated columns, drizzle-kit migrations, and a full section on + running SQLite *in production* (single-writer discipline, Litestream/LiteFS + durability, tuning) plus the Postgres cutover checklist. Use when starting + or reviewing a Bun+Drizzle app on SQLite, writing the schema or migrations, + choosing a column type, deciding whether SQLite can be the production + database, or porting a SQLite schema to Postgres. Ships copy-ready + `schema.ts`, `db.ts`, production `db.ts`, and `drizzle.config.ts`. +--- + +# SQLite for Local Dev β€” Postgres-Portable, Bun + Drizzle + +## 🎯 Why: Design for Change + +The goal of writing software is to be able to **change it safely** β€” +including changing the database. Every rule here (snake_case, plural tables, +NOT NULL FKs, JSON document store) exists so the schema ports to Postgres +with a dialect swap, not a rewrite. Portability *is* change-safety: when +SQLite stops fitting, you move with a Drizzle config edit instead of a +month of forensics. + + +SQLite here is the *development* database for a TypeScript/Bun app whose +production system of record will almost certainly be Postgres. That single +fact drives every rule: write the SQLite schema so that switching to +Postgres is a Drizzle dialect change and a data copy, **not** a redesign. +Use the same names, the same modeling discipline, and the same integrity +rules as `postgres-dba` β€” SQLite is just a smaller engine running them. + +This skill is about *the SQLite schema, the Drizzle layer, and operating +SQLite*. For schema-design philosophy (why NOT NULL FKs, why a surrogate +key) the authority is `postgres-dba`; this skill does not re-argue it, it +ports it. For app-layer TypeScript use the language skills. + +## How to use this skill + +1. Match the task to a rule below or in the decision guide. +2. Open the matching `references/*.md` for the rationale, the wrong way, + the right way, and the explicit-override escape hatch. +3. Copy the closest file from `templates/` and adapt it β€” the templates + already encode every convention, so you start compliant *and* portable. +4. Apply the rule unless you can state, in a code comment, the specific + reason it does not apply. "SQLite let me" is not a reason β€” SQLite lets + you do almost anything; Postgres will not. + +## The hard rules (non-negotiable defaults) + +1. **Naming is identical to `postgres-dba`.** `snake_case`, lowercase, + unquoted. Tables plural (`orders`), columns singular (`shipped_at`), + primary key literally `id`, FKs `<table_singular>_id`, booleans + `is_/has_`, timestamps past-tense `_at`. In Drizzle the *TypeScript* + property may be `camelCase`, but the **column name argument is always + the `snake_case` name** β€” `createdAt: integer('created_at', …)`. The + database never sees a quoted mixed-case identifier. See + `references/naming.md`. + +2. **`bun:sqlite` + `drizzle-orm/bun-sqlite`, one `Database` per process.** + That is the dev driver. Every connection runs the pragma block (Rule 3) + at open. SQLite has one writer at a time; a single shared `Database` + instance plus serialized writes is the model β€” do not invent a pool of + writers. See `references/drizzle.md`. + +3. **Pragmas are mandatory and per-connection.** Every connection sets, in + order: `journal_mode = WAL`, `foreign_keys = ON`, `busy_timeout = 5000`, + `synchronous = NORMAL`. **`foreign_keys` is OFF by default in SQLite** β€” + forgetting it silently disables every `ON DELETE` you wrote. This is the + single most common SQLite data-integrity bug. See `references/drizzle.md`. + +4. **Tables are `STRICT`, types come from the Postgres-portable set only.** + Declare `STRICT` (SQLite β‰₯ 3.37) so a column actually rejects wrong + types instead of silently coercing. Use only the type vocabulary in + `references/types.md`: `INTEGER`, `TEXT`, `REAL`, `BLOB` mapped to a + concrete Postgres target. No `VARCHAR(n)`, no `DATETIME`, no `BOOLEAN`, + no `NUMERIC` β€” those are affinity theater in SQLite and lie about the + Postgres column you will eventually create. + +5. **FKs are `NOT NULL` by default with explicit `ON DELETE`.** Same rule + as `postgres-dba` Rule 4. A nullable FK carries a + `// nullable-fk: <reason>` comment or it is a defect. Many-to-many is a + **compound primary key**, no surrogate `id` on a pure junction. See + `references/portability.md`. + +6. **Enums are `text` + a `CHECK (col IN (...))`, mirroring the Postgres + enum.** SQLite has no enum type. Use Drizzle `text({ enum: [...] })` + *and* an explicit check constraint so the values match the + `CREATE TYPE` you will write in Postgres. Booleans are + `integer({ mode: 'boolean' })` + `CHECK (col IN (0,1))`. See + `references/portability.md`. + +7. **Timestamps are one chosen representation, app-set, always UTC.** + SQLite has no `timestamptz`. Default: `integer` epoch-milliseconds via + Drizzle `{ mode: 'timestamp_ms' }`, set by the app, never + `CURRENT_TIMESTAMP` (which emits a non-ISO, tz-ambiguous string). This + ports to Postgres `timestamptz` mechanically. See `references/types.md`. + +8. **JSON is `text({ mode: 'json' })` with a relational spine, mirroring + the JSONB rule.** Keys/FKs/hot fields are real columns; open-ended data + is a JSON document, hot fields lifted out via a `GENERATED ... STORED` + column and indexed. This is `postgres-dba` Rule 8 with `jsonb` β†’ `text` + JSON. See `references/portability.md`. + +9. **Set-based / multi-row business logic lives in an explicit + transaction in a named module β€” *not* scattered across callers.** This + is the one place SQLite cannot match `postgres-dba` Rule 7 (no + `plpgsql`). Quarantine that logic in one repository/service function + wrapped in a `db.transaction(...)` so the Postgres cutover has exactly + one place to consider promoting to a function. See + `references/portability.md`. + +## Decision guide + +| Situation | Rule | Reference | +|---|---|---| +| Naming any table/column/index/constraint | Rule 1 | `references/naming.md` | +| Drizzle TS property vs DB column name | Rule 1 | `references/naming.md` | +| Choosing the driver / opening the DB | Rule 2 | `references/drizzle.md` | +| WAL / foreign_keys / busy_timeout setup | Rule 3 | `references/drizzle.md` | +| Picking a column type | Rule 4 | `references/types.md` | +| `STRICT` table or not | Rule 4 | `references/types.md` | +| FK nullability and `ON DELETE` | Rule 5 | `references/portability.md` | +| Many-to-many junction | Rule 5 | `references/portability.md` | +| Status / role / kind column | Rule 6 | `references/portability.md` | +| Boolean column | Rule 6 | `references/types.md` | +| Storing a timestamp / date / money | Rule 7 | `references/types.md` | +| Open-ended / document-shaped data | Rule 8 | `references/portability.md` | +| Indexing a value inside a JSON document | Rule 8 | `references/portability.md` | +| Multi-row business rule / invariant | Rule 9 | `references/portability.md` | +| Generating & applying migrations | β€” | `references/drizzle.md` | +| "Can SQLite *be* production here?" | β€” | `references/production.md` | +| Backups / durability / replication | β€” | `references/production.md` | +| Cutting over from SQLite to Postgres | β€” | `references/portability.md` | + +## Templates + +- `templates/schema.ts` β€” a Drizzle SQLite schema demonstrating every + rule: snake_case columns under camelCase keys, `id` surrogate key, a + `text`+`CHECK` enum, integer boolean, epoch-ms timestamps, NOT NULL FKs + with explicit `onDelete`, one annotated nullable-FK override, a + compound-key junction, a STORED generated column, and a JSON body with a + lifted+indexed hot field. +- `templates/db.ts` β€” the dev connection: `bun:sqlite` + Drizzle, the + mandatory pragma block applied at open, and startup migration. +- `templates/db.production.ts` β€” the production connection: pragmas tuned + for a server (mmap, cache), single-writer discipline, `IMMEDIATE` + transaction helper, and a `VACUUM INTO` backup hook. +- `templates/drizzle.config.ts` β€” drizzle-kit config for the SQLite + dialect, with the one-line change that points it at Postgres later. + +## What this skill will not do + +- Bless a SQLite-only construct that has no Postgres equivalent in the + schema (e.g. relying on rowid aliasing, type affinity coercion, or + `WITHOUT ROWID` for a normal table). If Postgres can't express it, it + doesn't belong in a portable schema. +- Bless `foreign_keys` left at the default. An app that doesn't set the + pragma has no referential integrity, full stop. +- Bless `CURRENT_TIMESTAMP` / `DATETIME('now')` for stored timestamps β€” + the format is not ISO-8601 and not tz-aware (Rule 7). +- Bless a multi-writer connection pool against one SQLite file. +- Bless SQLite in production *by default*. It can be production β€” under + the explicit conditions in `references/production.md`, and only there. +- Re-derive schema-design philosophy. That is `postgres-dba`'s job; this + skill ports its conclusions to a smaller engine. diff --git a/.claude/skills/sqlite-dev/references/drizzle.md b/.claude/skills/sqlite-dev/references/drizzle.md new file mode 100644 index 00000000..d295af9c --- /dev/null +++ b/.claude/skills/sqlite-dev/references/drizzle.md @@ -0,0 +1,81 @@ +# Driver, Connection & Migrations + +## Driver: `bun:sqlite` + `drizzle-orm/bun-sqlite` + +Bun ships a native SQLite driver. It is the fastest option in a Bun +process and needs no native compile step. Use it for dev: + +```ts +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +``` + +If a non-Bun runtime ever has to open the same file (a Node script, a +migration tool), `better-sqlite3` via `drizzle-orm/better-sqlite3` reads +the identical file and schema β€” the schema is driver-agnostic, only the +`db.ts` glue differs. Do not use an async/HTTP SQLite driver (libSQL +remote, D1) unless that *is* the production target; those change +transaction semantics and defeat the Postgres-portability goal. + +## One `Database` per process + +SQLite permits exactly one writer at a time against a file. A Bun web +server is one process; open **one** `Database`, wrap it once with +`drizzle()`, and export that. Importing a connection pool of writers +against one file buys you `SQLITE_BUSY`, not concurrency. Reads are +concurrent under WAL; writes serialize whether you like it or not β€” so +make it explicit (see `production.md` on `IMMEDIATE` transactions). + +## The mandatory pragma block (Rule 3) + +Run this immediately after opening, before any query, **on every +connection** (a connection does not inherit another's pragmas): + +```ts +db.exec('PRAGMA journal_mode = WAL;'); // concurrent readers + one writer +db.exec('PRAGMA foreign_keys = ON;'); // OFF BY DEFAULT β€” without this, no FK enforcement +db.exec('PRAGMA busy_timeout = 5000;'); // wait 5s for the write lock instead of throwing +db.exec('PRAGMA synchronous = NORMAL;'); // safe + fast under WAL (FULL is overkill here) +``` + +`foreign_keys = ON` is the line everyone forgets. Without it, every +`onDelete: 'cascade' | 'restrict' | 'set null'` you carefully wrote is +inert and the database silently accepts orphan rows. Treat a `db.ts` +without this line as a bug report. + +`journal_mode = WAL` persists in the database file; the other three are +per-connection and must be re-issued every open. The template re-issues +all four for safety. + +## STRICT tables + +Plain SQLite tables have *type affinity*: a `TEXT` column will quietly +store the integer `42`. That coercion is exactly the class of bug that +explodes at the Postgres cutover. Declare tables `STRICT` (SQLite β‰₯ 3.37; +Bun's bundled SQLite is new enough) so the engine rejects wrong types like +Postgres would. Drizzle does not emit `STRICT` for you β€” add it to the +generated migration (the template shows the one-line edit), or define +tables through a migration that includes `) STRICT;`. + +## Migrations with drizzle-kit + +- Author the schema in `src/db/schema.ts`. Never hand-write DDL as the + source of truth β€” the schema file is the source, migrations are + generated artifacts. +- `bunx drizzle-kit generate` β€” diffs the schema, writes a timestamped + SQL migration into `drizzle/`. +- Review the generated SQL. For SQLite, drizzle-kit uses a + table-rebuild strategy for many `ALTER`s (create new, copy, drop, + rename) because SQLite's `ALTER TABLE` is limited. Confirm the rebuild + preserves data and that `STRICT` survived. +- Apply at application startup with `migrate()` from + `drizzle-orm/bun-sqlite/migrator` (the dev `db.ts` template does this), + or `bunx drizzle-kit migrate` in CI. +- Commit `drizzle/` to the repo. Migrations are history; never edit an + applied one β€” add a new one. + +## When to deviate + +- Tests may open an in-memory database (`new Database(':memory:')`) and + skip WAL (`journal_mode=WAL` is meaningless in memory) β€” still set + `foreign_keys=ON`. That is the only sanctioned pragma deviation. diff --git a/.claude/skills/sqlite-dev/references/naming.md b/.claude/skills/sqlite-dev/references/naming.md new file mode 100644 index 00000000..c4dfa9f5 --- /dev/null +++ b/.claude/skills/sqlite-dev/references/naming.md @@ -0,0 +1,77 @@ +# Naming Conventions + +The names are **exactly** the `postgres-dba` names. SQLite is more +permissive about identifiers than Postgres β€” it does not fold case, it +tolerates almost anything quoted β€” and that permissiveness is a trap: a +name that works in SQLite but needs `"FooBar"` quoting in Postgres is a +name that breaks at cutover. Write Postgres-legal names now. + +## Rules (same as postgres-dba/references/naming.md) + +- **Case:** `snake_case`, all lowercase, in the *database*. No `CamelCase`, + no quoted mixed-case identifiers. +- **Tables:** plural nouns β€” `users`, `orders`, `order_items`. +- **Columns:** singular β€” `email`, `shipped_at`, `total_cents`. +- **Primary key:** always literally `id`. +- **Foreign keys:** `<referenced_table_singular>_id` β€” `user_id`, + `parent_order_id`. +- **Booleans:** `is_active`, `has_shipped`, `is_default`. Never bare + `active`; never negative `is_not_deleted`. +- **Timestamps:** past-tense event + `_at` β€” `created_at`, `updated_at`, + `deleted_at`. (Stored as epoch-ms integers β€” see `types.md` β€” but the + *name* is unchanged.) +- **Junction tables:** the two table names, alphabetical, plural: + `groups_users`. If it carries its own data, name it the domain noun + (`enrollments`) and treat it as a real entity. +- **Indexes:** `ix_<table>_<cols>`, `ux_<table>_<cols>` (unique). +- **Constraints:** name them β€” `ck_<table>_<rule>`, `fk_<table>_<col>`, + `uq_<table>_<cols>`. SQLite auto-names are even less legible than + Postgres's; name them so a constraint violation is debuggable. +- **No reserved words; no abbreviations** (`url`, `id`, `sku` are the only + blessed ones). + +## The Drizzle two-name rule + +Drizzle separates the **TypeScript property** (what your app code reads) +from the **database column name** (what SQL sees). Use that separation: +`camelCase` in TS for ergonomics, `snake_case` in the database for +portability. **The column-name string argument is mandatory and is always +`snake_case`.** + +### Wrong + +```ts +export const orders = sqliteTable('Orders', { // PascalCase table + orderId: integer('orderId').primaryKey(), // camelCase column, redundant prefix + customer: integer('customer'), // no _id, nullable by omission + active: integer('active'), // ambiguous boolean + created: text('created'), // not _at, not a timestamp type +}); +``` + +### Right + +```ts +export const orders = sqliteTable('orders', { + id: integer('id').primaryKey({ autoIncrement: true }), + customerId: integer('customer_id').notNull() + .references(() => customers.id, { onDelete: 'restrict' }), + isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), + createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull() + .$defaultFn(() => new Date()), +}, (t) => [ + index('ix_orders_customer_id').on(t.customerId), +]); +``` + +The Drizzle object is `orders` (matches the table). The property +`customerId` is camelCase for the app; the column is `'customer_id'`. When +the dialect later becomes `pg`, these exact column names already exist in +Postgres unquoted and lowercase β€” nothing to rename. + +## When to deviate + +- A name dictated by an external system you do not control. Isolate it, + put a view/Drizzle alias with conformant names in front of it. +- That is the entire list. See `postgres-dba/references/naming.md` for the + full rationale; it applies here verbatim. diff --git a/.claude/skills/sqlite-dev/references/portability.md b/.claude/skills/sqlite-dev/references/portability.md new file mode 100644 index 00000000..3d997a3b --- /dev/null +++ b/.claude/skills/sqlite-dev/references/portability.md @@ -0,0 +1,124 @@ +# Modeling for the Postgres Cutover + +Everything in `postgres-dba` about *modeling* (keys, FK nullability, +junctions, JSON-as-document, derived columns) applies unchanged. SQLite +just expresses some of it differently. This file is the difference list +and the cutover checklist. + +## Keys & foreign keys (postgres-dba Rules 2–4) + +- `id` surrogate key on every base table: + `integer('id').primaryKey({ autoIncrement: true })`. `AUTOINCREMENT` + makes ids monotonic and non-reused, matching `serial`. Natural keys get + a unique index, never the primary key. +- FKs are `NOT NULL` by default with an **explicit** `onDelete`. A + nullable FK requires a `// nullable-fk: <reason>` comment on the line or + it is a defect β€” identical to `postgres-dba` Rule 4. +- `foreign_keys = ON` (the pragma) is what makes any of this real. See + `drizzle.md`. + +## Many-to-many (postgres-dba Rule 3) + +A pure junction has a **compound primary key**, no surrogate `id`, both +columns NOT NULL FKs with `onDelete: 'cascade'`: + +```ts +export const groupsUsers = sqliteTable('groups_users', { + groupId: integer('group_id').notNull() + .references(() => groups.id, { onDelete: 'cascade' }), + userId: integer('user_id').notNull() + .references(() => customers.id, { onDelete: 'cascade' }), + addedAt: integer('added_at', { mode: 'timestamp_ms' }).notNull() + .$defaultFn(() => new Date()), +}, (t) => [primaryKey({ columns: [t.groupId, t.userId] })]); +``` + +If the join carries its own data and identity, it is an entity +(`enrollments`) with its own `id`, not a junction. + +## Enums (postgres-dba Rule 5) + +SQLite has no enum type. Reproduce the Postgres enum with `text` + a +`CHECK` whose value list is **character-for-character** the future +`CREATE TYPE`: + +```ts +status: text('status', { enum: ['pending','paid','shipped','delivered','cancelled'] }) + .notNull().default('pending'), +// + check('ck_orders_status', sql`status in ('pending','paid','shipped','delivered','cancelled')`) +``` + +Drizzle's `enum` gives you the *TypeScript* union; the `CHECK` gives you +the *database* guarantee SQLite otherwise omits. At cutover this becomes a +real `order_status` type β€” the allowed set already matches. + +## JSON as a document store (postgres-dba Rule 8) + +Same hybrid pattern, `jsonb` β†’ `text` JSON: + +- Keys, FKs, and hot/queried fields are real columns. +- Open-ended or document-shaped data is `text('body', { mode: 'json' }).$type<Body>()`. +- A field you filter or sort on is **lifted out** via a STORED generated + column and indexed β€” do not query into JSON in hot paths on either + engine: + +```sql +-- in the migration: +priority TEXT GENERATED ALWAYS AS (json_extract(body, '$.priority')) STORED, +``` +```ts +// schema.ts mirror so Drizzle knows the column: +priority: text('priority').generatedAlwaysAs(sql`json_extract(body, '$.priority')`, { mode: 'stored' }), +``` + +`json_extract(... '$.x')` ports to Postgres `body->>'x'`. No EAV tables on +either engine. + +## Generated columns (postgres-dba Rule 6) + +SQLite supports `GENERATED ALWAYS AS (expr) STORED` (β‰₯ 3.31). Use +`STORED` for anything indexed or searched (Postgres only has `STORED`, so +never rely on SQLite `VIRTUAL` for a portable column). Derived data you +filter/sort on is a generated column, not app code and not a trigger β€” +exactly Rule 6. + +## The one real gap: no `plpgsql` (postgres-dba Rule 7) + +SQLite has no stored procedures. Multi-row business rules and invariants +that `postgres-dba` would put in a `plpgsql` function must live in the +app β€” but **quarantined**, not scattered: + +- One repository/service function per operation, wrapped in + `db.transaction(...)` (use an `IMMEDIATE` transaction for write paths to + fail fast on lock contention β€” see `production.md`). +- Push what you *can* into the schema regardless of engine: `CHECK` + constraints, `NOT NULL`, `UNIQUE`, generated columns, FK actions. Those + port directly and need no rewrite. +- Leave a `// pg-candidate: <rule>` comment on each such transaction so + the cutover has an exact inventory of logic to consider promoting into + a database function. + +## Cutover checklist (SQLite β†’ Postgres) + +When production goes to Postgres: + +1. **Drizzle config:** `dialect: 'sqlite'` β†’ `'postgresql'`, swap the + driver in `db.ts` (`drizzle-orm/node-postgres` or `bun-sql`). The + schema file's column *names* are already Postgres-legal. +2. **Schema file:** swap `sqliteTable`β†’`pgTable`, the column builders + (`integer`β†’`serial`/`integer`, `text` enum β†’ `pgEnum`, timestamp-ms + integer β†’ `timestamp`/`timestamptz`, JSON `text` β†’ `jsonb`). Names do + not change. Regenerate migrations against an empty Postgres database. +3. **Enums:** create the `pgEnum` from the exact list already in the + `CHECK`/Drizzle `enum`. +4. **Timestamps:** migrate stored epoch-ms via + `to_timestamp(col / 1000.0)` (or parse the ISO text). +5. **`pg-candidate` transactions:** review each; promote to a `plpgsql` + function where it is genuinely set-based or must hold regardless of + caller (`postgres-dba/references/functions.md`). +6. **Data copy:** export rows, load into Postgres; verify FK pragma was on + in SQLite so there are no orphans to reject. +7. Delete this skill's `db.ts` pragma block β€” Postgres needs none of it. + +Because every rule above was followed, this is a mechanical port, not a +redesign. That is the entire point of the skill. diff --git a/.claude/skills/sqlite-dev/references/production.md b/.claude/skills/sqlite-dev/references/production.md new file mode 100644 index 00000000..7c0b4267 --- /dev/null +++ b/.claude/skills/sqlite-dev/references/production.md @@ -0,0 +1,108 @@ +# SQLite *in* Production + +The default assumption of this skill is Postgres in production. But SQLite +can be a perfectly good production database β€” for the right workload, with +the discipline below. This section is how to make that possible. It is not +permission to skip the analysis; it is the analysis. + +## When SQLite-in-prod is appropriate + +All of these should be true: + +- **Single node.** One application process (or a small set on one box) + owning one database file on a real, persistent, fsync-honoring disk β€” + not a network filesystem, not an ephemeral container layer. +- **Read-heavy or modest write volume.** WAL gives unlimited concurrent + readers but exactly **one writer at a time**. Hundreds of + reads/sec/concurrent: fine. Sustained, highly-concurrent independent + writers: that is what Postgres is for. +- **Writes are short.** A writer holds a global lock for the duration of + its transaction. Long-running write transactions stall every other + writer. Keep them milliseconds, not seconds. +- **Operational simplicity is a feature.** No DB server to run, patch, or + scale; the database is a file you can copy. For edge/embedded/internal + tools/single-tenant SaaS this is a genuine advantage. + +If any is false, SQLite is the dev database and Postgres is production β€” +follow `portability.md` and stop here. + +## Production connection settings + +The dev pragma block plus server tuning (see `templates/db.production.ts`): + +```ts +db.exec('PRAGMA journal_mode = WAL;'); +db.exec('PRAGMA foreign_keys = ON;'); +db.exec('PRAGMA busy_timeout = 5000;'); +db.exec('PRAGMA synchronous = NORMAL;'); // durable under WAL on good hardware +db.exec('PRAGMA wal_autocheckpoint = 1000;'); // pages; keep the WAL bounded +db.exec('PRAGMA cache_size = -65536;'); // ~64 MB page cache (negative = KiB) +db.exec('PRAGMA mmap_size = 268435456;'); // 256 MB memory-mapped I/O +db.exec('PRAGMA temp_store = MEMORY;'); +``` + +`synchronous = NORMAL` under WAL loses *no committed transaction* on an +app crash; it can lose the last commit only on an OS/power loss without a +working fsync. If that is unacceptable, use `FULL` and accept the write +cost β€” decide deliberately and comment it. + +## Single-writer discipline + +This is the rule that makes SQLite-in-prod safe: + +- **One `Database` instance for writes, process-wide.** Never a pool of + writers against the file. +- **Wrap every write path in an `IMMEDIATE` transaction.** A deferred + transaction takes the write lock lazily and can fail *mid-transaction* + with `SQLITE_BUSY`; `BEGIN IMMEDIATE` acquires it up front so contention + fails fast and cleanly (the helper is in the production template). +- **Keep write transactions tiny.** Do network/CPU work *before* opening + the transaction; inside it, only the writes. +- `busy_timeout` handles brief contention by waiting; `IMMEDIATE` plus + short transactions keeps that wait near zero. + +Reads need none of this β€” under WAL they never block and are never blocked +by the writer. + +## Durability & backups + +A single file is a single point of failure. Pick one: + +- **Litestream** (recommended for single node): streams the WAL + continuously to S3-compatible storage; point-in-time restore, near-zero + RPO, no app changes. This is the standard answer for SQLite-in-prod. +- **LiteFS** (Fly.io): a FUSE filesystem giving replicated SQLite with + read replicas and failover. Use when you need HA/read-scale and accept + its single-writer/primary model. +- **Snapshot backups** as a baseline regardless: `VACUUM INTO + '/backups/app-<ts>.db'` produces a consistent copy without blocking + readers (the production template exposes this as a scheduled hook). Plain + `cp` of a live WAL database is **not** a valid backup. + +Test restores. An untested backup is a hope, not a backup. + +## Operational notes + +- **WAL checkpointing:** `wal_autocheckpoint` keeps the `-wal` file + bounded; run a periodic `PRAGMA wal_checkpoint(TRUNCATE);` in a + low-traffic window if it still grows under sustained writes. +- **Disk:** local SSD/NVMe. Networked/`NFS` filesystems break SQLite's + locking β€” data corruption, not just slowness. Containers must mount a + real persistent volume; the writable container layer is not durable. +- **Migrations:** apply at deploy, not per-request. A schema-rebuild + migration (drizzle-kit's SQLite strategy) takes the write lock for the + rebuild β€” schedule it and expect brief write unavailability. +- **`STRICT` tables** still apply in prod; they are the cheapest + protection against affinity bugs you have. +- **No network exposure.** SQLite is in-process; there is nothing to + bind, authenticate, or firewall. That is part of why it's simple β€” keep + it that way. + +## Outgrowing it + +The triggers to migrate to Postgres: write contention you can't shrink +(persistent `SQLITE_BUSY` despite short `IMMEDIATE` transactions), a need +for multiple writer nodes, or analytical/concurrent workloads the single +writer can't serve. Because the schema followed `portability.md`, that +migration is the mechanical cutover checklist there β€” which is exactly why +you keep these conventions even when SQLite *is* production. diff --git a/.claude/skills/sqlite-dev/references/types.md b/.claude/skills/sqlite-dev/references/types.md new file mode 100644 index 00000000..6b2fcb1e --- /dev/null +++ b/.claude/skills/sqlite-dev/references/types.md @@ -0,0 +1,56 @@ +# Type Vocabulary β€” SQLite β†’ Postgres + +SQLite has four storage classes (`INTEGER`, `TEXT`, `REAL`, `BLOB`) and +lies about everything else via affinity. The job here is to pick, for each +*conceptual* type, the one SQLite representation that maps cleanly to the +Postgres column you will eventually create. Use only this table. The +"Drizzle" column is the canonical `schema.ts` declaration. + +| Concept | SQLite (STRICT) | Drizzle (sqlite) | Postgres target | Notes | +|---|---|---|---|---| +| Surrogate key | `INTEGER PRIMARY KEY AUTOINCREMENT` | `integer('id').primaryKey({ autoIncrement: true })` | `id serial primary key` | `AUTOINCREMENT` β‡’ ids never reused, matching `serial` semantics. Worth the tiny cost. | +| Foreign key | `INTEGER` + `REFERENCES` | `integer('x_id').references(...)` | `int ... references` | Requires `foreign_keys = ON`. | +| Short/long string | `TEXT` | `text('name')` | `text` | Never `VARCHAR(n)`. Length limits are a `CHECK`, not a type. | +| Enum | `TEXT` + `CHECK (c IN (...))` | `text('status', { enum: [...] })` + `check()` | `CREATE TYPE ... AS ENUM` | Keep the TS enum list and the Postgres type list identical. | +| Boolean | `INTEGER` + `CHECK (c IN (0,1))` | `integer('is_x', { mode: 'boolean' })` | `boolean` | Drizzle marshals 0/1 ↔ JS boolean. | +| Timestamp (default) | `INTEGER` (epoch ms) | `integer('x_at', { mode: 'timestamp_ms' })` | `timestamptz` | App-set, always UTC. Arithmetic- and sort-correct. See below. | +| Date-only | `TEXT` `'YYYY-MM-DD'` | `text('x_on')` | `date` | Zero-padded, lexicographically sortable. | +| Money | `INTEGER` minor units | `integer('amount_cents')` | `int` / `bigint` | Never `REAL` for money β€” float drift is non-negotiable. | +| Exact decimal | `TEXT` (canonical form) | `text('rate')` | `numeric` | Compute in a decimal lib, store as text; `REAL` is not exact. | +| Float / measure | `REAL` | `real('weight_kg')` | `double precision` | Only where lossy is acceptable. | +| JSON document | `TEXT` (JSON) | `text('body', { mode: 'json' }).$type<T>()` | `jsonb` | See `portability.md` Rule 8. | +| Binary blob | `BLOB` | `blob('payload')` | `bytea` | Prefer object storage + a `text` URL for anything large. | +| UUID | `TEXT` (canonical 8-4-4-4-12) | `text('public_id')` | `uuid` | Generate in the app (`crypto.randomUUID()`); store as text. | + +## Timestamps (Rule 7) β€” why epoch-ms integer + +`CURRENT_TIMESTAMP` / `datetime('now')` produce `'2026-05-18 14:03:00'`: +no `T`, no `Z`, no timezone, second precision. That string ports to +Postgres `timestamptz` as a guess, not a value. Instead: + +- Store `INTEGER` epoch **milliseconds**, set by the app + (`new Date()` via Drizzle `mode: 'timestamp_ms'`). +- It compares and sorts correctly as an integer, needs no parsing, and + converts to Postgres `timestamptz` with one + `to_timestamp(col / 1000.0)` expression at cutover. +- The column name still ends in `_at` (naming is unchanged). + +Acceptable alternative: ISO-8601 UTC `TEXT` +(`new Date().toISOString()` β†’ `'2026-05-18T14:03:00.000Z'`). It is +human-readable and sorts correctly because it is fixed-width UTC. Pick one +representation per project and never mix. + +## STRICT enforcement + +Every table is `STRICT` (see `drizzle.md`). Under `STRICT`, the storage +class in this table is enforced β€” an `INTEGER` column rejects `'oops'` +instead of silently storing the string, which is precisely the Postgres +behavior you are rehearsing for. + +## When to deviate + +- A `REAL` for genuinely approximate scientific/measurement data is fine β€” + document the precision tolerance in a comment so the Postgres + `double precision` choice is intentional. +- Storing UUIDv7 as a 16-byte `BLOB` for index density is allowed *if* a + comment records the Postgres plan (`uuid` column + app-side decode). diff --git a/.claude/skills/sqlite-dev/templates/db.production.ts b/.claude/skills/sqlite-dev/templates/db.production.ts new file mode 100644 index 00000000..cca67547 --- /dev/null +++ b/.claude/skills/sqlite-dev/templates/db.production.ts @@ -0,0 +1,57 @@ +// Production connection β€” ONLY if references/production.md says SQLite is +// an appropriate production database for this workload (single node, +// read-heavy / modest short writes, durable local disk). Otherwise +// production is Postgres: see references/portability.md. + +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import * as schema from './schema'; + +const DB_PATH = process.env.DATABASE_PATH ?? '/data/app.db'; // a real persistent volume + +const sqlite = new Database(DB_PATH, { create: true }); + +// Rule 3 pragmas + server tuning. synchronous=NORMAL under WAL loses no +// committed txn on app crash; only a possible last-commit loss on OS/power +// loss without working fsync. Switch to FULL (and comment why) if that is +// unacceptable for this data. +for (const p of [ + 'PRAGMA journal_mode = WAL;', + 'PRAGMA foreign_keys = ON;', + 'PRAGMA busy_timeout = 5000;', + 'PRAGMA synchronous = NORMAL;', + 'PRAGMA wal_autocheckpoint = 1000;', + 'PRAGMA cache_size = -65536;', // ~64 MB + 'PRAGMA mmap_size = 268435456;', // 256 MB + 'PRAGMA temp_store = MEMORY;', +]) { + sqlite.exec(p); +} + +export const db = drizzle(sqlite, { schema }); + +// Single-writer discipline (production.md): wrap every write path in an +// IMMEDIATE transaction so lock contention fails fast and cleanly instead +// of mid-transaction. Keep the callback tiny β€” do I/O and CPU work BEFORE +// calling this, only writes inside. +export function writeTx<T>(fn: (tx: typeof db) => T): T { + sqlite.exec('BEGIN IMMEDIATE;'); + try { + const result = fn(db); + sqlite.exec('COMMIT;'); + return result; + } catch (err) { + sqlite.exec('ROLLBACK;'); + throw err; + } +} + +// Consistent, non-blocking snapshot backup. Schedule this AND use +// Litestream (continuous WAL replication) for real durability β€” a single +// file is a single point of failure, and `cp` of a live DB is not a +// backup. Test restores. +export function backupTo(path: string): void { + sqlite.exec(`VACUUM INTO '${path.replace(/'/g, "''")}';`); +} + +export { sqlite }; diff --git a/.claude/skills/sqlite-dev/templates/db.ts b/.claude/skills/sqlite-dev/templates/db.ts new file mode 100644 index 00000000..565fd7aa --- /dev/null +++ b/.claude/skills/sqlite-dev/templates/db.ts @@ -0,0 +1,33 @@ +// Dev connection: bun:sqlite + Drizzle, one Database per process, the +// mandatory pragma block, and startup migration. +// +// import { db } from './db'; +// +// Rule 2: one shared Database instance. Rule 3: pragmas on every open. + +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; +import * as schema from './schema'; + +const DB_PATH = process.env.DATABASE_PATH ?? './data/app.db'; + +const sqlite = new Database(DB_PATH, { create: true }); + +// Rule 3 β€” mandatory, in this order, on every connection. `foreign_keys` +// is OFF by default in SQLite: omit it and every onDelete is silently +// inert. Treat a missing line here as a bug. +sqlite.exec('PRAGMA journal_mode = WAL;'); +sqlite.exec('PRAGMA foreign_keys = ON;'); +sqlite.exec('PRAGMA busy_timeout = 5000;'); +sqlite.exec('PRAGMA synchronous = NORMAL;'); + +export const db = drizzle(sqlite, { schema }); + +// Apply generated migrations at startup (or run `bunx drizzle-kit migrate` +// in CI instead and delete this). +migrate(db, { migrationsFolder: './drizzle' }); + +// Single-writer reminder: do not open a second writable Database against +// DB_PATH. Reads are concurrent under WAL; writes serialize regardless. +export { sqlite }; diff --git a/.claude/skills/sqlite-dev/templates/drizzle.config.ts b/.claude/skills/sqlite-dev/templates/drizzle.config.ts new file mode 100644 index 00000000..699a1ad8 --- /dev/null +++ b/.claude/skills/sqlite-dev/templates/drizzle.config.ts @@ -0,0 +1,27 @@ +// drizzle-kit config for the SQLite dev dialect. +// +// bunx drizzle-kit generate # diff schema.ts -> a migration in ./drizzle +// bunx drizzle-kit migrate # apply (or migrate() at startup, see db.ts) +// +// Postgres cutover (references/portability.md): change `dialect` to +// 'postgresql', point `dbCredentials` at the Postgres URL, swap the +// drizzle driver in db.ts, and adjust the column builders in schema.ts. +// The column NAMES are already Postgres-legal, so this is mechanical. + +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'sqlite', + schema: './src/db/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_PATH ?? './data/app.db', + }, + strict: true, + verbose: true, +}); + +// Reminder: drizzle-kit does NOT emit `STRICT` on CREATE TABLE. After +// `generate`, edit the new migration so each table ends `) STRICT;` +// (references/types.md) β€” this is what makes SQLite reject wrong types +// like Postgres will. diff --git a/.claude/skills/sqlite-dev/templates/schema.ts b/.claude/skills/sqlite-dev/templates/schema.ts new file mode 100644 index 00000000..2b5cc904 --- /dev/null +++ b/.claude/skills/sqlite-dev/templates/schema.ts @@ -0,0 +1,162 @@ +// Reference Drizzle SQLite schema β€” demonstrates every sqlite-dev rule and +// is deliberately Postgres-portable. Copy, rename, delete what you don't +// need. Change a convention only with a comment saying why. + +import { sql } from 'drizzle-orm'; +import { + sqliteTable, + integer, + text, + index, + uniqueIndex, + primaryKey, + check, +} from 'drizzle-orm/sqlite-core'; + +// NOTE: declare tables `STRICT` in the generated migration (drizzle-kit +// does not emit STRICT). See references/drizzle.md. + +// Rule 1: snake_case columns under camelCase keys, plural table name. +// Rule 4/Rule 2 (types/keys): id = serial-equivalent surrogate key. +export const customers = sqliteTable( + 'customers', + { + id: integer('id').primaryKey({ autoIncrement: true }), + email: text('email').notNull(), + fullName: text('full_name').notNull(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + }, + (t) => [ + uniqueIndex('ux_customers_email').on(t.email), + check('ck_customers_email_shape', sql`instr(${t.email}, '@') > 1`), + ], +); + +export const coupons = sqliteTable( + 'coupons', + { + id: integer('id').primaryKey({ autoIncrement: true }), + code: text('code').notNull(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + }, + (t) => [uniqueIndex('ux_coupons_code').on(t.code)], +); + +// Rule 6: enum = text + CHECK whose list IS the future Postgres CREATE TYPE. +// Rule 7: timestamps = app-set epoch-ms integers, never CURRENT_TIMESTAMP. +export const orders = sqliteTable( + 'orders', + { + id: integer('id').primaryKey({ autoIncrement: true }), + + // Rule 5: FK is NOT NULL by default, ON DELETE explicit. + customerId: integer('customer_id') + .notNull() + .references(() => customers.id, { onDelete: 'restrict' }), + + // Rule 5 override: a nullable FK MUST carry this comment or it is a defect. + // nullable-fk: a coupon is genuinely optional; cleared if the coupon is removed. + couponId: integer('coupon_id').references(() => coupons.id, { onDelete: 'set null' }), + + status: text('status', { + enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'], + }) + .notNull() + .default('pending'), + + createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + }, + (t) => [ + index('ix_orders_customer_id').on(t.customerId), + index('ix_orders_status').on(t.status), + check( + 'ck_orders_status', + sql`${t.status} in ('pending','paid','shipped','delivered','cancelled')`, + ), + ], +); + +export const products = sqliteTable( + 'products', + { + id: integer('id').primaryKey({ autoIncrement: true }), + sku: text('sku').notNull(), + name: text('name').notNull(), + // Rule: money is integer minor units, never REAL. + priceCents: integer('price_cents').notNull(), + isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), + }, + (t) => [ + uniqueIndex('ux_products_sku').on(t.sku), + check('ck_products_price_cents', sql`${t.priceCents} >= 0`), + check('ck_products_is_active_bool', sql`${t.isActive} in (0, 1)`), + ], +); + +export const orderItems = sqliteTable( + 'order_items', + { + id: integer('id').primaryKey({ autoIncrement: true }), + orderId: integer('order_id').notNull().references(() => orders.id, { onDelete: 'cascade' }), + productId: integer('product_id').notNull().references(() => products.id, { onDelete: 'restrict' }), + quantity: integer('quantity').notNull(), + unitCents: integer('unit_cents').notNull(), + // Rule 6 (postgres-dba): derived value we aggregate on -> STORED generated + // column. Cannot drift; ports to Postgres GENERATED ... STORED. + lineCents: integer('line_cents').generatedAlwaysAs( + sql`quantity * unit_cents`, + { mode: 'stored' }, + ), + }, + (t) => [ + uniqueIndex('uq_order_items_order_product').on(t.orderId, t.productId), + index('ix_order_items_order_id').on(t.orderId), + check('ck_order_items_quantity', sql`${t.quantity} > 0`), + check('ck_order_items_unit_cents', sql`${t.unitCents} >= 0`), + ], +); + +// Rule 3 (postgres-dba): pure many-to-many -> compound primary key, NO +// surrogate id, both sides NOT NULL FKs with CASCADE. +export const groups = sqliteTable( + 'groups', + { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + }, + (t) => [uniqueIndex('ux_groups_name').on(t.name)], +); + +export const groupsUsers = sqliteTable( + 'groups_users', + { + groupId: integer('group_id').notNull().references(() => groups.id, { onDelete: 'cascade' }), + userId: integer('user_id').notNull().references(() => customers.id, { onDelete: 'cascade' }), + addedAt: integer('added_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.groupId, t.userId] })], // the pair IS the identity +); + +// Rule 8: JSON document store β€” relational spine + JSON body, a hot field +// lifted into a STORED generated column and indexed. Ports to Postgres jsonb. +type ProfileBody = { priority?: 'low' | 'normal' | 'high'; tags?: string[]; [k: string]: unknown }; + +export const customerProfiles = sqliteTable( + 'customer_profiles', + { + id: integer('id').primaryKey({ autoIncrement: true }), + customerId: integer('customer_id').notNull().references(() => customers.id, { onDelete: 'cascade' }), + body: text('body', { mode: 'json' }).$type<ProfileBody>().notNull(), + // Lifted hot field. json_extract(...) -> Postgres body->>'priority'. + priority: text('priority').generatedAlwaysAs( + sql`json_extract(body, '$.priority')`, + { mode: 'stored' }, + ), + }, + (t) => [ + uniqueIndex('ux_customer_profiles_customer_id').on(t.customerId), + index('ix_customer_profiles_priority').on(t.priority), + ], +);