diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3c1bc60..01127fc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,8 +2,8 @@ -Spec: specs/ - +Spec: specs/ + ## Test plan diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a825c11..8109439 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,9 +13,12 @@ on: jobs: deploy: runs-on: ubuntu-latest - # Only on a successful CI run, and only for main. + # Only on a successful CI run of a push to main. The `event == 'push'` guard is + # load-bearing: without it, a fork PR opened from a branch named "main" satisfies + # `head_branch == 'main'` and would deploy the fork's commit with this repo's secrets. if: >- github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' steps: # When filling in: check out the exact commit CI tested — a `workflow_run` diff --git a/.gitignore b/.gitignore index 66522fe..e51ef38 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ dist/ build/ .output/ .cache/ +coverage/ *.tgz npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* # Environment .env diff --git a/CLAUDE.md b/CLAUDE.md index 83b5bb6..334edde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,13 +8,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co A monorepo with two apps under `apps/*`, infrastructure, and shared DB scripts: -- `apps/backend` — the API server. See `apps/backend/CLAUDE.md`. -- `apps/frontend` — the single-page app. See `apps/frontend/CLAUDE.md`. -- `db/` — top-level **database scripts**: reversible migrations under `db/migrations/` (plus seed/reset scripts). See `db/CLAUDE.md`. -- `infra/` — Terraform for the project's cloud resources. See `infra/CLAUDE.md`. -- `design/` — design mockups / UI reference, **reference only** (not part of the buildable workspace). See *UI mockup / design reference* below. -- `stacks/` — optional stack packs: appendix docs binding the agnostic contracts to one concrete stack; one chosen at instantiation, the rest deleted. Each area's `CLAUDE.md` tells you to read the adopted pack's matching appendix before working there. See `stacks/README.md`. -- `add-ons/` — optional capability add-ons: agnostic patterns for features the base leaves out (test mode, OTP login); zero or more chosen at instantiation, the rest deleted, the active stack pack supplying their concrete bindings. **Every directory kept under `add-ons/` is adopted — read its `README.md` and follow it whenever you touch the capability it covers.** See `add-ons/README.md`. +- `apps/backend` — the API server. Read `apps/backend/CLAUDE.md` before working here. +- `apps/frontend` — the single-page app. Read `apps/frontend/CLAUDE.md` before working here. +- `db/` — database scripts: reversible migrations under `db/migrations/`, plus seed/reset scripts. Read `db/CLAUDE.md` before working here. +- `infra/` — home of the project's Terraform, empty until the first workload. Read `infra/CLAUDE.md` before working here. +- `design/` — UI mockups plus the design guide (`design-guide.html` + `tokens.css`), reference only — not part of the buildable workspace. See `design/README.md`. +- `specs/` — feature specs written before implementation. Convention in `specs/README.md`. +- `stacks/` — optional stack packs binding the agnostic contracts to one concrete stack; one chosen at instantiation, the rest deleted. Each area's `CLAUDE.md` points at the adopted pack's matching appendix; a new adoption starts at the pack's own `README.md`. See `stacks/README.md`. +- `add-ons/` — optional capability add-ons (test mode, OTP login); zero or more kept at instantiation, the rest deleted, the active stack pack supplying their concrete bindings. **Every directory kept under `add-ons/` is adopted — read its `README.md` and follow it whenever you touch that capability.** See `add-ons/README.md`. ## Common commands @@ -34,99 +35,78 @@ A monorepo with two apps under `apps/*`, infrastructure, and shared DB scripts: migrate # TODO: run db/ migrations ``` -**Deployment should go through CI/CD, not a local `deploy` script.** Keep workflows under `.github/workflows/`. A local deploy path may exist for emergencies; do not invoke it as part of normal work. +Deployment goes through CI/CD — workflows under `.github/workflows/` — never a local script. ## Architecture at a glance -### Backend - -**Backend** — an onion with a pure domain at the centre (Domain → Service → Repo/Controller), dependencies pointing inward via ports. Cross-cutting concerns are decorators/aspects, not middleware sprinkled in handlers. **Read `apps/backend/CLAUDE.md` before touching `apps/backend/`.** - -### Frontend - -**Frontend** — store / services / pages / components layering with consistent loading/error/empty/success states and reuse of base UI primitives. **Read `apps/frontend/CLAUDE.md` before touching `apps/frontend/`.** - -### UI mockup / design reference - -Design mockups live in the **`design/`** folder, kept as **reference only** — not part of the buildable workspace. **They are the reference for a screen's *initial build* only.** Use them as the source for visual design, screen inventory, copy, and flows when planning and first building a screen; **don't copy their code** (the mockup's framework is usually not the app's). After the first build, expect the screen to drift as it's iterated and improved — from then on the **running app is the reference, not the mockup**, so don't re-check later changes against it. When planning a *new* screen, point the relevant mockup files at the spec so it starts aligned. +- **Backend** — an onion with a pure domain at the centre (Domain → Service → Repo/Controller), dependencies pointing inward via ports; cross-cutting concerns are decorators/aspects, not middleware sprinkled in handlers. Contract: `apps/backend/CLAUDE.md`. +- **Frontend** — store / services / pages / components layering with consistent loading/error/empty/success states and reuse of base UI primitives. Contract: `apps/frontend/CLAUDE.md`. +- **UI mockups** — `design/` mockups are the reference for a screen's *initial build only*; never copy their code. After the first build, the running app is the reference. Full lifecycle: `design/README.md`. ## Coding standards -These apply to **both** apps and now live next to the code they govern — see the coding-standards material in `apps/backend/CLAUDE.md` (its **Cross-cutting concerns** and **Coding standards** sections) and the **Coding standards** section in `apps/frontend/CLAUDE.md`. In short: keep cross-cutting concerns in shared decorators/plugins (backend) or hooks/services (frontend) rather than duplicating them; keep `utils/`/`lib/` pure and un-peppered; and use real libraries instead of hand-rolling — especially for dates. - -- **Configuration.** All runtime config is read from the environment in one place per app and validated at startup against a declared schema, so a missing or malformed value fails fast with a clear, named error rather than misbehaving mid-request. `.env.example` is the canonical, comment-documented list of every variable, updated in the same change that adds a config key. No inner layer reads config directly — it is passed inward as values. +Per-app standards live next to the code they govern — see `apps/backend/CLAUDE.md` and `apps/frontend/CLAUDE.md`. Cross-app: -### Readability and Naming - -Readable code is a review priority. - -Assess whether names make intent clear without requiring the reviewer to reconstruct meaning from implementation details. - -#### Naming - -- Avoid abbreviations unless they are standard in the domain or codebase. -- Prefer precise names over short names. -- Avoid misleading names. -- Avoid single-letter variables except for trivial loop counters or conventional mathematical usage. -- Use names that reflect business meaning, not only technical mechanics. +- Cross-cutting concerns live in shared decorators/plugins (backend) or hooks/services (frontend) — never duplicated per handler or screen. +- `utils/` / `lib/` stay pure and un-peppered. +- **Configuration.** All runtime config is read from the environment in one place per app and validated at startup against a declared schema, so a missing or malformed value fails fast with a clear, named error. `.env.example` is the canonical, comment-documented list of every variable, updated in the same change that adds a config key. No inner layer reads config directly — it is passed inward as values. +- **Naming.** Readable code is a review priority; names must make intent clear without reconstructing the implementation: no non-standard abbreviations, precise over short, never misleading, no single-letter names outside trivial loop counters or mathematical convention, business meaning over technical mechanics. ## Principles (must follow) -Load-bearing engineering rules; honor them on every change. They are stack- and tooling-agnostic. The first four are adapted from Andrej Karpathy's coding guidelines, folded into this file so no external reference is needed. +Load-bearing engineering rules, stack- and tooling-agnostic (the first four adapted from Andrej Karpathy's coding guidelines). -- **Think before coding.** Don't assume, don't hide confusion, surface tradeoffs. State your assumptions and ask when uncertain; present multiple interpretations rather than silently picking one; suggest simpler alternatives and respectfully push back when warranted; stop and name what's confusing rather than proceeding on unclear requirements. -- **Simplicity first / YAGNI.** The minimum code that solves the problem, nothing speculative — no unrequested features, no abstractions for single-use code, no configurability or error handling for cases that can't occur. Any added complexity (extra project, framework, abstraction layer, build target, third-party SDK, distributed component) must be justified with the simpler alternative explicitly rejected; "we might want X later" is not a justification. If 200 lines could be 50, rewrite it shorter — would an experienced engineer find this unnecessarily complex? -- **Change the right place, surgically.** First identify *where* a change belongs — the correct layer and boundary — and make it there; don't patch wherever is convenient. Keep business logic out of controllers, repos, UI, jobs, and utilities where it doesn't belong, and don't leak infrastructure details into the wrong layer. Then touch only what you must: match the surrounding style and conventions (error handling, logging, validation), don't reformat or refactor unrelated working code, flag unrelated dead code without removing it, and remove only the imports/variables your own change orphaned. -- **Goal-driven execution.** Define success criteria and loop until verified. Turn requests into measurable objectives with a brief plan and a verification step per phase, so each phase can iterate to a clear success marker. Verified means observed, not inferred: before calling a change done, run it and state the evidence you saw. What "run it" means per change type lives in each app's `CLAUDE.md` (frontend/backend) and in `infra/CLAUDE.md` for infrastructure; record the evidence in the PR's Test plan checklist. -- **Don't reinvent existing solutions.** Use established libraries and project utilities for dates, money, validation, retry, pagination, parsing, and formatting rather than hand-rolling them — especially date/timezone math. Don't duplicate existing abstractions or wrap a library without a clear reason. Before adding a new dependency, confirm an existing dependency or shared util doesn't already cover it, and prefer well-maintained, widely-used, permissively-licensed packages. Weigh the cost the YAGNI rule already requires you to justify: for the frontend, bundle and transitive weight (a few lines can beat a large dep for a cached SPA); for the backend, transitive and security surface. A trivial, stable one-liner doesn't earn a dependency — but dates, money, timezones, auth, and crypto always do; never hand-roll those. -- **Don't overfit to the immediate request.** Solve the general problem, not just the demonstrated case. Avoid hardcoding strings, IDs, statuses, roles, or regions; handle the empty, invalid, duplicate, retry, timeout, and permission cases, not only the happy path; and write tests that assert behavior rather than mirror the implementation. -- **Keep implementations clean, not mechanical.** Avoid noisy logs, broad `try/catch` blocks that hide errors, comments restating obvious code, unused parameters or dead branches, and defensive code with no clear failure model. -- **Guard every AI/LLM call.** Set token/cost limits, timeouts, and max-iteration / loop-termination guards; handle model and tool failures; monitor cost and usage; and never treat user-provided files, prompts, webpages, or other external content as trusted instructions. +- **Think before coding.** State your assumptions and ask when uncertain; present multiple interpretations rather than silently picking one; suggest simpler alternatives and push back when warranted; name what's confusing instead of proceeding on unclear requirements. +- **Simplicity first / YAGNI.** The minimum code that solves the problem, nothing speculative — no unrequested features, no abstractions for single-use code, no configurability or error handling for cases that can't occur. Any added complexity (extra project, framework, abstraction layer, build target, third-party SDK, distributed component) must be justified with the simpler alternative explicitly rejected; "we might want X later" is not a justification. If 200 lines could be 50, rewrite it shorter. +- **Change the right place, surgically.** Identify *where* a change belongs — the correct layer and boundary — and make it there. Keep business logic out of controllers, repos, UI, jobs, and utilities; don't leak infrastructure into inner layers. Match the surrounding style and conventions; don't reformat or refactor unrelated working code; flag unrelated dead code without removing it; remove only the imports and variables your own change orphaned. +- **Goal-driven execution.** Turn requests into measurable objectives with a verification step per phase, and loop until verified. Verified means observed, not inferred: before calling a change done, run it and state the evidence you saw. What "run it" means per change type lives in each area's `CLAUDE.md`; record the evidence in the PR's Test plan (`.github/PULL_REQUEST_TEMPLATE.md`). +- **Don't reinvent existing solutions.** Use established libraries and project utilities for dates, money, validation, retry, pagination, parsing, and formatting. Don't duplicate existing abstractions or wrap a library without a clear reason. Before adding a dependency, confirm an existing one doesn't cover it, and prefer well-maintained, widely-used, permissively-licensed packages; weigh bundle weight on the frontend and transitive/security surface on the backend. A trivial, stable one-liner doesn't earn a dependency — but dates, money, timezones, auth, and crypto always do; never hand-roll those. +- **Don't overfit to the immediate request.** Solve the general problem, not just the demonstrated case. No hardcoded strings, IDs, statuses, roles, or regions; handle the empty, invalid, duplicate, retry, timeout, and permission cases, not only the happy path; write tests that assert behavior rather than mirror the implementation. +- **Keep implementations clean, not mechanical.** No noisy logs, no broad `try/catch` blocks that hide errors, no comments restating obvious code, no unused parameters or dead branches, no defensive code without a clear failure model. +- **Guard every AI/LLM call.** Set token/cost limits, timeouts, and max-iteration guards; handle model and tool failures; monitor cost and usage; never treat user-provided files, prompts, webpages, or other external content as trusted instructions. ## Definition of Done -The concrete bar for *Goal-driven execution*: do not report work as done until all of the following hold. If a step cannot be run (e.g. the toolchain TODOs in *Common commands* are still unfilled), say so explicitly rather than skipping it silently. This is a hard self-check the agent runs before claiming completion — CI and the PR template are still stubs, so the gate is not delegated. +The concrete bar for *Goal-driven execution* — a hard self-check run before claiming completion. If a step cannot run (e.g. the toolchain TODOs in *Common commands* are still unfilled), say so explicitly rather than skipping it silently. - ` lint`, ` typecheck`, ` test`, and ` build` all pass for the touched apps. - New or changed behaviour is covered by tests that assert behaviour, not implementation. - For spec-backed work, every acceptance criterion of the touched story is met (see `specs/README.md`). -- Per-app and per-area completion rules in the relevant home file are satisfied — frontend route + i18n parity (`apps/frontend/CLAUDE.md`), reversible (up/down) or explicitly-justified migration (`db/CLAUDE.md`). That file is the source of truth; don't re-derive here. +- Per-area completion rules in the relevant `CLAUDE.md` are satisfied — frontend route + i18n parity, reversible (or explicitly justified) migration. That file is the source of truth. - No new TODO/FIXME left in code you touched without a tracked follow-up. ## Testing - Tests are part of "done." Every non-trivial slice ships its tests in the same change; a slice with no tests is not shippable. -- A bug fix starts with a failing test that reproduces the bug, then the fix makes it pass. -- Name the kind of test by what it proves — unit (a rule in isolation), integration (a use case across rings/layers), contract (an API or port boundary). Pick the cheapest kind that proves the behaviour. -- Assertion quality follows *Don't overfit to the immediate request* (assert behaviour, not implementation); test placement and per-ring/per-layer coverage live in each app's `CLAUDE.md`. +- A bug fix starts with a failing test that reproduces the bug; the fix makes it pass. +- Name the kind of test by what it proves — unit (a rule in isolation), integration (a use case across rings/layers), contract (an API or port boundary) — and pick the cheapest kind that proves the behaviour. +- Assert behaviour, not implementation; test placement and per-ring/per-layer coverage live in each app's `CLAUDE.md`. ## Development workflow -How work flows from spec to merge. These two rules are load-bearing; the worktree mechanics below are how they're carried out day to day. - -- **Spec-first, independently testable slices.** Non-trivial features start from a short written spec before implementation, kept under `specs/`. User stories are priority-tagged (P1 = MVP) and each slice is shippable / demoable on its own; P1 alone is a viable MVP. Avoid cross-story coupling that breaks that independence. Keep this discipline regardless of which spec tool (if any) you use. -- **Trunk-based, linear history.** A single long-lived integration branch, `main`. Feature work happens on short-lived branches (see *Working in a git worktree* below); rebase / fast-forward onto trunk to keep history linear. Trunk stays releasable — hide incomplete work behind a flag. A flag here is a boolean key in the app's validated config schema (see *Configuration*), default off — no flag service or SDK unless a project explicitly adopts one and records the choice. Keep PRs small where practical. Commits: imperative subject, one logical change per commit; follow the repo's existing Conventional Commits prefix style (feat/fix/docs/refactor/test/chore, optional scope) so history stays scannable. +- **Spec-first, independently shippable slices.** Non-trivial features start from a short written spec under `specs/` before implementation; stories are priority-tagged and P1 alone is a viable MVP. Convention and slice rules: `specs/README.md`. +- **Trunk-based, linear history.** A single long-lived integration branch, `main`. Feature work happens on short-lived branches in worktrees (below); rebase / fast-forward onto trunk to keep history linear. Trunk stays releasable — hide incomplete work behind a flag: a boolean key in the app's validated config schema (see *Configuration*), default off; no flag service or SDK unless a project explicitly adopts one and records the choice. Keep PRs small. Commits: imperative subject, one logical change per commit, Conventional Commits prefixes (feat/fix/docs/refactor/test/chore, optional scope). ### Self-review before merge -Before opening a PR or merging, read your **full diff** end to end — as a reviewer would, including files you don't remember touching — and confirm it satisfies the rules already stated above and in the relevant `apps/*/CLAUDE.md`: the change lives in the correct layer/ring with no business logic leaked outward, no unrelated code was reformatted, and only imports your own change orphaned were removed (see *Change the right place, surgically*), and names reflect business meaning (see *Readability and Naming*). Don't merge on memory of what you edited; re-read what actually changed. +Before opening a PR or merging, read your **full diff** end to end — as a reviewer would, including files you don't remember touching — and confirm it satisfies the rules above and in the relevant area `CLAUDE.md`. Never merge on memory of what you edited. ### Working in a git worktree -Worktrees are the **default** here — most work runs in parallel with Claude across several worktrees at once. Feature work happens in a git worktree under `.claude/worktrees/` (or your preferred location) on its own short-lived branch. +Worktrees are the **default** — work runs in parallel across several worktrees at once, under `.claude/worktrees/` on short-lived branches. - **Before anything else in a new worktree, copy over all gitignored runtime config** — a fresh worktree is created without it (root `.env`, any `apps/*/.env*`, local secrets) and anything depending on it will silently misbehave. From the worktree root: `main="$(git worktree list --porcelain | sed -n 's/^worktree //p' | head -1)"; for f in .env apps/backend/.env apps/frontend/.env; do if [ -f "$main/$f" ]; then cp "$main/$f" "./$f" && echo "copied $f"; else echo "not in main checkout (skipped): $f"; fi; done` — it reports each file so a missing one is visible, not silent. Copy every gitignored env file your project uses, not only the three listed. -- Shared local infrastructure (a containerized DB, etc.) is typically **shared** across worktrees by a fixed name — starting a second copy will conflict; reuse the running one. -- The shared DB's schema is **global state** across worktrees — a migration, reset, or seed run in one worktree changes every worktree's app. Don't run a reset or destructive migration check while a parallel worktree depends on the current schema; use a throwaway DB for round-trip/destructive checks. +- Shared local infrastructure (a containerized DB, etc.) is **shared** across worktrees by a fixed name — reuse the running instance; never start a second copy. +- The shared DB's schema is global state across worktrees — rules in `db/CLAUDE.md`. **When the work is done** — an ordered merge-back gate; the moment trunk is mutated is the moment quality is enforced: -1. **Rebase** the branch onto the current default branch and resolve any conflicts — pulling in changes that landed on trunk while you worked. -2. On the rebased branch, **run the full lint + typecheck + test + build suite and confirm it passes** — never merge red. The suite must run on the integrated state (after the rebase, not before). If no suite exists yet (toolchain still TODO), say so explicitly per the Definition of Done. -3. **Fast-forward merge** into the default branch (the rebase makes this a clean ff, preserving linear history). +1. **Rebase** the branch onto the current default branch and resolve any conflicts. +2. On the rebased branch, **run the full lint + typecheck + test + build suite and confirm it passes** — never merge red. The suite runs on the integrated state (after the rebase, not before). If no suite exists yet (toolchain still TODO), say so explicitly per the Definition of Done. +3. **Fast-forward merge** into the default branch. 4. **Stop** any dev servers / test instances started for the work. 5. **Delete** the worktree (`git worktree remove`) and its merged branch. -6. **Push** the default branch only after confirming. By default this template's `.github/workflows/deploy.yml` runs after a green CI run on `main` (a `workflow_run` trigger), so once its deploy step is filled in a push to the default branch ships to the configured target — confirm with the user before pushing, and check `deploy.yml` if the trigger has been changed. +6. **Push** the default branch only after confirming with the user — `.github/workflows/deploy.yml` runs after a green CI run on `main`, so once its deploy step is filled in, a push ships to the configured target. Check `deploy.yml` if the trigger has been changed. ## Learnings diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53ee9ae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cavalry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 24d1605..469f90c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,23 @@ -# cavalry-template-spa + + + Cavalry + **An opinionated template for spinning up production-ready, full-stack projects — fast, and without re-litigating a single engineering decision.** -Every new Cavalry Collective project starts here. Clone it, run the Day-1 checklist once, and start shipping features the same day — with the architecture, quality gates, and conventions of a mature codebase already in force. +Every new Cavalry project starts here. Clone it, run the Day-1 checklist once, and start shipping features the same day — with the architecture, quality gates, and conventions of a mature codebase already in force. ## Why this exists Most project templates give you scaffolding: a folder of generated code that's stale the week after it's cut. This template ships something more durable — **contracts**. A set of `CLAUDE.md` files encode how software is built here: a backend onion with a pure domain at the centre, a layered frontend with a design-token keystone, reversible migrations, spec-first slices, and a Definition of Done where *verified means observed, not inferred*. -The contracts are written for humans **and** for AI agents. An agent working in this repo auto-loads the contract for whatever area it touches, so the hundredth feature is built to the same standard as the first — whether a person or an agent wrote it. That is the entire bet: **the biggest lever on project quality is an opinionated approach, stated where the work happens.** +The contracts are written for humans **and** for AI agents. An agent working in this repo auto-loads the contract for whatever area it touches (`CLAUDE.md` files load automatically — the root at session start, each area's the moment the agent works in that directory; every other document is read where a loaded file points at it), so the hundredth feature is built to the same standard as the first — whether a person or an agent wrote it. That is the entire bet: **the biggest lever on project quality is an opinionated approach, stated where the work happens.** ## The ideology - **Opinionated where it matters, agnostic where it doesn't.** The base contracts pin the *shape* of the system — rings, layers, envelopes, gates — and deliberately not the framework. A **stack pack** (`stacks/`) then binds those contracts to one concrete stack, resolving every disagreement in an explicit conflict register. No silent contradictions. -- **Simplicity first.** The minimum code that solves the problem; every added abstraction must defeat the simpler alternative on the record. If 200 lines could be 50, it's 50. -- **Quality is a gate, not a vibe.** Nothing is "done" until it's been run and observed: four data states exercised, endpoints hit, migrations round-tripped, screens checked at 320 px. The design guide (`design/`) locks the visual system *before* the first screen is built. +- **Simplicity first.** The minimum code that solves the problem; every added abstraction must beat the simpler alternative on the record — and shorter wins. +- **Quality is a gate, not a vibe.** Nothing is "done" until it's been run and observed: four data states exercised, endpoints hit, migrations round-tripped, screens checked at the narrowest supported width. The design guide (`design/`) locks the visual system *before* the first screen is built. - **Spec-first, independently shippable slices.** Non-trivial work starts as a short written spec under `specs/`; P1 stories alone form a viable MVP. Trunk stays releasable, history stays linear. - **Instructions over machinery.** The template carries no build scripts, hooks, or generated artifacts — just precise instructions in the files agents and humans already read. What you see is the whole mechanism. @@ -35,12 +38,12 @@ That's it. There is nothing to install and no generator to run — the template | `CLAUDE.md` | Root architecture principles and workflow | | `apps/backend/` | Backend app — onion architecture (Domain → Service → Repo → Controller) | | `apps/frontend/` | Frontend SPA — layered store / services / pages / components | -| `db/` | Database & migration contract (`db/CLAUDE.md`) with reversible migrations under `db/migrations/` | -| `infra/` | Terraform infrastructure (GCP-first conventions; adaptable) | -| `design/` | UI mockups + the **design guide** (`design-guide.html` + `tokens.css`) — "Keystone", the visual keystone confirmed before UI work: design principles + full foundations as a token-driven SaaS system (Cavalry palette by default), rebranded per project; components deliberately left flexible; reference, not part of the build | +| `db/` | Database & migration contract, reversible migrations under `db/migrations/` | +| `design/` | UI mockups + the **Keystone design guide** — confirmed before any UI work (see step 10) | +| `infra/` | Terraform conventions and guardrails | | `specs/` | Feature specs — written before implementation | -| `stacks/` | Optional stack packs — appendix docs binding the agnostic contracts to one concrete stack; one chosen at instantiation, the rest deleted. See [`stacks/README.md`](stacks/README.md) | -| `add-ons/` | Optional capability add-ons — agnostic patterns you opt into at Day-1 (test mode, OTP login); the active stack pack supplies their concrete bindings. See [`add-ons/README.md`](add-ons/README.md) | +| `stacks/` | Optional stack packs — one chosen at instantiation, the rest deleted ([`stacks/README.md`](stacks/README.md)) | +| `add-ons/` | Optional capabilities — test mode, OTP login — opted into at Day-1 ([`add-ons/README.md`](add-ons/README.md)) | | `.github/workflows/` | CI and deploy stubs — fill in your toolchain commands | | `project.code-workspace` | VS Code workspace (hides agent worktrees from search and watchers) | @@ -54,13 +57,13 @@ The template is intentionally framework-agnostic. You choose: - Cloud provider and Terraform provider - Database client -Pick what fits the project. The CLAUDE.md files tell you where things go and how to structure them — not which library to use. +Pick what fits the project. The CLAUDE.md files tell you where things go and how to structure them — not which library to use. (The template is most at home in a JavaScript/TypeScript ecosystem — the toolchain verbs, parity checks, and shipped packs are JS-shaped — but the architectural contracts themselves carry to any stack.) -Or choose a stack pack under `stacks/` (e.g. `nextjs-nestjs-postgres`) for a vetted set of these choices plus copy-paste commands; the base CLAUDE.md files stay framework-agnostic. The pack is opt-in, not a mandate — see [`stacks/README.md`](stacks/README.md). +Or choose a stack pack under `stacks/` for a vetted set of these choices plus copy-paste commands. Three ship today: **`nextjs-nestjs-postgres`** (server-first Next.js · NestJS · Postgres/Prisma), **`taro-fastify-mysql-tencent`** (Taro H5 · Fastify · MySQL on Tencent Cloud), and **`vercel`** (Next.js · Fastify · Neon Postgres on Vercel). Two add-ons ship today: **`test-mode`** and **`otp-auth`**. The base CLAUDE.md files stay framework-agnostic either way — a pack is opt-in, not a mandate ([`stacks/README.md`](stacks/README.md)). ## Day-1 checklist -Run this once, top to bottom, the first time you instantiate the template. Each step names the file and the marker to replace. The placeholders are grep-able: `` in the root `CLAUDE.md` command block, `FILL IN ON SETUP` in `apps/frontend/CLAUDE.md`, and `TODO: replace` in the `.github/workflows/` stubs. Step 13 checks they are all gone. +Run this once, top to bottom, the first time you instantiate the template. Each step names the file and the marker to replace. The placeholders are grep-able: `` in the root `CLAUDE.md` command block, `FILL IN ON SETUP` in `apps/frontend/CLAUDE.md`, and `TODO:` in the `.github/workflows/` stubs. Step 14 checks they are all gone. 1. **Create the repo.** Click **Use this template** → **Create a new repository** on GitHub. 2. **Clone** your new repo. @@ -71,26 +74,35 @@ Run this once, top to bottom, the first time you instantiate the template. Each - [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) — frontend layering and conventions - [`db/CLAUDE.md`](db/CLAUDE.md) — database & migration contract - [`infra/CLAUDE.md`](infra/CLAUDE.md) — Terraform authoring style and guardrails -5. **Choose a stack pack — or stay agnostic.** +5. **Start with a clean `specs/`.** If numbered spec directories from the template's own development are present under `specs/`, delete them (keep `specs/README.md`) — they document building this template, not your project. +6. **Choose a stack pack — or stay agnostic.** - **Pack path (fast):** pick the pack under `stacks/` matching your stack (e.g. `nextjs-nestjs-postgres`), then: - `rm -rf` every other `stacks/*` directory — the one pack left is the adopted one; each area's `CLAUDE.md` already points agents at its appendices (mechanism: `stacks/README.md` *Activation*). - Copy the pack README **dev** command block into the root `CLAUDE.md` "Common commands" placeholder (delete the banner); copy its **CI** block into `.github/workflows/ci.yml`. They are different blocks — never paste a dev-only migration command into CI. - Record the choice in root `CLAUDE.md` **Learnings**: `Stack: ; appendices under stacks//`. - - **Agnostic path:** keep `stacks/` for reference (or delete it) and fill in the toolchain yourself — see step 6. -6. **Choose your add-ons.** Under `add-ons/`, keep the optional capabilities you want (`test-mode`, `otp-auth`, …) and **delete the directories you don't** — every directory kept is adopted, and the root `CLAUDE.md` points agents at each kept add-on's README. The active stack pack supplies each adopted add-on's concrete bindings. See [`add-ons/README.md`](add-ons/README.md). -7. **Fill the toolchain placeholders** (agnostic path; the pack does this for you in step 5): + - **Agnostic path:** keep `stacks/` for reference (or delete it) and fill in the toolchain yourself — see step 8. +7. **Choose your add-ons.** Under `add-ons/`, keep the optional capabilities you want (`test-mode`, `otp-auth`, …) and **delete the directories you don't** — every directory kept is adopted, and the root `CLAUDE.md` points agents at each kept add-on's README. The active stack pack supplies each adopted add-on's concrete bindings. See [`add-ons/README.md`](add-ons/README.md). +8. **Fill the toolchain placeholders** (agnostic path; the pack does this for you in step 6): - Root `CLAUDE.md` "Common commands" — replace the seven ``/`TODO` commands and delete the PLACEHOLDER banner. - `.github/workflows/ci.yml` — replace the TODO steps with real install/lint/typecheck/test/build, plus the i18n key-parity check and migration up/down round-trip. - - `.github/workflows/deploy.yml` — replace the TODO step. + - `.github/workflows/deploy.yml` — replace the TODO step (keep the `event == 'push'` guard in the job condition — it prevents fork PRs from triggering a deploy). - Add a real `.env.example` (already whitelisted in `.gitignore`). -8. **Declare the primary form factor.** In `apps/frontend/CLAUDE.md`, fill in the form-factor line: +9. **Declare the primary form factor.** In `apps/frontend/CLAUDE.md`, fill in the form-factor line: ```markdown **Primary form factor (FILL IN ON SETUP):** `` ``` -9. **Rebrand & confirm the design guide — before building any screen.** The template ships **Keystone** (`design/design-guide.html` + `design/tokens.css`): design principles plus the full foundations — colour, type, spacing, layout, elevation, motion, states, content, data formatting — as a token-driven SaaS system shipping the Cavalry palette by default (components deliberately left flexible per app). Rebrand it — edit the **primitive** tier in `tokens.css`, or have your AI assistant regenerate it from your brand — then open the guide in a browser and confirm it reads as one coherent system. This is the visual keystone gate (`apps/frontend/CLAUDE.md` → *Design guide*); the app's token source and `atoms/` then implement what it shows — don't build screens against an unconfirmed system. -10. **Copy runtime config.** Copy any gitignored runtime config (`.env`, secrets) into your local checkout — it is not carried over from the template. -11. **Protect `main`.** Add a branch protection rule / ruleset requiring the CI workflow to pass before merge. Trunk must stay releasable — and on packs whose pipeline ships whatever lands on `main` (e.g. `vercel`), green-CI-before-merge *is* the deploy gate. -12. **Stand up staging (if your pack defines one).** Bring up the persistent preview/staging environment your stack pack specifies before feature work — for the `vercel` pack that is the `develop` branch plus its dedicated Neon branch (`stacks/vercel/infra.md` → *Staging environment*), migrated with the same manual runbook as prod (`stacks/vercel/db.md` → *Production & staging migrations*). -13. **Confirm green.** Push and watch the first CI run pass. Then confirm no placeholder survives — both must return nothing: `grep -rn 'FILL IN ON SETUP\|TODO: replace' . --exclude-dir=stacks --exclude-dir=specs --exclude-dir=.git --exclude=README.md` and `grep -n '^ ' CLAUDE.md`. (This README's own checklist names the markers, so it is excluded; delete it once instantiation is done if you prefer a clean tree.) +10. **Rebrand & confirm the design guide — before building any screen.** The template ships **Keystone** (`design/design-guide.html` + `design/tokens.css`): design principles plus the full foundations — colour, type, spacing, layout, elevation, motion, states, content, data formatting — as a token-driven SaaS system shipping the Cavalry palette by default (components deliberately left flexible per app). Rebrand it — edit the **primitive** tier in `tokens.css`, or have your AI assistant regenerate it from your brand — then open the guide in a browser and confirm it reads as one coherent system. This is the visual keystone gate (`apps/frontend/CLAUDE.md` → *Design guide*); the app's token source and `atoms/` then implement what it shows — don't build screens against an unconfirmed system. Replace the Cavalry brand assets under `design/brand/` with your own (and swap the lockup at the top of this README). +11. **Copy runtime config.** Copy any gitignored runtime config (`.env`, secrets) into your local checkout — it is not carried over from the template. +12. **Protect `main`.** Add a branch protection rule / ruleset requiring the CI workflow to pass before merge. Trunk must stay releasable — and on packs whose pipeline ships whatever lands on `main` (e.g. `vercel`), green-CI-before-merge *is* the deploy gate. +13. **Stand up staging (if your pack defines one).** Bring up the persistent preview/staging environment your stack pack specifies before feature work — the pack's `infra.md` names the environment and its runbook. +14. **Confirm green.** Push and watch the first CI run pass. Then confirm no placeholder survives — all three must return nothing: `grep -rn 'FILL IN ON SETUP' . --exclude-dir=stacks --exclude-dir=specs --exclude-dir=.git --exclude=README.md`, `grep -n 'TODO:' .github/workflows/*.yml`, and `grep -n '^ ' CLAUDE.md`. (This README's own checklist names the markers, so it is excluded; delete it once instantiation is done if you prefer a clean tree.) -> If you chose the server-first `nextjs-nestjs-postgres` pack, soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". The repo name still encodes "spa" and is immutable — accepted as stale. +> If you chose the server-first `nextjs-nestjs-postgres` pack, soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". + +## License + +[MIT](LICENSE) © Cavalry. Projects created from this template may keep or replace the license — the template itself stays free to use, copy, and adapt. + +--- + +Built and maintained by **[Cavalry](https://cavalry.sg)** — senior engineers pairing with AI to ship software we answer for. This template is how we start everything we build. diff --git a/add-ons/README.md b/add-ons/README.md index 985120f..1560ec4 100644 --- a/add-ons/README.md +++ b/add-ons/README.md @@ -15,7 +15,7 @@ A directory `add-ons//` with a `README.md` of agnostic guidance. `` ## Opt in — adoption is keeping the directory -Keep the add-ons you want under `add-ons/`, delete the directories you don't. Opting out *is* deleting the directory; every directory still present is adopted. The Day-1 checklist (root `README.md`) is where a fresh project chooses. +Keep the add-ons you want under `add-ons/`; opting out *is* deleting the directory — every directory still present is adopted. The Day-1 checklist (root `README.md`) is where a fresh project chooses. Activation is by instruction: the root `CLAUDE.md` tells agents to read every kept add-on's `README.md` and follow it when touching the capability it covers. Add-ons are cross-cutting (backend + frontend + db at once), so the pointer lives in the always-loaded root file rather than a per-area one. The README under `add-ons/` is the single source of truth — edit it in place; there is no generated copy. diff --git a/add-ons/otp-auth/README.md b/add-ons/otp-auth/README.md index 0cd457a..7f278fc 100644 --- a/add-ons/otp-auth/README.md +++ b/add-ons/otp-auth/README.md @@ -21,6 +21,10 @@ One-time-code auth: a user proves control of a phone or email by entering a code ## Make it robust +- **Generate codes with a CSPRNG, minimum 6 digits** — never `Math.random()` or anything timestamp-derived. +- **A code is single-use** — consume it on a successful verify; a consumed code never verifies again. +- **TTL is minutes, not hours.** +- **Cap failed verify attempts per challenge** (e.g. 5); past the cap, invalidate the challenge and require a fresh send. - **Idempotent verify.** A retry or double-submit must never create a second account or double-consume. Put a unique constraint on the natural key (target + purpose) so the race resolves to `409`, and have the client treat `409` as "already done, proceed". - **A knowable test code.** Gate a knowable code behind **test mode** (a logged real code, or a fixed code valid *only* in test mode) so the flow is walkable without a live provider. The verify path still runs — only delivery is stubbed. - **Log every send and verify** with `{purpose, masked target, test-mode, provider status, correlation id}` — never the code or full contact. diff --git a/apps/backend/CLAUDE.md b/apps/backend/CLAUDE.md index 3606f50..62c3f12 100644 --- a/apps/backend/CLAUDE.md +++ b/apps/backend/CLAUDE.md @@ -1,58 +1,57 @@ # Backend -The backend contract — read before touching anything under `apps/backend/`. Repo-wide rules (principles, workflow, cross-app standards) live in the root `CLAUDE.md`. Stack: not yet chosen (see the root `CLAUDE.md`). **If a stack pack is adopted (a single directory kept under `stacks/`), also read its `backend.md` appendix before working here** — it adds the concrete bindings, and its conflict register resolves any disagreement with this file, for that stack only. The examples below use JS-style filenames (`container.js`) and an Express/Fastify-style HTTP layer **illustratively**; treat file extensions and framework specifics as examples, not mandates — the same way `apps/frontend/CLAUDE.md` does. +The backend contract — read before touching anything under `apps/backend/`. Repo-wide rules (principles, workflow, cross-app standards) live in the root `CLAUDE.md`. Stack pack adopted? Read its `backend.md` appendix first — precedence rules in `stacks/README.md`. Examples below (JS-style filenames such as `container.js`, an Express/Fastify-style HTTP layer) are illustrative, not mandates. -The backend is an **onion**: a pure domain at the centre, wrapped by rings that depend inward toward it. Everything below follows from that. +The backend is an **onion**: a pure domain at the centre, wrapped by rings that depend inward toward it. ## The dependency rule **Dependencies point inward. Nothing in an inner ring knows anything about an outer ring.** - The domain depends on nothing; each outer ring depends only on the rings inside it. -- When an inner ring needs an outer capability (load data, send mail), it defines the **contract** — a "port" — and an outer ring provides the implementation. A port is a documented set of method signatures: in a language with first-class interfaces (e.g. TypeScript) express it as an interface; in one without (e.g. plain JS) express it as an agreed shape honoured by duck typing and optionally pinned with a JSDoc `@typedef`. Either way the implementation is supplied from outside (see *Wiring*), so the dependency is inverted and the arrow still points inward. -- Data crosses in translated form: DTOs at the edge, domain objects inside. Outer-ring types — HTTP requests, database rows, SDK objects — never travel inward; the domain neither imports nor names them. - -Test any boundary: can you swap what's outside (the database, the delivery mechanism) without touching what's inside? If not, something has leaked. If a change wants to point a dependency outward, reshape the change, not the rule. +- When an inner ring needs an outer capability (load data, send mail), it defines a **port** — an interface in a typed language, or an agreed duck-typed shape (optionally pinned with a JSDoc `@typedef`) in an untyped one. The implementation is supplied from outside (see *Wiring*), so the arrow still points inward. +- Data crosses boundaries translated: DTOs at the edge, domain objects inside. HTTP requests, database rows, and SDK objects never travel inward; the domain neither imports nor names them. +- Boundary test: what's outside (the database, the delivery mechanism) must be swappable without touching what's inside. If a change wants to point a dependency outward, reshape the change, not the rule. ## The four rings -From the centre out: **Domain → Service → Repo / Controller**. Repo and Controller are both outer adapters; neither depends on the other. The outer three keep the project's familiar controller / service / repo names; the domain at the centre is the DDD core they build on. - -For each ring: what it holds, what it may depend on, what it must never do. +From the centre out: **Domain → Service → Repo / Controller**. Repo and Controller are peer outer adapters; neither depends on the other. ### Domain — the core -The business expressed in code: entities, value objects, domain services, and the invariants and rules true regardless of how the system is delivered or stored. It also defines the **ports** — the repository and gateway contracts the outer rings implement. +The business expressed in code: entities, value objects, domain services, and the invariants true regardless of how the system is delivered or stored. Defines the **ports** the outer rings implement. - **Depends on:** nothing. Pure and stateless — no I/O, no database handle, no clock, no network, no framework types. -- **Never:** performs I/O or names a specific technology. A rule that can only be tested by standing up a database is in the wrong shape — express it so it can be tested in isolation. +- **Never:** performs I/O or names a specific technology. +- Express rules so they test in isolation — a rule testable only by standing up a database is mis-shaped. +- Validate state transitions against the rules **before** applying a status or lifecycle change — never merely because an external request, callback, message, or event asked for it. -Keep this ring small, dense, and protected. It's the part worth defending. +Keep this ring small, dense, and protected. ### Service — use cases -Orchestrates one use case end to end: load through the domain's repository ports, invoke the domain, persist the result. Owns the transaction boundary — **one transaction per use case**. +Orchestrates one use case end to end: load through the domain's ports, invoke the domain, persist the result. Owns the transaction boundary — **one transaction per use case**. - **Depends on:** the domain and the ports it defines — nothing concrete. -- **Never:** touches HTTP/web concepts, builds queries, or reaches for framework globals. Orchestration lives here; the *rules* live in the domain. +- **Never:** touches HTTP concepts, builds queries, or reaches for framework globals. Orchestration lives here; the rules live in the domain. ### Repo — adapters outward -Concrete implementations of the ports the inner rings define: repositories backed by the database, plus clients for external services (storage, mail/SMS, payments, translation). Each adapter carries a **mapper** that translates between the outside shape (a DB row, an external payload) and domain objects — so storage shapes stop at this boundary. +Implements the ports: repositories backed by the database, plus clients for external services (storage, mail/SMS, payments). Each adapter carries a **mapper**, so storage and external shapes stop at this boundary. -- **Depends on:** the inner rings, to *implement* their ports. -- **Never:** holds business rules or decisions. Adapters move and translate data across the boundary; branching beyond what a query or call needs means a rule has leaked out of the domain. +- **Depends on:** the inner rings, to implement their ports. +- **Never:** holds business rules or decisions. Branching beyond what a query or call needs means a rule has leaked out of the domain. ### Controller — delivery inward -The edge where the outside world meets the app: REST handlers, request/response DTOs, and the auth guards protecting them. A handler validates input, invokes **one** use case, and maps the result back out. +The edge: REST handlers, request/response DTOs, and the auth guards protecting them. A handler validates input, invokes **one** use case, and maps the result back out. - **Depends on:** the service ring it invokes. - **Never:** holds business logic, transactions, or queries, or reaches past the service into the repo ring. ## Folder layout -Organise **by feature first, layers within**: each domain area is a self-contained module owning its four rings, with shared building blocks and the wiring beside the modules. (A small module needn't use every folder — add a ring's folder when it earns one.) +Organise **by feature first, layers within**: each domain area is a self-contained module owning its rings; add a ring's folder only when it earns one. ``` apps/backend/ @@ -71,38 +70,39 @@ apps/backend/ └─ tests/ # mirrors src/ (or co-locate per module) ``` -A module never imports another module's inner rings — cross-module use goes through the other module's service or a shared port. Within a module, dependencies point inward: `controller/ → service/ → domain/`, and `repo/ → domain/` (implementing its ports). `shared/utils/` depends on nothing; `shared/aspects/` wrap a ring and depend inward only. +- A module never imports another module's inner rings — cross-module use goes through the other module's service or a shared port. +- Within a module, dependencies point inward: `controller/ → service/ → domain/`, and `repo/ → domain/` (implementing its ports). +- `shared/utils/` is pure and stateless — no I/O, no framework. `shared/aspects/` wrap a ring and depend inward only. ## Wiring -Ports are defined inside, implemented outside, and connected in one place — the **composition root** (`container.js`). This is how the backend does dependency inversion (see *The dependency rule* for the typed/untyped port forms): the contract is the agreed method shape, and the concrete implementation is supplied at boot. +Ports are defined inside, implemented outside, and connected in one place — the **composition root** (`container.js`). -- An inner ring receives its dependencies (a constructor argument or factory parameter); it never `import`s a concrete adapter directly. -- The composition root is the only place that knows both a port and its implementation. Swapping an adapter (real database → in-memory for a test) is a change there and nowhere else. -- Keep wiring out of the rings — it is glue, not logic. Manual constructor wiring is enough; reach for a DI container (e.g. Awilix) only once the graph grows unwieldy. +- An inner ring receives its dependencies as constructor or factory arguments; it never imports a concrete adapter. +- The composition root is the only place that knows both a port and its implementation; adapter swaps (real database → in-memory for a test) happen there and nowhere else. +- Manual constructor wiring by default; adopt a DI container only once the graph grows unwieldy. ## Testing the rings -The architecture exists to make testing cheap — exploit it. Each ring maps to a kind of test; **most coverage sits in the fast inner rings**, thinning outward. +Each ring maps to a kind of test; **most coverage sits in the fast inner rings**, thinning outward. -- **Domain — pure unit tests.** No mocks, no I/O (the ring forbids I/O, so its tests need none). Assert the invariants and rules directly. -- **Service — use-case tests.** Drive the use case with in-memory fakes of the ports (the composition-root swap described in *Wiring*); assert orchestration and transaction boundaries, not the database. +- **Domain — pure unit tests.** No mocks, no I/O. Assert the invariants and rules directly. +- **Service — use-case tests.** Drive the use case with in-memory fakes of the ports; assert orchestration and transaction boundaries, not the database. - **Repo — integration tests.** Run against a real database / external sandbox; assert the mapper round-trips and the queries behave. -- **Controller — contract tests.** Assert status codes, validation rejection, auth guards, and request/response schema (see *Endpoint contract* and *Status codes*). +- **Controller — contract tests.** Assert status codes, validation rejection, auth guards, and request/response schemas. ## Verifying a change Before calling a backend change done (the root's *verified means observed* gate): -- Run the test suite for the touched module. -- Exercise the actual endpoint over HTTP — the happy path plus at least one error path. -- Confirm the status code, error shape, and correlation id match the *Endpoint contract* and *Cross-cutting* error rules already defined here. - -State what you observed (which paths you exercised, what you saw), not just that you ran it. +- Run the touched module's tests. +- Exercise the endpoint over HTTP — the happy path plus at least one error path. +- Confirm the status code, error shape, and correlation id match the *Endpoint contract* and *Error responses* rules. +- State what you observed (which paths, what you saw), not just that you ran it. ## RESTful conventions -These govern the **controller** ring — the default API contract; prefer a more specific project rule where one exists. +These govern the **controller** ring. ### Resource naming @@ -148,11 +148,11 @@ Every error response uses one envelope, produced only by the single error-mappin ``` - **`code`** — stable, machine-readable, `SCREAMING_SNAKE_CASE`, named in domain terms. Clients branch on `code`; they never parse `message`. -- **`message`** — human-readable and safe: no stack traces, SQL, or internal identifiers (what users actually read is governed by the frontend's error-copy rules). -- **`correlationId`** — the request's correlation id. It also travels as the **`x-correlation-id` response header on every response**, success or failure; the body field is the copy the frontend surfaces (see *Cross-app conventions* in `apps/frontend/CLAUDE.md`). +- **`message`** — human-readable and safe: no stack traces, SQL, or internal identifiers. +- **`correlationId`** — the request's correlation id. It also travels as the **`x-correlation-id` response header on every response**, success or failure. - Validation failures (`400`) may add `error.details`: a list of `{ "field": , "message": }` entries. -Success shapes for symmetry: a single resource is returned as the object itself (no wrapper); lists use the pagination envelope above. +Success shapes: a single resource is returned as the bare object (no wrapper); lists use the pagination envelope above. ### Endpoint contract @@ -160,45 +160,39 @@ Each endpoint defines its required permissions, request schema, response schema, ## Cross-cutting concerns -Concerns that touch every request — auth, context, logging, transactions, error mapping — are implemented as **decorators / aspects (AOP)**: declared once and applied declaratively to the ring they wrap, so a handler or use case carries only its own logic. In Node this is the framework's middleware/plugin layer — Express middleware, or Fastify plugins plus lifecycle hooks and decorators. **Scope each aspect to the subtree that needs it** — register it on the plugin/router branch it applies to, not globally, so unrelated routes stay clean. Each aspect still obeys the dependency rule — it lives in its ring and passes data inward only as plain values. +Concerns that touch every request — auth, context, logging, transactions, error mapping — are **decorators / aspects**: declared once and applied declaratively to the ring they wrap, so a handler or use case carries only its own logic. Scope each aspect to the subtree that needs it, not globally. Each aspect obeys the dependency rule — it lives in its ring and passes data inward only as plain values. -- **Auth:** guards at the controller ring reject unauthenticated requests at the edge; rule-level authorisation that depends on domain state lives in the domain or use case. +- **Auth:** guards at the controller ring reject unauthenticated requests at the edge; authorisation that depends on domain state lives in the domain or use case. - **Request context / identity:** established at the edge, passed inward as an argument — never read from a global by an inner ring. -- **Transactions:** the boundary wraps the use case (see Service). -- **Logging & audit:** one shared path carrying the request's correlation id, so a request traces end to end. Keep it out of the domain. Emit **structured records** (key/value fields, not concatenated strings) at meaningful **levels** — `error` for handled failures, `warn` for recoverable anomalies, `info` for state changes, `debug` behind a flag for diagnostics. **Never log secrets, tokens, credentials, auth headers, or PII; redact at the logging boundary** and log identifiers (e.g. a user id) rather than payloads. Log a failure **once**, where it is handled — not at every ring on the way out (re-logging the same error is the noise the root Principles forbid). -- **Audit trail:** when the app must answer *who changed what* (permission or role changes, contact-detail edits, moderation, money movement), that is a concern **distinct** from operational logging — logs rotate and aren't queryable as history. Record every meaningful state change through **one shared `record()` call** in the service ring — actor, action, target, and the before/after where it matters — to durable, queryable storage, carrying the request's correlation id. One call site per state change, invoked by the use case that owns the change; not scattered inserts, and not the log stream. -- **Errors:** the domain raises failures in domain terms; the controller ring is the single place that maps them onto transport responses, using the *Error responses* envelope — one shape app-wide. - -## Standards reference - -### Business rules - -- Validate state transitions against the rules **before** applying a status or lifecycle change — never because an external request, callback, message, or event asked for it. +- **Transactions:** the boundary wraps the use case (see *Service*). +- **Logging:** one shared path carrying the request's correlation id, so a request traces end to end. Emit structured key/value records at levels — `error` for handled failures, `warn` for recoverable anomalies, `info` for state changes, `debug` behind a flag. Never log secrets, tokens, credentials, auth headers, or PII — redact at the logging boundary and log identifiers (e.g. a user id), not payloads. Log a failure once, where it is handled. +- **Audit trail:** distinct from operational logging — logs rotate and aren't queryable as history. Record every meaningful state change (actor, action, target, and the before/after where it matters) through **one shared `record()` call** in the service ring, invoked by the use case that owns the change, to durable queryable storage, carrying the correlation id. +- **Errors:** the domain raises failures in domain terms; the controller ring is the single place that maps them onto the *Error responses* envelope — one shape app-wide. -### Integrations +## Integrations Treat every external API, callback, webhook, queue, and event as untrusted and unreliable. The integration code is a repo-ring adapter; the decisions it enforces belong to the domain. -- **Idempotency:** handle repeated requests, retries, and replays without duplicating actions or overwriting valid results. Use the primary business record id as the idempotency key unless a clearer business key exists. -- **Concurrency:** assume several workers may process the same record at once. Use conditional updates, locking, transactions, or version checks. +- **Idempotency:** handle repeated requests, retries, and replays without duplicating actions or overwriting valid results; key on the primary business record id unless a clearer business key exists. +- **Concurrency:** assume several workers may process the same record at once; use conditional updates, locking, transactions, or version checks. - **Validation:** validate structure, required fields, types, business rules, authenticity, and ownership before sending or applying anything. - **Ordering:** where order matters, process by event time, sequence/version number, or business rule — not arrival order. -- **Failure handling:** classify failures as transient, permanent, invalid, unsupported, duplicate, or unknown. Retry only transient ones, with bounded retries and backoff, and a defined final-failure path. -- **Unclear outcomes:** never treat a timeout, transport error, malformed or unexpected response, or ambiguous result as success. Preserve existing valid data and route the outcome to reconciliation or manual recovery. -- **Gate a risky integration behind a default-off flag with a no-op sink.** An integration that spends money or reaches real users (SMS/email/payment/push) ships behind a **default-off** validated-config boolean (root *Configuration*), read in **one** place, that routes to a **stdout / no-op sink** when off. Exercise it against the sink until you flip the flag on per-environment; flipping it back off is the instant rollback. (This flag underpins the optional **test-mode** and **otp-auth** add-ons — see `add-ons/`.) +- **Failure handling:** classify failures as transient, permanent, invalid, unsupported, duplicate, or unknown; retry only transient ones, with bounded retries, backoff, and a defined final-failure path. +- **Unclear outcomes:** never treat a timeout, transport error, or malformed/ambiguous response as success; preserve existing valid data and route the outcome to reconciliation or manual recovery. +- **Risky integrations** — anything that spends money or reaches real users (SMS/email/payment/push) — ship behind a **default-off** validated-config boolean (root *Configuration*), read in one place, routing to a no-op sink when off. Exercise against the sink until the flag flips on per environment; flipping it back off is the instant rollback. This flag underpins the **test-mode** and **otp-auth** add-ons (`add-ons/`). -### Security baseline +## Security baseline -The edge already validates input (the handler validates input; *Endpoint contract* defines validation rules) and places authorisation (edge guards reject unauthenticated requests; rule-level authz that depends on domain state lives in the domain / use case — see *Cross-cutting → Auth*). Add the rules that aren't yet stated: +Beyond edge validation and the auth guards above: -- **Parameterised data access:** pass query parameters as bound values; never interpolate request data into a query/filter string. -- **Secrets from the environment:** secrets and config come from the environment, never hardcoded, committed, or echoed in errors/logs; inner rings receive config as injected values, not by reading globals. -- **Ownership:** verify ownership on every client-supplied id before acting on the record — make this explicit for ordinary requests, not just the webhooks the *Integrations* rules already cover. -- **Security response headers:** send the standard HTTP hardening headers — transport security, content-type and framing protections, a referrer policy, and (where the app serves HTML) a **Content-Security-Policy** — from one shared place. Roll out a new or tightened CSP in **report-only** mode first, then promote it to enforcing once the violation reports are clean; an enforcing CSP shipped blind breaks inline styles and third-party embeds. (Exact header set + mechanism: the active stack pack.) -- **Guard server-side requests to user-supplied URLs (SSRF):** when the app fetches a URL a user or admin configured (a webhook target, an import source), restrict it to allowed schemes and **public** hosts and reject internal targets (loopback, private, link-local, cloud-metadata) **before** the request leaves — validated when the URL is saved *and* re-checked at call time. (Concrete check: the active stack pack.) -- **Secrets stored through the API are write-only:** never return a stored secret on read — expose only a "configured" indicator — and treat a blank value on update as "keep the existing secret", so re-saving a form never clears one the user didn't retype. (Concrete masking/merge: the active stack pack.) +- **Parameterised data access:** pass query parameters as bound values; never interpolate request data into a query or filter string. +- **Secrets from the environment:** never hardcoded, committed, or echoed in errors/logs; inner rings receive config as injected values, not by reading globals. +- **Ownership:** verify ownership on every client-supplied id before acting on the record — on ordinary requests, not just the webhooks *Integrations* covers. +- **Security response headers:** send the standard hardening headers — transport security, content-type and framing protections, a referrer policy, and (where the app serves HTML) a **Content-Security-Policy** — from one shared place. Roll out a new or tightened CSP **report-only** first; promote to enforcing once the violation reports are clean. Exact header set + mechanism: the active stack pack. +- **SSRF guard on user-supplied URLs:** when the app fetches a URL a user or admin configured (a webhook target, an import source), allow only permitted schemes and **public** hosts; reject loopback, private, link-local, and cloud-metadata targets before the request leaves — validated when the URL is saved *and* re-checked at call time. Concrete check: the active stack pack. +- **Secrets stored through the API are write-only:** never return a stored secret on read — expose only a "configured" indicator — and treat a blank value on update as "keep the existing secret". Concrete masking/merge: the active stack pack. ## Coding standards -- **Don't reinvent libraries** (full rule: root `CLAUDE.md` Principles, *Don't reinvent existing solutions*). Backend specifics worth repeating in-context: dates/timezones, phone canonicalisation, identifiers, CSV, and schema validation all use an established library and a single shared helper — never a hand-rolled one. -- **Schema changes are reversible migrations under `db/`** — never issue DDL or alter schema from application code; the repo ring's adapters read the schema, they don't mutate it. See `db/CLAUDE.md`. +- Dates/timezones, phone canonicalisation, identifiers, CSV, and schema validation use an established library via a single shared helper — never hand-rolled (root *Don't reinvent existing solutions*). +- Schema changes are reversible migrations under `db/` — never issue DDL or alter schema from application code; repo adapters read the schema, never mutate it. See `db/CLAUDE.md`. diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md index c5350a3..fd62386 100644 --- a/apps/frontend/CLAUDE.md +++ b/apps/frontend/CLAUDE.md @@ -1,245 +1,235 @@ # Frontend -The frontend contract. Read this before touching anything under `apps/frontend/`. Repo-wide rules (principles, worktree workflow, cross-app standards) live in the root `CLAUDE.md`; this file governs how the single-page app itself is structured. **If a stack pack is adopted (a single directory kept under `stacks/`), also read its `frontend.md` appendix before working here** — it adds the concrete bindings, and its conflict register resolves any disagreement with this file, for that stack only. +The frontend contract. Read this before touching anything under `apps/frontend/`; repo-wide rules (principles, worktree workflow, cross-app standards) live in the root `CLAUDE.md`. Stack pack adopted? Read its `frontend.md` appendix first — precedence rules in `stacks/README.md`. -The frontend is organised along **two axes that never blur**: horizontal **layers** (what a piece of code *is* — store, service, page, component) and vertical **feature slices** (what business capability it serves). Components themselves follow **atomic design** — see *Component structure* below. +Two axes never blur: horizontal **layers** (what code *is* — store, service, page, component) and vertical **feature slices** (what business capability it serves). Components follow **atomic design** — see *Component structure*. ## Project structure -Mirror this shape under `apps/frontend/src/`. It is **illustrative**: the toolchain is not yet chosen (see the root `CLAUDE.md`), so treat file extensions and framework specifics as examples, not mandates. +Mirror this shape under `apps/frontend/src/`. The toolchain is not yet chosen (root `CLAUDE.md`), so file extensions and framework specifics are illustrative, not mandates. ``` src/ - store/ # state layer — one slice per domain - services/ # API clients — each domain mirrors a backend route group - pages/ # screens (atomic "pages" tier) — compose organisms, no business logic + store/ # one slice per domain + services/ # API clients — mirror backend route groups + pages/ # screens — compose organisms, no business logic components/ - atoms/ # smallest primitives, by type — Button, Input, Icon (on the headless lib) - molecules/ # small compositions of atoms, by type — FormField, SearchBar, Card + atoms/ # smallest primitives, by type (on the headless lib) + molecules/ # generic compositions, by type organisms/ - / # feature-meaningful sections, grouped by feature — BidTable, SiteHeader - templates/ # page-level layout scaffolds — the shared layout, page chrome + / # feature sections, by feature + templates/ # page-level layout scaffolds i18n/ # one dictionary per language - lib/ # genuinely shared, side-effect-light helpers - routes. # the single central route registry + lib/ # shared, side-effect-light helpers + routes. # the single route registry tokens. # the single design-token source ``` -**Grouping is set by the tier, not by preference** (full rule in *Component structure*): `atoms/` and `molecules/` are grouped **by type** and shared globally — they carry no business vocabulary; `organisms/` are grouped **by feature** — they do. A feature's vertical slice therefore spans `store/` + `services/` + `components/organisms/`, so it can still be understood, changed, and removed as a unit. Promote code into `atoms/`/`molecules/` or `lib/` only once it is genuinely shared — not in anticipation of reuse. +A feature's vertical slice spans `store/` + `services/` + `components/organisms/`, so it can be understood, changed, and removed as a unit (grouping rules: *Component structure*). Promote code into `atoms/`/`molecules/` or `lib/` only once genuinely shared — never in anticipation of reuse; co-locate a one-off helper with its only caller until reuse appears (`src/lib/` holds only genuinely shared, side-effect-light code). ## Layering -Each layer has one job, may depend only on the layers beneath it, and must never reach upward. +Each layer has one job, may depend only on the layers beneath it, and never reaches upward. Tier definitions: *Component structure*. -- **Store (`src/store/`)** — owns application state, one slice per domain. May depend on services. Must never import a page or render anything. -- **Services (`src/services/`)** — own all data fetching and mutation; each domain mirrors a backend route group. **All network access lives here**, never scattered across presentational components. May depend on `lib/`. Must never hold view state. - - **API contract.** The backend endpoint contract (see *Endpoint contract* in `apps/backend/CLAUDE.md`) is the single source of truth for request/response shapes and status codes; the service mirrors it and never invents its own shape. - - **Prefer a generated or shared contract artifact** over hand-copying when the toolchain supports it (e.g. an OpenAPI/JSON-schema document the backend emits and the frontend types against). When it doesn't, every contract change is one PR touching the backend endpoint *and* its mirroring frontend service together. - - **Validate responses against the declared shape** rather than trusting them, so a contract break surfaces as a typed error (feeding the `error` state) instead of an undefined-field render. -- **Pages (`src/pages/`)** — compose organisms into a screen (the atomic *pages* tier). **Hold no business logic;** they wire data from store/services into components. Must never fetch directly or embed reusable UI inline. -- **Templates (`src/components/templates/`)** — page-level layout scaffolds (the one shared layout, page chrome) that arrange organisms with no real data. May use organisms and primitives; hold no business logic. See *Page layout & design tokens*. -- **Organisms / feature components (`src/components/organisms//`)** — compose atoms and molecules into a feature-meaningful section. May use `atoms/`, `molecules/`, and `lib/`. Must never be imported by a primitive. -- **Shared primitives (`src/components/atoms/`, `src/components/molecules/`)** — the reusable base. May depend only on the UI library and the design tokens. Must never know about a specific feature or page. +- **Store** — application state, one slice per domain. May depend on services; never imports a page or renders anything. +- **Services** — all data fetching and mutation; each domain mirrors a backend route group; **all network access lives here**. May depend on `lib/`; never hold view state. + - The backend endpoint contract (*Endpoint contract*, `apps/backend/CLAUDE.md`) is the single source of truth for shapes and status codes; the service mirrors it, never invents its own. + - Prefer a generated or shared contract artifact (e.g. an OpenAPI/JSON-schema document the backend emits); without one, every contract change is one PR touching the backend endpoint *and* its mirroring frontend service. + - Validate responses against the declared shape so a contract break surfaces as a typed error feeding the `error` state, not an undefined-field render. +- **Pages** — compose organisms into a screen and wire data from store/services into components. No business logic; never fetch directly or embed reusable UI inline. +- **Templates** — arrange organisms with no real data; no business logic. See *Page layout & design tokens*. +- **Organisms** — may use `atoms/`, `molecules/`, and `lib/`; never imported by a primitive. +- **Shared primitives (`atoms/`, `molecules/`)** — depend only on the UI library and the design tokens; never know a specific feature or page. -Cross-cutting rules for every layer: - -- **Loading / error / empty / success states are handled consistently** — the same four states, presented the same way, on every data-backed screen. - - **An empty state is designed, not blank.** Every empty state states *why* there's nothing and offers the primary next action where one exists. Handle the cases distinctly — they differ in copy and CTA: first-run / never-created ("create your first X"), no-results / filtered-to-nothing (offer to clear filters or adjust the query), and access-restricted (explain the missing permission). A data-load **failure** is an error state, never an empty state — show a retry, not "nothing here". -- **Don't accumulate one-off helpers in `src/lib/`** — co-locate a helper with its only caller until reuse actually appears. +**Loading/error/empty/success are handled consistently on every data-backed screen** — presented the same way. An empty state is designed, not blank: state *why* there's nothing and offer the primary next action where one exists. First-run ("create your first X"), no-results/filtered-to-nothing (offer to clear filters), and access-restricted (explain the missing permission) are distinct cases — they differ in copy and CTA. A data-load **failure** is an error state with a retry, never an empty state. ## URL routing -A route is part of the app's public contract; an internal file path is an implementation detail. **Keep the two separate.** Browser URLs stay clean and human-meaningful and **never expose internal build/source paths** (no `/src/` or `/pages/` prefix in the address bar). +Browser URLs stay clean and human-meaningful and **never expose internal build/source paths** (no `/src/` or `/pages/` prefix in the address bar). -- **One central registry.** Every route lives in a single routing config (`routes.`), registered the moment its page is created — never ship a page without its route entry. Reading `routes.` is the way to audit routing; do not maintain a second route→URL list anywhere else (including this file). A CI check in the spirit of the i18n key-parity check can enforce completeness. -- **Build URLs through the registry, never by hand.** Resolve links and redirects from named routes, not by concatenating path strings — so internal structure can never leak into a URL, and renaming a route updates every link at once. +- **One central registry.** Every route lives in `routes.`, registered the moment its page is created. Audit routing by reading the registry; maintain no second route→URL list anywhere else (including this file). +- **Build URLs from named routes in the registry,** never by concatenating path strings — internal structure can't leak into a URL, and renaming a route updates every link at once. -## Design guide — the visual keystone (confirm before building UI) +## Design guide (confirm before building UI) -**Lock the visual system before building any screen.** The project's visual system lives in the **design guide** (`design/design-guide.html` — "Keystone"): the design principles plus every foundation — colour, type, spacing, layout, shape, surfaces & elevation, motion, iconography, states/focus, accessibility, content, data formatting — and the composition chapters (screen archetypes, forms, view states & feedback) — rendered live from the single design-token source (`design/tokens.css`, the seed for the app's `tokens.`). A live mirror of the tokens, not a stale screenshot. **Foundations only, by design** — components stay flexible per app and are built *from* these foundations. +**Lock the visual system before building any screen.** It lives in the design guide (`design/design-guide.html`), rendered live from the single design-token source (`design/tokens.css` — the seed for the app's `tokens.`). **Foundations only, by design** — components stay flexible per app and are built *from* these foundations. -- ***Confirm* the design guide before building screens.** It is a gate: for a new project (or a rebrand) no screen or component work starts until the guide reflects the project's brand, has been reviewed in a browser, and signed off. Once the system is established small additions don't re-gate — but a new foundational token lands in the guide first. -- **Customise by editing tokens, not screens.** A rebrand edits the **primitive** token tier — or has your AI assistant regenerate it from the brand — and the semantic tier and the whole guide re-derive. This is the "one token source, three tiers" rule below — the guide is its human-reviewable face. -- **The guide binds components without prescribing them.** Every component consumes semantic tokens, answers with the guide's state ladder and focus spec, and meets its accessibility floor; a component that violates a foundation is the defect (the DRY-gate audit under *Component structure* catches duplicates). A pattern that recurs across projects earns a specimen in the guide; a stack pack may add a Storybook against the same tokens (optional upgrade). +- **Confirm the guide first.** For a new project or rebrand, no screen or component work starts until the guide reflects the brand, is browser-reviewed, and signed off. An established system doesn't re-gate small additions — but a new foundational token lands in the guide first. +- **Customise by editing the primitive token tier, not screens;** the semantic tier and the whole guide re-derive. +- **The guide binds components without prescribing them.** Every component consumes semantic tokens, follows the guide's state ladder and focus spec, and meets its accessibility floor; a component violating a foundation is the defect. A pattern recurring across projects earns a guide specimen. -**Never-violate gates** — the build-time digest; the named guide chapter is canonical: +**Never-violate gates** — the named guide chapter is canonical: -1. Every colour, size, space, and duration resolves to a semantic token — a hex or px literal in a screen is the defect (guide → *Tokens*). -2. Pick the screen archetype before building any screen — its zones, page rhythm, and width are fixed, never re-derived per page (guide → *Screen archetypes*). -3. Surfaces follow the ladder: no card-like container inside another; separate in order whitespace → background shift → border → divider (tables/dense rows only) (guide → *Surfaces & elevation*). -4. Reuse first: archetype → documented pattern → existing screens/primitives → extend a primitive → only then new, with the PR recording why nothing fit (guide → *Components & reuse*). -5. One density app-wide, set at the token layer — never mixed within a page hierarchy (guide → *Screen archetypes*). -6. Forms and view states follow the composition patterns — the pattern outranks the component library's defaults (guide → *Forms*, *View states & feedback*). +1. Every colour, size, space, and duration resolves to a semantic token — a hex or px literal in a screen is the defect (*Tokens*). +2. Pick the screen archetype before building any screen — its zones, page rhythm, and width are fixed, never re-derived per page (*Screen archetypes*). +3. Surfaces follow the ladder: no card-like container inside another; separate in order whitespace → background shift → border → divider (tables/dense rows only) (*Surfaces & elevation*). +4. Reuse first: archetype → documented pattern → existing screens/primitives → extend a primitive → only then new, with the PR recording why nothing fit (*Components & reuse*). +5. One density app-wide, set at the token layer — never mixed within a page hierarchy (*Screen archetypes*). +6. Forms and view states follow the composition patterns — the pattern outranks the component library's defaults (*Forms*, *View states & feedback*). ## Page layout & design tokens -Consistency is a system, not a per-page effort. Two things make every screen feel like one product: a **single shared layout** and a **single token source**. A page author composes the layout and reaches for tokens — and never re-decides spacing, colour, or navigation. - -**Primary form factor (FILL IN ON SETUP):** `` plus the supported viewport range. This choice drives the default navigation pattern and which furniture the shared layout carries. +**Primary form factor (FILL IN ON SETUP):** `` plus the supported viewport range. This drives the default navigation pattern and the furniture the shared layout carries. -**One shared layout.** Every page builds on common layout components that supply the standing furniture — header / navigation, page chrome, consistent gutters and background, and the navigation pattern for the declared form factor. The page provides its content; the layout owns the frame — don't hand-roll a page shell. **The layout owns every clearance and inset; pages never re-derive them:** fixed/sticky chrome reserves its space through one clearance token (composed once with its safe-area inset), and top-spacing variants are a **prop the layout offers** — a page picks one, it never re-decides the padding. +**One shared layout** supplies the standing furniture — header/navigation, page chrome, gutters, background, and the navigation pattern for the declared form factor. The page provides content; the layout owns the frame — never hand-roll a page shell. **The layout owns every clearance and inset; pages never re-derive them:** fixed/sticky chrome reserves its space through one clearance token (composed once with its safe-area inset), and top-spacing variants are a **prop the layout offers** — a page picks one, never re-decides the padding. -**Layouts are responsive by default** — content reflows without horizontal scroll or clipping across the declared viewport range; no fixed pixel widths that break it. +**Layouts are responsive by default** — content reflows without horizontal scroll or clipping across the declared viewport range; no fixed pixel widths that break it (rules: *Responsive layout*). -**One token source, three tiers.** All spacing, colour, typography, radius, and elevation come from a single design-token source, never hardcoded per page. Structure tokens in three layers so they stay coherent and themeable: +**One token source, three tiers:** 1. **Primitive tokens** — raw, context-free values (`--red-400`, `--space-3`). -2. **Semantic tokens** — decisions that map primitives to meaning (`--color-bg`, `--gutter-screen`, `--header-clearance`). Components reference *these* (plus form-factor-specific tokens such as `--bottom-nav-clearance` only when mobile is the primary form factor). +2. **Semantic tokens** — decisions mapping primitives to meaning (`--color-bg`, `--gutter-screen`, `--header-clearance`; form-factor-specific tokens such as `--bottom-nav-clearance` only when mobile is primary). 3. **Component tokens** — per-component overrides, where a component genuinely needs them. -Pages and components consume **semantic** tokens; they never reach past them to a raw primitive value. - -**A token's committed value must match its documented scale — guard it.** Check the token file against its declared scale, in the spirit of the i18n key-parity check. +Pages and components consume **semantic** tokens; they never reach past them to a raw primitive value. A token's committed value must match its documented scale. ## Responsive layout -"Responsive by default" (above) is a promise; these rules keep it, whatever primary form factor you declared. Fix each failure with a primitive or token applied **once** — never a per-page tweak. (The concrete idioms per CSS toolchain live in the active stack pack.) +Fix each failure below with a primitive or token applied **once**, never a per-page tweak; the concrete CSS idioms live in the active stack pack. -- **Author from the smallest supported width up.** The floor is WCAG **Reflow**: no sideways scroll or lost content at **320 CSS px**, and the layout survives **200% text zoom**. +- **Author from the smallest supported width up.** The floor is WCAG Reflow: no sideways scroll or lost content at **320 CSS px**, and the layout survives **200% text zoom**. - **Prefer intrinsic sizing; reach for breakpoints last.** Fluid type/space and self-wrapping grids adapt *between* breakpoints; a reusable component adapts to **its container's** width, not the viewport's. Add a viewport breakpoint only for a genuine page-level layout change. -- **No horizontal overflow at the minimum width.** Atomic values (phone numbers, IDs, amounts) never wrap mid-token — bake no-wrap into the shared inline-value primitive; long free text wraps or truncates, never pushes width (a flex/grid child needs `min-width: 0` to be allowed to shrink); wide tables and code blocks scroll inside their own box, never the page. -- **Reserve space for fixed / sticky chrome with one semantic token** (`--header-clearance`) applied by the shared layout — never re-measured or re-padded per page. -- **Size full-bleed sections to content, not the viewport** — a content-driven min-height plus vertical padding, never `100vh`; where something must truly fill the viewport prefer `svh` over `vh`, and `dvh` only to deliberately track the browser chrome. +- **No horizontal overflow at the minimum width.** Atomic values (phone numbers, IDs, amounts) never wrap mid-token — bake no-wrap into the shared inline-value primitive; long free text wraps or truncates, never pushes width (flex/grid children need `min-width: 0` to shrink); wide tables and code blocks scroll inside their own box, never the page. +- **Pagination controls render a bounded window of page slots (~7: first, last, current ± 1, ellipsis), never the full page list** — a large page count must not widen the layout. +- **Reserve space for fixed/sticky chrome with the one semantic clearance token** applied by the shared layout — never re-measured or re-padded per page. +- **Size full-bleed sections to content, not the viewport** — a content-driven min-height plus vertical padding, never `100vh`; where something must truly fill it prefer `svh` over `vh`, `dvh` only to deliberately track browser chrome. - **Treat configurable copy as variable-length.** Any admin/CMS-editable string must survive a one-word *and* a three-line value without clipping or colliding with chrome; balance headings by default. -- **Multi-field rows collapse to full-width below the breakpoint,** each field keeping a min-width that leaves its content legible. -- **Adapt by disclosure, never by hiding meaning** — if navigation doesn't fit, collapse it into a menu; don't drop destinations or actions on small screens. +- **Multi-field rows collapse to full width below the breakpoint,** each field keeping a min-width that leaves its content legible. +- **Adapt by disclosure, never by hiding meaning** — if navigation doesn't fit, collapse it into a menu; never drop destinations or actions on small screens. ## Navigation chrome, overlays & scroll -Persistent chrome (a bottom nav, a sticky header), overlays, and client-side route changes recur as rework in an SPA — fix each at the root, not per screen. (Companion to *Responsive layout*, which owns overflow and viewport sizing; and to *One shared layout*, which owns clearance.) +Fix each of these at the root, not per screen: -- **Render overlays and fixed chrome in a top-level portal** — an ancestor's `transform` or low `z-index` otherwise drags or buries them (fixed bars sliding with page transitions; sheets rendering under the nav). -- **Reset or restore scroll in an effect keyed on the actual route/view change**, not synchronously at the navigation call; a keep-alive surface has **one explicit scroll owner**. Otherwise a newly-shown view inherits the previous one's scroll offset. -- **Global-nav visibility is a denylist of chrome-less routes, not an allowlist** — a new screen keeps the nav by default; only auth/legal/full-screen-editor routes opt out (an editor with its own sticky action bar hides the global nav so its primary action isn't clipped). -- **Under the soft keyboard, a flex column scrolls — it does not squeeze:** the scroll region is `overflow-y: auto`, non-shrinkable panels are `flex-shrink: 0`. Otherwise panels collapse to a clipped sliver when the keyboard opens. +- **Render overlays and fixed chrome in a top-level portal** — an ancestor's `transform` or low `z-index` otherwise drags or buries them. +- **Reset or restore scroll in an effect keyed on the actual route/view change,** not synchronously at the navigation call; a keep-alive surface has **one explicit scroll owner**. +- **Global-nav visibility is a denylist of chrome-less routes, not an allowlist** — a new screen keeps the nav by default; only auth/legal/full-screen-editor routes opt out. +- **Under the soft keyboard, a flex column scrolls — it does not squeeze:** the scroll region is `overflow-y: auto`, non-shrinkable panels are `flex-shrink: 0`. ## Visual quality bar -Tokens say *where* values come from; this says *which* values are good. Checkable, per screen: +Checkable, per screen: -- **Type.** One modular type scale in the token source; at most 2 font families, and on any single screen ~4 type sizes and ~2 weights. Body copy capped at ~60–75ch measure. Adding a size means adding a scale step in tokens, not a one-off value in a component. -- **Spacing.** Every margin / padding / gap resolves to an existing step on the spacing scale. Don't introduce ad-hoc values or new steps to make one screen fit; if the scale can't express it, fix the scale, not the instance. -- **Hierarchy.** Exactly one primary (filled) action per view; everything else is secondary / tertiary. One H1 per page; heading levels nest in order and never skip (h1 → h2 → h3), so the heading outline doubles as document structure for assistive tech. -- **Colour.** Use semantic intent tokens for meaning (success / warning / danger / info); never encode meaning in a raw hue or colour alone — pair it with text or an icon. Limit accent surfaces so the single primary CTA stays the most prominent element. -- **Alignment & density.** Content aligns to the shared layout's grid / gutters — no per-screen one-off gutters. Control sizing / density follows the declared primary form factor and stays consistent within a view; don't hardcode a global density. +- **Type.** One modular type scale in the token source; at most 2 font families; ~4 type sizes and ~2 weights per screen; body copy capped at ~60–75ch measure. A new size is a new scale step in tokens, never a one-off value in a component. +- **Spacing.** Every margin/padding/gap resolves to an existing step on the spacing scale. If the scale can't express it, fix the scale, not the instance. +- **Hierarchy.** Exactly one primary (filled) action per view; everything else is secondary/tertiary. One H1 per page; heading levels nest in order and never skip — the heading outline doubles as document structure for assistive tech. +- **Colour.** Semantic intent tokens for meaning (success/warning/danger/info); never encode meaning in colour alone — pair it with text or an icon. Limit accent surfaces so the single primary CTA stays the most prominent element. +- **Alignment & density.** Content aligns to the shared layout's grid/gutters — no per-screen one-off gutters. Control sizing/density follows the declared primary form factor and stays consistent within a view. ## Interaction feedback & perceived performance -- **Every actionable control shows its state from tokens.** Pressed / active, focus-visible, and disabled states are defined on the shared `atoms/`/`molecules/` primitives (not per page) and driven by semantic tokens. Hover is a pointer-device affordance; on a touch-primary form factor the pressed / active state carries the feedback — never leave the touch path without visible press feedback. (Keyboard focus-visible is owed by the headless foundation; surface it, don't suppress it.) -- **In-flight feedback stays on the control that triggered the action.** A local action disables its own control and shows an inline busy indicator there — never blank the whole screen with a top-level spinner for a local action. Reserve full-screen / section loading for a screen's initial data fetch (the `loading` state above). -- **Prefer optimistic updates for low-risk mutations** (toggles, reorders, favourites) with rollback + an error message on failure; reserve blocking spinners for genuinely blocking waits. -- **Initial content load uses skeletons that match the final layout;** short indeterminate waits use a spinner. Don't layout-shift from spinner to content. -- **Avoid indicator flicker:** delay showing a busy indicator (~150 ms) and keep it visible a small minimum once shown; debounce live search / filter input (~250 ms). Treat these as defaults a project may tune, not magic numbers. -- **Move focus deliberately after a navigational or destructive action** — to the next logical element, the confirmation, or back to the triggering control after a modal closes — so keyboard and screen-reader users aren't dropped at the top of the document. +- **Control states come from tokens on the shared primitives.** Pressed/active, focus-visible, and disabled are defined on `atoms/`/`molecules/`, never per page. Hover is a pointer-device affordance; touch-primary paths always show visible press feedback. Surface the headless foundation's focus-visible; don't suppress it. +- **In-flight feedback stays on the triggering control** — inline busy indicator plus disable; never a top-level spinner for a local action. Full-screen/section loading only for a screen's initial data fetch. +- **Prefer optimistic updates for low-risk mutations** (toggles, reorders, favourites) with rollback + an error message on failure; blocking spinners only for genuinely blocking waits. +- **Initial load uses skeletons matching the final layout;** short indeterminate waits use a spinner; no spinner-to-content layout shift. +- **Avoid indicator flicker:** delay busy indicators (~150 ms) with a small minimum visible time; debounce live search/filter input (~250 ms) — defaults a project may tune, not magic numbers. +- **Move focus deliberately after a navigational or destructive action** — to the next logical element, the confirmation, or back to the trigger after a modal closes. ## Forms -- **Validation timing.** Don't surface a field error before the user has interacted with that field. Validate a field on blur after first interaction, and the whole form on submit. Once a field shows an error, re-validate it on change so the error clears the moment it's fixed. Never error-shout on first keystroke. -- **Error placement & a11y.** Show each field's error inline, adjacent to the field, programmatically associated with it (`aria-describedby`) and conveyed by more than colour. On a failed submit, move focus to the first invalid field. -- **Destructive actions.** Require an explicit confirm step that names the consequence ("Delete 3 invoices?"). For irreversible / high-risk actions require deliberate confirmation (typed value or equivalent), never a bare button. -- **Unsaved-changes guard.** When a form holds meaningful unsaved edits, warn before discarding them — on both in-app route changes and browser unload / refresh. Don't prompt for trivial / transient inputs (e.g. a search box). -- **Submit handling.** While a submit is in flight, disable the submit control and prevent re-submission; surface progress through the same loading / error / success convention used elsewhere, not a per-form one. +- **Validation timing.** Validate a field on blur after first interaction, the whole form on submit; once a field shows an error, re-validate on change so it clears the moment it's fixed. Never error before first interaction or on first keystroke. +- **Error placement & a11y.** Field errors sit inline, adjacent to the field, `aria-describedby`-associated, and conveyed by more than colour; a failed submit moves focus to the first invalid field. +- **Destructive actions.** Require an explicit confirm naming the consequence ("Delete 3 invoices?"); irreversible/high-risk actions require deliberate confirmation (typed value or equivalent), never a bare button. +- **Unsaved-changes guard.** Warn before discarding meaningful unsaved edits — on in-app route changes and browser unload/refresh; not for trivial/transient inputs (a search box). +- **Submit handling.** Disable the submit control in flight and prevent re-submission; surface progress through the shared loading/error/success convention, not a per-form one. ## Microcopy & content -- **Capitalization is uniform.** Pick one convention project-wide and apply it everywhere — default to sentence case for all UI text except proper nouns. Don't mix title case and sentence case across buttons, headings, and labels. -- **Action labels are verb-first and specific.** Buttons and menu items name the action and its object — "Save changes", "Delete invoice", "Send invite" — not "OK", "Submit", or "Yes". -- **Error copy is user-facing and actionable.** State what happened, why if known, and what the user can do next. Blame-free; never exposes stack traces, status codes, internal identifiers, or raw exception text. (Distinct from the backend's error mapping, which shapes the transport response; this governs what the user reads.) -- **Empty / loading / success copy is concise and human** — paired with the four-state rule above; the states already exist, this governs their wording. -- **Keep user-facing copy centralized and reviewable** — out of component bodies, so all product copy can be audited in one place. In multilingual projects this is the i18n dictionaries; in single-language projects, a single strings / copy module serves the same purpose. No hardcoded display literals scattered through components. (Planned-screen copy still comes from the design mockups; these rules govern the microcopy agents would otherwise invent ad hoc — errors, empties, confirmations, labels.) +- **Capitalization is uniform** — one convention project-wide; default sentence case except proper nouns. Never mix title case and sentence case across buttons, headings, and labels. +- **Action labels are verb-first and specific** — "Save changes", "Delete invoice", "Send invite" — not "OK", "Submit", or "Yes". +- **Error copy is user-facing and actionable.** State what happened, why if known, and what the user can do next. Blame-free; never exposes stack traces, status codes, internal identifiers, or raw exception text. +- **Empty/loading/success copy is concise and human.** +- **Centralize user-facing copy** — i18n dictionaries in multilingual projects, a single strings module otherwise — so all product copy is auditable in one place; no hardcoded display literals in components. ## Component structure — atomic design -Visual and behavioural consistency comes from **reuse**, not from discipline repeated per screen. Structure every component into one of five atomic tiers, over a headless foundation you never skip — the foundation is a **dependency, not a folder**: unstyled, behavioural primitives from a headless UI library that solves focus management, keyboard handling, and widget-level ARIA for the components routed through it (not page-level a11y; see *Accessibility baseline*), with atoms built *on top of* it. - -1. **Atoms** (`components/atoms/`) — the smallest indivisible primitives, each mapping the project's tokens and conventions onto the foundation: `