From 62e95ae9f34a3ca66321d7a4f217fd03ed0768a2 Mon Sep 17 00:00:00 2001 From: DeyangChan Date: Wed, 29 Jul 2026 14:04:20 +0800 Subject: [PATCH 01/21] =?UTF-8?q?refactor(stacks):=20rename=20packs=20to?= =?UTF-8?q?=20identity=20names=20=E2=80=94=20vercel-csr,=20wechat,=20enter?= =?UTF-8?q?prise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vercel → vercel-csr (the -csr/-ssr suffix pair now carries the rendering contrast), taro-fastify-mysql-tencent → wechat (product surface), nextjs-nestjs-postgres → enterprise (architectural character). Naming rule in stacks/README.md rewritten for identity names; each pack README records its underlying triple in a Naming note. All live references updated; retired add-ons/seo/specs artifacts left as historical record. Co-Authored-By: Claude Fable 5 --- README.md | 14 +++++++------- add-ons/saas-billing/bindings.md | 6 +++--- add-ons/seo/bindings.md | 8 ++++---- stacks/README.md | 2 +- .../README.md | 6 ++++-- .../backend.md | 0 .../{nextjs-nestjs-postgres => enterprise}/db.md | 0 .../frontend.md | 0 stacks/{vercel => vercel-csr}/README.md | 6 +++--- stacks/{vercel => vercel-csr}/backend.md | 0 stacks/{vercel => vercel-csr}/db.md | 0 stacks/{vercel => vercel-csr}/frontend.md | 0 stacks/{vercel => vercel-csr}/infra.md | 2 +- stacks/vercel-ssr/README.md | 2 +- stacks/vercel-ssr/backend.md | 2 +- stacks/vercel-ssr/infra.md | 2 +- .../README.md | 8 ++++---- .../backend.md | 0 .../{taro-fastify-mysql-tencent => wechat}/db.md | 4 ++-- .../frontend.md | 0 .../infra.md | 2 +- 21 files changed, 33 insertions(+), 31 deletions(-) rename stacks/{nextjs-nestjs-postgres => enterprise}/README.md (88%) rename stacks/{nextjs-nestjs-postgres => enterprise}/backend.md (100%) rename stacks/{nextjs-nestjs-postgres => enterprise}/db.md (100%) rename stacks/{nextjs-nestjs-postgres => enterprise}/frontend.md (100%) rename stacks/{vercel => vercel-csr}/README.md (91%) rename stacks/{vercel => vercel-csr}/backend.md (100%) rename stacks/{vercel => vercel-csr}/db.md (100%) rename stacks/{vercel => vercel-csr}/frontend.md (100%) rename stacks/{vercel => vercel-csr}/infra.md (95%) rename stacks/{taro-fastify-mysql-tencent => wechat}/README.md (81%) rename stacks/{taro-fastify-mysql-tencent => wechat}/backend.md (100%) rename stacks/{taro-fastify-mysql-tencent => wechat}/db.md (96%) rename stacks/{taro-fastify-mysql-tencent => wechat}/frontend.md (100%) rename stacks/{taro-fastify-mysql-tencent => wechat}/infra.md (97%) diff --git a/README.md b/README.md index 6d54cae..a02e129 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The template is intentionally framework-agnostic. You choose: Pick what fits the project. The CLAUDE.md files tell you where things go and how to structure them — not which library to use. -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/` (e.g. `enterprise`) 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). ## Day-1 checklist @@ -72,7 +72,7 @@ Run this once, top to bottom, the first time you instantiate the template. Each - [`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.** - - **Pack path (fast):** pick the pack under `stacks/` matching your stack (e.g. `nextjs-nestjs-postgres`), then: + - **Pack path (fast):** pick the pack under `stacks/` matching your stack (e.g. `enterprise`), 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//`. @@ -81,7 +81,7 @@ Run this once, top to bottom, the first time you instantiate the template. Each 7. **Fill the toolchain placeholders.** On the pack path, step 5 already filled the first two bullets; **both paths** still do the last two: - Root `CLAUDE.md` "Common commands" — replace the seven ``/`TODO` commands and delete the PLACEHOLDER banner. - `.github/workflows/ci.yml` — replace every commented gate (**the file is the canonical gate list**): install/lint/typecheck/test/build, the i18n key-parity check, the migration up/down round-trip, and the a11y scan. A pack's CI block covers the toolchain gates; still wire the remaining `ci.yml` gates (migration gates per the pack's `db.md`, the a11y scan). - - `.github/workflows/deploy.yml` — replace the TODO step (or, on a pack whose register deletes the stub, e.g. `vercel`/`vercel-ssr`, delete it per that register). + - `.github/workflows/deploy.yml` — replace the TODO step (or, on a pack whose register deletes the stub, e.g. `vercel-csr`/`vercel-ssr`, delete it per that register). - 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: ```markdown @@ -89,10 +89,10 @@ Run this once, top to bottom, the first time you instantiate the template. Each ``` 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. Install the rule **after step 13's first green push** (or run steps 5–10 on a branch and merge them via a PR) — a required-status rule rejects a direct push whose CI has never run. 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` and `vercel-ssr` packs that is the `develop` branch plus its dedicated Neon branch (the pack's `infra.md` → *Staging environment*), migrated with the same manual runbook as prod (the pack's `db.md` → *Production & staging migrations*). +11. **Protect `main`.** Add a branch protection rule / ruleset requiring the CI workflow to pass before merge. Install the rule **after step 13's first green push** (or run steps 5–10 on a branch and merge them via a PR) — a required-status rule rejects a direct push whose CI has never run. Trunk must stay releasable — and on packs whose pipeline ships whatever lands on `main` (e.g. `vercel-csr`), 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-csr` and `vercel-ssr` packs that is the `develop` branch plus its dedicated Neon branch (the pack's `infra.md` → *Staging environment*), migrated with the same manual runbook as prod (the pack's `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 | grep -v '^\./README\.md:'` and `grep -n '^ ' CLAUDE.md`. (Only this root README — whose checklist names the markers — is filtered out; delete it once instantiation is done if you prefer a clean tree.) -> If you chose a server-first Next.js pack (`nextjs-nestjs-postgres` or `vercel-ssr`), soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", the opening line of `apps/frontend/CLAUDE.md` ("how the single-page app is structured") likewise, and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". (`vercel-ssr`'s one-app restructure step covers this and more — see its README.) The repo name still encodes "spa" and is immutable — accepted as stale. +> If you chose a server-first Next.js pack (`enterprise` or `vercel-ssr`), soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", the opening line of `apps/frontend/CLAUDE.md` ("how the single-page app is structured") likewise, and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". (`vercel-ssr`'s one-app restructure step covers this and more — see its README.) The repo name still encodes "spa" and is immutable — accepted as stale. > -> **The `vercel` pack is not one of them** — it is a client-rendered SPA with no SSR, so the base framing above is already correct for it and every one of those files stays exactly as shipped. Don't soften anything. +> **The `vercel-csr` pack is not one of them** — it is a client-rendered SPA with no SSR, so the base framing above is already correct for it and every one of those files stays exactly as shipped. Don't soften anything. diff --git a/add-ons/saas-billing/bindings.md b/add-ons/saas-billing/bindings.md index 9cd1ac3..1762869 100644 --- a/add-ons/saas-billing/bindings.md +++ b/add-ons/saas-billing/bindings.md @@ -2,11 +2,11 @@ Pre-written entries for the stack packs this template ships, kept **inside the add-on** so adopting it touches nothing else. At adoption, copy the active pack's entry below into that pack's `backend.md` (its add-on-bindings section). Each entry supplies what the README's *Binds to a stack* asks for: the provider + SDK/adapter home, the webhook raw-body/signature seam, the stub sink, the job runner, and the config home. A shipped pack with no section here is silent — a defect; every pack is covered below. **After copying the active pack's entry, delete this file** — the pack's `backend.md` is the binding's only home from then on (contrast `add-ons/seo/bindings.md`, which is read in place and kept). -## `nextjs-nestjs-postgres` — bound +## `enterprise` — bound - **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` Node SDK). The gateway is a repo-ring adapter behind the domain's `BillingGateway` port token, bound in the billing module's `providers` array; `BILLING_ENABLED=false` binds the stub gateway instead. Webhook seam: signature verification needs the **raw body** — capture it for the webhook route only (e.g. `fastify-raw-body` scoped to that route) and verify in a guard before the Zod pipe parses. Reconciliation sweep and usage rollover run on the pack's BullMQ / `@nestjs/schedule` binding. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` live in the Zod-validated config schema. -## `vercel` — bound +## `vercel-csr` — bound - **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` SDK). The gateway adapter registers in `container.js`; the flag routes to the stub via a container registration (tests swap it with `overrides`). Webhook seam: Fastify parses JSON by default — preserve the raw payload for the webhook route only (scoped raw-body plugin / content-type parser) and verify the Stripe signature before parsing. Jobs on serverless: no resident runner — bind the reconciliation sweep to **Vercel Cron** hitting a dedicated authenticated route, and finish each batch inside the request. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` join the boot-time Zod env schema. @@ -14,6 +14,6 @@ Pre-written entries for the stack packs this template ships, kept **inside the a - **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` SDK). The gateway is a repo-ring adapter behind the domain's billing gateway port, registered in `src/server/container.ts`; `BILLING_ENABLED=false` binds the stub gateway instead. Webhook seam: an external route handler (that pack's *External HTTP* edge — a thin delegate under `app/`) reads the raw payload with `request.text()` and verifies the Stripe signature before anything parses; route handlers don't pre-parse, so the raw body is available by construction. Jobs on serverless: no resident runner — bind the reconciliation sweep, usage rollover, and trial expiry to **Vercel Cron** hitting a dedicated authenticated route, finishing each batch inside the request. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` join the boot-time Zod env schema. The program's internal billing endpoints bind to this pack's queries/actions controller edge (its `/internal/v1` conflict-register entry applies — same envelope fields, function transport); only the webhook stays an HTTP route handler. -## `taro-fastify-mysql-tencent` — bound +## `wechat` — bound - **If you adopt the `saas-billing` add-on** (`add-ons/saas-billing/`): the provider is market-dependent — **Stripe** for international deployments; a mainland-China product swaps the gateway for **WeChat Pay** behind the same port. `lib/billing.js` is the gateway home (services call it, never an SDK directly); the default-off billing flag routes it to the stub sink. Webhook route: preserve the raw payload (scoped content-type parser) and verify the signature in a `preHandler` before schema validation. The reconciliation sweep is a separately-bundled scheduled SCF function (same pattern as `migrate.js`); provider secrets are validated at boot in the entry. diff --git a/add-ons/seo/bindings.md b/add-ons/seo/bindings.md index 6d8150f..8c9be4f 100644 --- a/add-ons/seo/bindings.md +++ b/add-ons/seo/bindings.md @@ -2,9 +2,9 @@ The shipped packs' answers to seam items S1–S10 (`README.md`, *Binds to a stack*), kept in this directory so the add-on's whole footprint lives here — keeping or deleting `add-ons/seo/` at Day-1 carries everything, and no stack appendix changes either way. At Day-1, keep the section for the adopted pack and delete the rest along with their packs. A pack with no section here is **silent** — a defect: add its bound section (one line per seam item, keyed by id) or record it unbound like the Taro section below. -## vercel — unbound +## vercel-csr — unbound -Unmeetable seam item: **S1** — that pack is a **client-rendered SPA with no SSR by deliberate design** (`stacks/vercel/frontend.md` → *Rendering model*). Every route is served as the same static `index.html` and painted by JavaScript, so there is no mechanism that makes an indexable route complete without client-side scripts, and nothing to hang S2–S9 off either: no server render to emit per-route `` tags, no server response to carry a 301 or a real 404 status. Adding one is a pack change, not a patch — do not reach for a prerender/SSG plugin to close this gap; that is precisely what the pack's forbidden list rules out. +Unmeetable seam item: **S1** — that pack is a **client-rendered SPA with no SSR by deliberate design** (`stacks/vercel-csr/frontend.md` → *Rendering model*). Every route is served as the same static `index.html` and painted by JavaScript, so there is no mechanism that makes an indexable route complete without client-side scripts, and nothing to hang S2–S9 off either: no server render to emit per-route `` tags, no server response to carry a 301 or a real 404 status. Adding one is a pack change, not a patch — do not reach for a prerender/SSG plugin to close this gap; that is precisely what the pack's forbidden list rules out. Workable alternative: adopt the sibling **`vercel-ssr`** pack, whose bound section is directly below — it is the same platform and the same database, and it exists for this requirement. A project that must keep the SPA serves its crawlable surface **outside** the app bundle (its own server-rendered origin — a marketing site, a docs site) and binds the add-on there. @@ -25,7 +25,7 @@ Full-stack Next.js is this add-on's best case: an indexable route is complete wi - **S9** ownership verification: a validated env key (e.g. `SEARCH_CONSOLE_VERIFICATION_TOKEN`) feeds the root layout's Metadata API `verification` field — absent key, absent tag. - **S10** budget + measurement: a checked-in per-route payload budget asserted in CI against `next build` output (gate G6); the three loading-experience axes are the Core Web Vitals (LCP, INP, CLS) — Lighthouse for lab runs, Vercel Speed Insights for field data (already allow-listed in that pack's Content-Security-Policy baseline). -## nextjs-nestjs-postgres — bound +## enterprise — bound Per seam item: @@ -40,6 +40,6 @@ Per seam item: - **S9** ownership verification: a validated env key (e.g. `SEARCH_CONSOLE_VERIFICATION_TOKEN`, documented in `.env.example`) feeds the Metadata API `verification` field — absent key, absent tag. - **S10** budget + measurement: a checked-in per-route payload budget asserted in CI against `next build` output (gate G6); the three loading-experience axes are the Core Web Vitals (LCP, INP, CLS), measured with Lighthouse against the running app (R19's observation check). -## taro-fastify-mysql-tencent — unbound +## wechat — unbound Unmeetable seam item: **S1** — the form factor is a phone-first, app-like H5/PWA client, and Taro H5's client-only rendering cannot serve indexable routes complete without client JS. Workable alternative: a project on this stack that grows a public crawlable surface serves it outside the Taro bundle (its own prerendered or server-rendered pages) and binds the add-on there. Residual posture: R10 survives unbinding — a publicly reachable H5 origin that shouldn't appear in search results still serves a refuse-indexing response (deny-all robots / noindex header) regardless. diff --git a/stacks/README.md b/stacks/README.md index 03dd3d6..3c0d4c6 100644 --- a/stacks/README.md +++ b/stacks/README.md @@ -4,7 +4,7 @@ The base CLAUDE.md files are framework-agnostic on purpose. A **stack pack** bin ## What a pack is -A pack is a directory `stacks//` of **guidance-as-text** — concrete config and command snippets to copy, never installed dependencies, lockfiles, or generated scaffolding in the buildable tree. `` is `--`, lowercase and hyphenated; **append the distinguishing choice as a suffix when the triple alone would collide or mislead** — the client/ORM (e.g. `nextjs-nestjs-postgres-prisma` vs a future TypeORM pack) or the deployment platform when it is load-bearing (e.g. `taro-fastify-mysql-tencent`). **Platform exception:** a pack whose identity is the deployment platform rather than the framework triple may be named for the platform (e.g. `vercel`) — its README records the would-be triple. Multiple packs on one platform coexist by suffixing the architectural shape that distinguishes them — e.g. `vercel` (a client-rendered SPA, no SSR) and `vercel-ssr` (its server-rendered full-stack sibling), where the suffix *is* the rendering model; each README names its sibling(s) and the contrast, so an adopter picks deliberately. +A pack is a directory `stacks//` of **guidance-as-text** — concrete config and command snippets to copy, never installed dependencies, lockfiles, or generated scaffolding in the buildable tree. `` is a **short identity name**, lowercase and hyphenated — the name an adopter recognizes the stack by: the deployment platform or product surface when that is the load-bearing choice (`vercel-csr`, `vercel-ssr`, `wechat`), a well-known stack acronym (`mern`), or the stack's architectural character (`enterprise`). The identity name stays auditable because **every pack README records the underlying `--` triple** in a *Naming* note. Packs sharing one platform coexist by suffixing the architectural shape that distinguishes them — e.g. `vercel-csr` (a client-rendered SPA, no SSR) and `vercel-ssr` (its server-rendered full-stack sibling), where the suffix *is* the rendering model; each README names its sibling(s) and the contrast, so an adopter picks deliberately. ## Required file set diff --git a/stacks/nextjs-nestjs-postgres/README.md b/stacks/enterprise/README.md similarity index 88% rename from stacks/nextjs-nestjs-postgres/README.md rename to stacks/enterprise/README.md index 7a9e002..5879f9f 100644 --- a/stacks/nextjs-nestjs-postgres/README.md +++ b/stacks/enterprise/README.md @@ -1,7 +1,9 @@ -# Stack pack: nextjs-nestjs-postgres +# Stack pack: enterprise Frontend **Next.js** (App Router, server-first) · Backend **NestJS** · DB **Postgres via Prisma**. Language-neutral: TypeScript or plain JavaScript, with JS-path notes in each appendix. This is the **manifest** — it wires the pack onto a project; the bindings and conflict registers live in the three appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. +> **Naming.** Named for its architectural character — the structured, batteries-included stack (NestJS modules + DI, Prisma, App Router) an adopter picks for team-scale, convention-heavy work; the underlying triple is `nextjs-nestjs-postgres` (per `../README.md`). Unlike the platform-named siblings (`vercel-csr`, `vercel-ssr`, `wechat`), this pack is platform-neutral — it deploys through whatever the base `infra/` contract stands up. + ## Appendix → base mapping | Appendix | Binds onto | Scope | @@ -18,7 +20,7 @@ Run as part of the root `README.md` `## Day-1 checklist`: delete every other `st Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and the **CI** block into `.github/workflows/ci.yml` — never the same block in both, and never `prisma migrate dev` in CI. -Record in root `CLAUDE.md` **Learnings**: `Stack: nextjs-nestjs-postgres; appendices under stacks/nextjs-nestjs-postgres/`. +Record in root `CLAUDE.md` **Learnings**: `Stack: enterprise; appendices under stacks/enterprise/`. ## Suggested toolchain (pnpm workspaces) diff --git a/stacks/nextjs-nestjs-postgres/backend.md b/stacks/enterprise/backend.md similarity index 100% rename from stacks/nextjs-nestjs-postgres/backend.md rename to stacks/enterprise/backend.md diff --git a/stacks/nextjs-nestjs-postgres/db.md b/stacks/enterprise/db.md similarity index 100% rename from stacks/nextjs-nestjs-postgres/db.md rename to stacks/enterprise/db.md diff --git a/stacks/nextjs-nestjs-postgres/frontend.md b/stacks/enterprise/frontend.md similarity index 100% rename from stacks/nextjs-nestjs-postgres/frontend.md rename to stacks/enterprise/frontend.md diff --git a/stacks/vercel/README.md b/stacks/vercel-csr/README.md similarity index 91% rename from stacks/vercel/README.md rename to stacks/vercel-csr/README.md index 2711e75..11b570a 100644 --- a/stacks/vercel/README.md +++ b/stacks/vercel-csr/README.md @@ -1,10 +1,10 @@ -# Stack pack: vercel +# Stack pack: vercel-csr Frontend **React SPA** (Vite, TypeScript) — **client-rendered, no SSR** · Backend **Fastify** (plain JavaScript, ESM) · DB **Postgres** — **Neon** (serverless) in production, Docker locally — via **node-pg-migrate** + **`pg`**. The whole product deploys to **Vercel**: two Vercel projects (the SPA as static assets on the CDN, and the Fastify API as a serverless function), Vercel Blob for object storage, Terraform (Vercel provider) as IaC. This is the **manifest** — it wires the pack onto a project; bindings and conflict registers live in the appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. > **Rendering model — the choice this pack exists to make.** The frontend is a **single-page app**: one static `index.html`, rendered entirely in the browser. **There is no server-side rendering, and none may be added** — not per request, not at build time. The base contract's SPA framing applies verbatim; the enforceable rules and the greppable forbidden list are in `frontend.md` → *Rendering model*. A requirement that genuinely needs server-rendered HTML (public search indexability above all) is a **pack change** — adopt `vercel-ssr` instead. -> **Naming.** Named for its distinguishing choice — the everything-on-Vercel platform — under the platform exception in `../README.md` (the convention triple would be `react-fastify-postgres`). Sibling: `vercel-ssr` is the server-rendered, one-app full-stack Next.js alternative on the same platform; **the `-ssr` suffix is the contrast — this pack is the SPA one**, and it keeps a separate Fastify API. Platform packs coexist by shape suffix per `../README.md`. +> **Naming.** Named for the platform plus its distinguishing shape — client-side rendering on Vercel (the underlying triple is `react-fastify-postgres`, per `../README.md`). Sibling: `vercel-ssr` is the server-rendered, one-app full-stack Next.js alternative on the same platform; **the `-csr`/`-ssr` suffixes are the contrast — this pack is the SPA one**, and it keeps a separate Fastify API. Platform packs coexist by shape suffix per `../README.md`. ## Appendix → base mapping @@ -19,7 +19,7 @@ This pack ships the optional `infra.md` (permitted by `../README.md`): the deplo ## Day-1 wiring -Run as part of the root `README.md` `## Day-1 checklist`: delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here, `infra.md` included (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Finally record in root `CLAUDE.md` **Learnings**: `Stack: vercel; appendices under stacks/vercel/ (4 appendices incl. infra)`. +Run as part of the root `README.md` `## Day-1 checklist`: delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here, `infra.md` included (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Finally record in root `CLAUDE.md` **Learnings**: `Stack: vercel-csr; appendices under stacks/vercel-csr/ (4 appendices incl. infra)`. > **Skip the root `README.md`'s "soften the SPA framing" step.** That instruction targets the server-first packs. This pack **is** a SPA, so the base framing in root `CLAUDE.md`, `apps/frontend/CLAUDE.md`, and the **What's included** "Frontend SPA" row is already correct — leave every one of them as shipped. The repo name is accurate here too. diff --git a/stacks/vercel/backend.md b/stacks/vercel-csr/backend.md similarity index 100% rename from stacks/vercel/backend.md rename to stacks/vercel-csr/backend.md diff --git a/stacks/vercel/db.md b/stacks/vercel-csr/db.md similarity index 100% rename from stacks/vercel/db.md rename to stacks/vercel-csr/db.md diff --git a/stacks/vercel/frontend.md b/stacks/vercel-csr/frontend.md similarity index 100% rename from stacks/vercel/frontend.md rename to stacks/vercel-csr/frontend.md diff --git a/stacks/vercel/infra.md b/stacks/vercel-csr/infra.md similarity index 95% rename from stacks/vercel/infra.md rename to stacks/vercel-csr/infra.md index ca17173..49f2fbb 100644 --- a/stacks/vercel/infra.md +++ b/stacks/vercel-csr/infra.md @@ -41,7 +41,7 @@ Bringing a project online includes its observability — treat it as part of go- - **Enable Vercel Observability** on both projects (requests, function invocations, and runtime logs retained and queryable in the dashboard). - **Ship runtime logs off-platform via a log drain**, so the backend's structured log lines (correlation id, handled errors, integration/webhook results) stay searchable beyond Vercel's short retention. -- **⚠️ The log drain is integration-owned, NOT Terraform.** Wire it through a marketplace integration (e.g. Sentry) that owns the drain on the API project. The Terraform `vercel` provider **cannot import an integration-owned drain**, so **do not** author a `vercel_log_drain` / `observability.tf` resource for it — `terraform apply` would create a **second, duplicate drain**. The integration keeps it live; Terraform simply doesn't track it. Widen coverage (frontend logs, preview/`develop` logs) in the integration's settings, not Terraform. +- **⚠️ The log drain is integration-owned, NOT Terraform.** Wire it through a marketplace integration (e.g. Sentry) that owns the drain on the API project. The Terraform `vercel-csr` provider **cannot import an integration-owned drain**, so **do not** author a `vercel_log_drain` / `observability.tf` resource for it — `terraform apply` would create a **second, duplicate drain**. The integration keeps it live; Terraform simply doesn't track it. Widen coverage (frontend logs, preview/`develop` logs) in the integration's settings, not Terraform. - **Frontend product analytics** — Vercel Web Analytics + Speed Insights — are wired in `frontend.md`. - **When prod misbehaves**, outcomes that are *log-only* (e.g. OTP attempts, webhook deliveries — not persisted) are still recoverable from the request logs: query the project's request-logs filtered by `environment=production`, path, status, and a content substring, rather than `vercel logs`, which only live-tails the last ~2 minutes. diff --git a/stacks/vercel-ssr/README.md b/stacks/vercel-ssr/README.md index 66896c1..c90f9ad 100644 --- a/stacks/vercel-ssr/README.md +++ b/stacks/vercel-ssr/README.md @@ -2,7 +2,7 @@ One **full-stack Next.js** app (App Router, TypeScript) owning both the UI and the server side — Server Components and Server Actions instead of a separate API app · DB **Postgres** — **Neon** (serverless) in production, Docker locally — via **node-pg-migrate** + **`pg`**. Deploys to **Vercel** as a **single project**; Terraform (Vercel provider) as IaC. This is the **manifest** — it wires the pack onto a project; bindings and conflict registers live in the appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. -> **Naming.** Named for the platform plus its distinguishing shape — full-stack SSR on Vercel — under the platform exception in `../README.md`; the would-be convention triple collapses to the pair `nextjs-postgres` (Next.js fills both the frontend and backend slots). Sibling contrast: the `vercel` pack (would-be triple `react-fastify-postgres`) is a **client-rendered SPA with no SSR at all**, served as static assets beside a separate Fastify API on the same platform — **this pack is the server-rendered alternative**, and the `-ssr` suffix marks exactly that. Pick this one when routes must arrive as complete HTML (public search indexability above all); pick `vercel` when they need not. +> **Naming.** Named for the platform plus its distinguishing shape — full-stack SSR on Vercel — under the platform exception in `../README.md`; the would-be convention triple collapses to the pair `nextjs-postgres` (Next.js fills both the frontend and backend slots). Sibling contrast: the `vercel-csr` pack (would-be triple `react-fastify-postgres`) is a **client-rendered SPA with no SSR at all**, served as static assets beside a separate Fastify API on the same platform — **this pack is the server-rendered alternative**, and the `-ssr` suffix marks exactly that. Pick this one when routes must arrive as complete HTML (public search indexability above all); pick `vercel-csr` when they need not. ## Appendix → base mapping diff --git a/stacks/vercel-ssr/backend.md b/stacks/vercel-ssr/backend.md index 00522df..297c6b4 100644 --- a/stacks/vercel-ssr/backend.md +++ b/stacks/vercel-ssr/backend.md @@ -6,7 +6,7 @@ Binds `apps/backend/CLAUDE.md` — relocated at Day-1 to `apps/frontend/src/serv ## Stack binding at a glance -- **Delivery mechanism: Next.js itself — no separate HTTP framework.** The onion lives under `src/server/`; the App Router invokes its controller edge directly. Pack decision — rejected alternative: a second in-repo API server (it forks the deploy unit and re-introduces the network hop, proxy, and CORS surface this shape exists to avoid; the sibling `vercel` pack (would-be triple `react-fastify-postgres`, a client-rendered SPA) *is* that architecture, kept separate). +- **Delivery mechanism: Next.js itself — no separate HTTP framework.** The onion lives under `src/server/`; the App Router invokes its controller edge directly. Pack decision — rejected alternative: a second in-repo API server (it forks the deploy unit and re-introduces the network hop, proxy, and CORS surface this shape exists to avoid; the sibling `vercel-csr` pack (would-be triple `react-fastify-postgres`, a client-rendered SPA) *is* that architecture, kept separate). - **Language: TypeScript.** Ports are interfaces; DTOs are Zod schemas with inferred types. - **Folder layout: the base shape under `src/server/`** — `src/server/modules//{domain,service,repo,controller,dtos}`, `src/server/shared/{aspects,utils}`, `src/server/container.ts`. Path references in the relocated contract map `apps/backend/src/` → `apps/frontend/src/server/`. - **The server boundary is enforced, not hoped for:** `container.ts` and every aspect open with `import 'server-only'`, so a client-component import fails the build instead of leaking server code into the bundle. diff --git a/stacks/vercel-ssr/infra.md b/stacks/vercel-ssr/infra.md index abfc14d..cebdbd0 100644 --- a/stacks/vercel-ssr/infra.md +++ b/stacks/vercel-ssr/infra.md @@ -40,7 +40,7 @@ Bringing a project online includes its observability — treat it as part of go- - **Enable Vercel Observability** on the project (requests, function invocations, and runtime logs retained and queryable in the dashboard). - **Ship runtime logs off-platform via a log drain**, so the server side's structured log lines (correlation id, handled errors, integration/webhook results) stay searchable beyond Vercel's short retention. -- **⚠️ The log drain is integration-owned, NOT Terraform.** Wire it through a marketplace integration (e.g. Sentry) that owns the drain on the project. The Terraform `vercel` provider **cannot import an integration-owned drain**, so **do not** author a `vercel_log_drain` / `observability.tf` resource for it — `terraform apply` would create a **second, duplicate drain**. The integration keeps it live; Terraform simply doesn't track it. Widen coverage (preview/`develop` logs) in the integration's settings, not Terraform. +- **⚠️ The log drain is integration-owned, NOT Terraform.** Wire it through a marketplace integration (e.g. Sentry) that owns the drain on the project. The Terraform `vercel-csr` provider **cannot import an integration-owned drain**, so **do not** author a `vercel_log_drain` / `observability.tf` resource for it — `terraform apply` would create a **second, duplicate drain**. The integration keeps it live; Terraform simply doesn't track it. Widen coverage (preview/`develop` logs) in the integration's settings, not Terraform. - **Frontend product analytics** — Vercel Web Analytics + Speed Insights — are wired in `frontend.md`. - **When prod misbehaves**, outcomes that are *log-only* (e.g. OTP attempts, webhook deliveries — not persisted) are still recoverable from the request logs: query the project's request-logs filtered by `environment=production`, path, status, and a content substring, rather than `vercel logs`, which only live-tails the last ~2 minutes. diff --git a/stacks/taro-fastify-mysql-tencent/README.md b/stacks/wechat/README.md similarity index 81% rename from stacks/taro-fastify-mysql-tencent/README.md rename to stacks/wechat/README.md index b526440..f19c5e8 100644 --- a/stacks/taro-fastify-mysql-tencent/README.md +++ b/stacks/wechat/README.md @@ -1,8 +1,8 @@ -# Stack pack: taro-fastify-mysql-tencent +# Stack pack: wechat Frontend **Taro 4 H5** (React 18, plain JavaScript) · Backend **Fastify 4** (CommonJS) · DB **MySQL 8** — **CynosDB** (serverless) in production, Docker locally — via **Knex** (`mysql2`). Deploys to **Tencent Cloud**: one **SCF Web Function** serves both the JSON API and the built H5 bundle, a separate SCF **event** function runs migrations; **CynosDB** for data, **COS** (private) for media behind signed URLs, **VOD** for video, **EdgeOne** as the CDN/WAF edge; Terraform (`tencentcloud` provider) for IaC and **GitHub Actions** as the deploy pipeline. This is the manifest; bindings and conflict registers live in the appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. -> **Naming.** The framework triple is `taro-fastify-mysql`; the `-tencent` suffix marks the deployment platform, appended because the Tencent-Cloud specifics (SCF bundling, CynosDB serverless, COS/VOD, EdgeOne, mainland ICP + public-net egress) are load-bearing (`../README.md` permits appending the distinguishing choice). Lift the app to another cloud and the triple stays, the suffix changes. +> **Naming.** Named for the product surface — a WeChat mini-program — because that is the identity an adopter picks it by; the underlying triple is `taro-fastify-mysql` on Tencent Cloud (per `../README.md`). The Tencent-Cloud specifics (SCF bundling, CynosDB serverless, COS/VOD, EdgeOne, mainland ICP + public-net egress) are load-bearing throughout the appendices. Lift the app to another cloud and the triple stays; the platform specifics are what change. ## Appendix → base mapping @@ -17,7 +17,7 @@ This pack ships the optional `infra.md` (permitted by `../README.md`): the deplo ## Day-1 wiring -Part of the root `README.md` Day-1 checklist. Delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here, `infra.md` included (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Record in root `CLAUDE.md` **Learnings**: `Stack: taro-fastify-mysql-tencent; appendices under stacks/taro-fastify-mysql-tencent/`. +Part of the root `README.md` Day-1 checklist. Delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here, `infra.md` included (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Record in root `CLAUDE.md` **Learnings**: `Stack: wechat; appendices under stacks/wechat/`. ## Suggested toolchain @@ -45,4 +45,4 @@ Bindings for the shipped add-ons: **test-mode** and **otp-auth** in `backend.md` ## Deploy seam -Deployment is a GitHub Actions pipeline (`.github/workflows/deploy.yml`) — this pack fills it in rather than deleting it (contrast `vercel`). On a push to the default branch (or `workflow_dispatch`): build frontend + backend, compose one SCF zip, resume CynosDB if paused, `terraform apply`, push function code out-of-band, invoke the migrate function, smoke-test the live URL. Terraform owns function config / env / role / triggers and every other resource — see `infra.md`. Protect the default branch so CI is green before merge; a push then both merges and deploys. +Deployment is a GitHub Actions pipeline (`.github/workflows/deploy.yml`) — this pack fills it in rather than deleting it (contrast `vercel-csr`). On a push to the default branch (or `workflow_dispatch`): build frontend + backend, compose one SCF zip, resume CynosDB if paused, `terraform apply`, push function code out-of-band, invoke the migrate function, smoke-test the live URL. Terraform owns function config / env / role / triggers and every other resource — see `infra.md`. Protect the default branch so CI is green before merge; a push then both merges and deploys. diff --git a/stacks/taro-fastify-mysql-tencent/backend.md b/stacks/wechat/backend.md similarity index 100% rename from stacks/taro-fastify-mysql-tencent/backend.md rename to stacks/wechat/backend.md diff --git a/stacks/taro-fastify-mysql-tencent/db.md b/stacks/wechat/db.md similarity index 96% rename from stacks/taro-fastify-mysql-tencent/db.md rename to stacks/wechat/db.md index c160802..39a251b 100644 --- a/stacks/taro-fastify-mysql-tencent/db.md +++ b/stacks/wechat/db.md @@ -21,7 +21,7 @@ Binds the base `db/CLAUDE.md` (+ the backend repo ring) to **MySQL 8** — **Cyn - Base schema conventions apply unchanged (`db/CLAUDE.md` *Schema conventions*); MySQL-8 specifics below. - **A `CHECK` constraint validates against *existing* rows on MySQL 8** — so you cannot add one to a table whose legacy rows already violate it. Enforce such invariants **in the service**, not with a late `CHECK`, when legacy or imported data may violate them. -- **Fixed value sets: a native MySQL `ENUM` is acceptable** — widening it (`ALTER TABLE ... MODIFY ... ENUM(...)`) is a plain reversible migration (contrast Postgres, where the `vercel` pack avoids native enums). A new challenge `purpose` is added this way. +- **Fixed value sets: a native MySQL `ENUM` is acceptable** — widening it (`ALTER TABLE ... MODIFY ... ENUM(...)`) is a plain reversible migration (contrast Postgres, where the `vercel-csr` pack avoids native enums). A new challenge `purpose` is added this way. ## Repo ring binding (Knex) @@ -31,7 +31,7 @@ Binds the base `db/CLAUDE.md` (+ the backend repo ring) to **MySQL 8** — **Cyn ## Local dev, seed & the destructive test-DB ritual -- **Local MySQL is one fixed-name Docker container**, shared across worktrees per the base — reuse it, run `migrate`, never start a second copy (the base shared-DB rule stands; this stack does **not** use per-worktree databases, unlike the `vercel` pack). +- **Local MySQL is one fixed-name Docker container**, shared across worktrees per the base — reuse it, run `migrate`, never start a second copy (the base shared-DB rule stands; this stack does **not** use per-worktree databases, unlike the `vercel-csr` pack). - **Seed** realistic, named accounts + content (base `db/CLAUDE.md`) so manual/e2e testing and the **test-mode** add-on's test-user picker have lifelike data; idempotent, upsert by business key. - **The test suite is destructive** — it truncates tables. The runner **refuses to run unless `DB_NAME` ends in `_test`**, and `pnpm test` auto-suffixes it, so the dev schema is never touched. One-time setup creates and migrates the `*_test` schema (`pnpm --filter backend test:db:setup`, idempotent). This `*_test`-schema guard *is* this stack's binding of the base "destructive checks go to a throwaway DB" rule. diff --git a/stacks/taro-fastify-mysql-tencent/frontend.md b/stacks/wechat/frontend.md similarity index 100% rename from stacks/taro-fastify-mysql-tencent/frontend.md rename to stacks/wechat/frontend.md diff --git a/stacks/taro-fastify-mysql-tencent/infra.md b/stacks/wechat/infra.md similarity index 97% rename from stacks/taro-fastify-mysql-tencent/infra.md rename to stacks/wechat/infra.md index 8faf898..2b850f0 100644 --- a/stacks/taro-fastify-mysql-tencent/infra.md +++ b/stacks/wechat/infra.md @@ -38,7 +38,7 @@ Binds `infra/CLAUDE.md` to the **`tencentcloud` Terraform provider**: the produc ## Deploy pipeline — GitHub Actions (`.github/workflows/deploy.yml`) -This stack **fills `deploy.yml` in** (contrast `vercel`, which deletes it). On a push to the default branch (or `workflow_dispatch` with `reset_schema` / `force_reseed_test_users` inputs), the ordered job: +This stack **fills `deploy.yml` in** (contrast `vercel-csr`, which deletes it). On a push to the default branch (or `workflow_dispatch` with `reset_schema` / `force_reseed_test_users` inputs), the ordered job: 1. builds the frontend same-origin (`TARO_APP_API_BASE=/api`); 2. esbuild-bundles `handler.js` + `migrate.js` and composes **one SCF zip** — bootstrap + `node_modules` (`mysql2` only) + `db/migrations/` + the H5 `public/`; From 1652d5f7095bc092bdfb8202512020ffd05f1408 Mon Sep 17 00:00:00 2001 From: DeyangChan Date: Wed, 29 Jul 2026 14:04:32 +0800 Subject: [PATCH 02/21] =?UTF-8?q?docs:=20rename=20repo=20cavalry-template-?= =?UTF-8?q?spa=20=E2=86=92=20vstack-template-base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old name encoded the SPA shape; the new name is stack-neutral, so the 'repo name encodes spa — accepted as stale' caveats in the README and the vercel-csr/vercel-ssr pack manifests are no longer true and are dropped. Co-Authored-By: Claude Fable 5 --- README.md | 4 ++-- stacks/vercel-csr/README.md | 2 +- stacks/vercel-ssr/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a02e129..f7bcb0e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# cavalry-template-spa +# vstack-template-base **An opinionated template for spinning up production-ready, full-stack projects — fast, and without re-litigating a single engineering decision.** @@ -93,6 +93,6 @@ Run this once, top to bottom, the first time you instantiate the template. Each 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-csr` and `vercel-ssr` packs that is the `develop` branch plus its dedicated Neon branch (the pack's `infra.md` → *Staging environment*), migrated with the same manual runbook as prod (the pack's `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 | grep -v '^\./README\.md:'` and `grep -n '^ ' CLAUDE.md`. (Only this root README — whose checklist names the markers — is filtered out; delete it once instantiation is done if you prefer a clean tree.) -> If you chose a server-first Next.js pack (`enterprise` or `vercel-ssr`), soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", the opening line of `apps/frontend/CLAUDE.md` ("how the single-page app is structured") likewise, and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". (`vercel-ssr`'s one-app restructure step covers this and more — see its README.) The repo name still encodes "spa" and is immutable — accepted as stale. +> If you chose a server-first Next.js pack (`enterprise` or `vercel-ssr`), soften the SPA framing the base ships agnostic: root `CLAUDE.md` "the single-page app" → "the web frontend", the opening line of `apps/frontend/CLAUDE.md` ("how the single-page app is structured") likewise, and the **What's included** "Frontend SPA" row above → "Frontend (server-first Next.js)". (`vercel-ssr`'s one-app restructure step covers this and more — see its README.) > > **The `vercel-csr` pack is not one of them** — it is a client-rendered SPA with no SSR, so the base framing above is already correct for it and every one of those files stays exactly as shipped. Don't soften anything. diff --git a/stacks/vercel-csr/README.md b/stacks/vercel-csr/README.md index 11b570a..24ddc8e 100644 --- a/stacks/vercel-csr/README.md +++ b/stacks/vercel-csr/README.md @@ -21,7 +21,7 @@ This pack ships the optional `infra.md` (permitted by `../README.md`): the deplo Run as part of the root `README.md` `## Day-1 checklist`: delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here, `infra.md` included (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Finally record in root `CLAUDE.md` **Learnings**: `Stack: vercel-csr; appendices under stacks/vercel-csr/ (4 appendices incl. infra)`. -> **Skip the root `README.md`'s "soften the SPA framing" step.** That instruction targets the server-first packs. This pack **is** a SPA, so the base framing in root `CLAUDE.md`, `apps/frontend/CLAUDE.md`, and the **What's included** "Frontend SPA" row is already correct — leave every one of them as shipped. The repo name is accurate here too. +> **Skip the root `README.md`'s "soften the SPA framing" step.** That instruction targets the server-first packs. This pack **is** a SPA, so the base framing in root `CLAUDE.md`, `apps/frontend/CLAUDE.md`, and the **What's included** "Frontend SPA" row is already correct — leave every one of them as shipped. ## Suggested toolchain (pnpm workspaces, ESM, Node 22) diff --git a/stacks/vercel-ssr/README.md b/stacks/vercel-ssr/README.md index c90f9ad..0cc9a81 100644 --- a/stacks/vercel-ssr/README.md +++ b/stacks/vercel-ssr/README.md @@ -22,7 +22,7 @@ This stack has no separate backend app — the onion lives inside the Next.js ap 1. `git mv apps/backend/CLAUDE.md apps/frontend/src/server/CLAUDE.md` — the onion contract moves with the code it governs; agents pick it up when working under `src/server/`. 2. Prepend one line to the moved file: `> Relocated from apps/backend/ — this directory is the backend; path references to apps/backend/src/ map here. Bindings: the adopted pack's backend.md.` 3. `rm -rf apps/backend`. -4. Soften the root framing: in the root `CLAUDE.md` repo shape, drop the `apps/backend` row and describe `apps/frontend` as "the full-stack Next.js app (UI + server side)"; update the root `README.md` **What's included** rows to match. The repo name still encodes "spa" and is immutable — accepted as stale. +4. Soften the root framing: in the root `CLAUDE.md` repo shape, drop the `apps/backend` row and describe `apps/frontend` as "the full-stack Next.js app (UI + server side)"; update the root `README.md` **What's included** rows to match. `backend.md`'s conflict register records this override; the relocated contract applies unchanged apart from paths. From f49407600481cbcc8ff75373188ec1f9b6de234c Mon Sep 17 00:00:00 2001 From: DeyangChan Date: Wed, 29 Jul 2026 14:15:12 +0800 Subject: [PATCH 03/21] =?UTF-8?q?feat(stacks):=20add=20mern=20pack=20?= =?UTF-8?q?=E2=80=94=20React=20SPA=20+=20Express=205=20+=20MongoDB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform-neutral identity-named pack (triple react-express-mongo): Vite/React SPA, Express 5 plain-JS ESM API as a long-lived process, MongoDB via Mongoose with migrate-mongo up/down migrations. Binds test-mode and otp-auth in backend.md; saas-billing bound and seo recorded unbound (CSR SPA, S1 unmeetable) in the add-ons' bindings files. Co-Authored-By: Claude Fable 5 --- add-ons/saas-billing/bindings.md | 4 ++ add-ons/seo/bindings.md | 8 +++ stacks/mern/README.md | 51 ++++++++++++++++++ stacks/mern/backend.md | 61 +++++++++++++++++++++ stacks/mern/db.md | 58 ++++++++++++++++++++ stacks/mern/frontend.md | 91 ++++++++++++++++++++++++++++++++ 6 files changed, 273 insertions(+) create mode 100644 stacks/mern/README.md create mode 100644 stacks/mern/backend.md create mode 100644 stacks/mern/db.md create mode 100644 stacks/mern/frontend.md diff --git a/add-ons/saas-billing/bindings.md b/add-ons/saas-billing/bindings.md index 1762869..e5e036a 100644 --- a/add-ons/saas-billing/bindings.md +++ b/add-ons/saas-billing/bindings.md @@ -17,3 +17,7 @@ Pre-written entries for the stack packs this template ships, kept **inside the a ## `wechat` — bound - **If you adopt the `saas-billing` add-on** (`add-ons/saas-billing/`): the provider is market-dependent — **Stripe** for international deployments; a mainland-China product swaps the gateway for **WeChat Pay** behind the same port. `lib/billing.js` is the gateway home (services call it, never an SDK directly); the default-off billing flag routes it to the stub sink. Webhook route: preserve the raw payload (scoped content-type parser) and verify the signature in a `preHandler` before schema validation. The reconciliation sweep is a separately-bundled scheduled SCF function (same pattern as `migrate.js`); provider secrets are validated at boot in the entry. + +## `mern` — bound + +- **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` SDK). The gateway is a repo-ring adapter behind the domain's billing gateway port, wired in `container.js`; `BILLING_ENABLED=false` binds the stub gateway there instead (tests swap it via `overrides`). Webhook seam: signature verification needs the **raw body**, and the app-wide `express.json()` consumes it — mount `express.raw({ type: 'application/json' })` on the webhook route only and verify the Stripe signature against the raw buffer before anything parses. Jobs: the API is a long-lived process, so the reconciliation sweep, usage rollover, and trial expiry run on an in-process scheduler (e.g. `node-cron`) started from the composition root — keep each run idempotent; if the deployment scales past one instance, move the schedule to the pipeline the base `infra/CLAUDE.md` contract stands up (this pack ships no `infra.md`) rather than letting instances double-fire. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` join the boot-time Zod env schema. diff --git a/add-ons/seo/bindings.md b/add-ons/seo/bindings.md index 8c9be4f..777c232 100644 --- a/add-ons/seo/bindings.md +++ b/add-ons/seo/bindings.md @@ -43,3 +43,11 @@ Per seam item: ## wechat — unbound Unmeetable seam item: **S1** — the form factor is a phone-first, app-like H5/PWA client, and Taro H5's client-only rendering cannot serve indexable routes complete without client JS. Workable alternative: a project on this stack that grows a public crawlable surface serves it outside the Taro bundle (its own prerendered or server-rendered pages) and binds the add-on there. Residual posture: R10 survives unbinding — a publicly reachable H5 origin that shouldn't appear in search results still serves a refuse-indexing response (deny-all robots / noindex header) regardless. + +## mern — unbound + +Unmeetable seam item: **S1** — that pack is a **client-rendered SPA with no SSR by deliberate design** (`stacks/mern/frontend.md` → *Rendering model*). Every route is served as the same static `index.html` and painted by JavaScript, so there is no mechanism that makes an indexable route complete without client-side scripts, and nothing to hang S2–S9 off either: no server render to emit per-route `` tags, no server response to carry a 301 or a real 404 status. Adding one is a pack change, not a patch — do not reach for a prerender/SSG plugin to close this gap; that is precisely what the pack's forbidden list rules out. + +Workable alternative: this pack has no same-stack SSR sibling — adopt a server-rendered pack instead (**`vercel-ssr`** or **`enterprise`**, both bound above), or keep the SPA and serve the crawlable surface **outside** the app bundle (its own server-rendered origin — a marketing site, a docs site) and bind the add-on there. + +Residual posture: **R10 survives unbinding** — the SPA origin is publicly reachable, so it still serves a refuse-indexing response. Bind that much: a static `public/robots.txt` disallowing all, plus `X-Robots-Tag: noindex` from the serving layer's headers on every non-production environment (the pack's deploy seam — `stacks/mern/frontend.md` → *Serving `dist/`*, requirement 3's header home), and a Playwright assertion on the non-production response (gate G3). No platform answers noindex for this pack by default — the serving layer must, and the suite assertion, not platform behaviour, is the gate. diff --git a/stacks/mern/README.md b/stacks/mern/README.md new file mode 100644 index 0000000..c721498 --- /dev/null +++ b/stacks/mern/README.md @@ -0,0 +1,51 @@ +# Stack pack: mern + +Frontend **React SPA** (Vite, TypeScript) — **client-rendered, no SSR** · Backend **Express 5** (plain JavaScript, ESM) · DB **MongoDB** via **Mongoose**, migrations via **migrate-mongo**. Platform-neutral: the product deploys through whatever the base `infra/` contract stands up — this pack ships no `infra.md`. This is the **manifest** — it wires the pack onto a project; bindings and conflict registers live in the appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. + +> **Naming.** Named for the well-known stack acronym — MongoDB, Express, React, Node (the underlying triple is `react-express-mongo`, per `../README.md`). Like `enterprise`, this pack is platform-neutral — it binds no deployment platform. Its shape is the `vercel-csr` shape minus the platform: a client-rendered SPA with a separate long-lived Node API. + +> **Rendering model.** The frontend is a **single-page app** — one static `index.html`, rendered entirely in the browser. **There is no server-side rendering, and none may be added** — not per request, not at build time. The base contract's SPA framing applies verbatim; the enforceable rules and the greppable forbidden list are in `frontend.md` → *Rendering model*. A requirement that genuinely needs server-rendered HTML (public search indexability above all) is a **pack change** — adopt a server-rendered pack (`vercel-ssr`, `enterprise`) instead. + +## Appendix → base mapping + +| Appendix | Binds onto | Scope | +|---|---|---| +| `frontend.md` | `apps/frontend/CLAUDE.md` | the client-only rendering model (no SSR), Vite + React Router, the `dist/` serving requirements (SPA fallback + `/api` proxy), REST-only data flow, Tailwind 4 + Radix, four-states mapping | +| `backend.md` | `apps/backend/CLAUDE.md` | Express routers/middleware → onion mapping, manual composition root, long-lived-process entrypoint, plain-JS stance | +| `db.md` | `db/CLAUDE.md` + repo ring | Mongoose models as the schema home, migrate-mongo (real up/down pairs), replica-set transactions, document-store deltas | + +This pack ships no `infra.md` (infra is cloud-shaped, not app-stack-shaped — `../README.md`); the deploy seam is the base `infra/CLAUDE.md` contract plus the serving requirements `frontend.md` names. Each appendix opens with the verbatim precedence line and ends with its conflict register; conflicts live in the appendices, not here. + +## Day-1 wiring + +Run as part of the root `README.md` `## Day-1 checklist`: delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Finally record in root `CLAUDE.md` **Learnings**: `Stack: mern; appendices under stacks/mern/`. + +> **Skip the root `README.md`'s "soften the SPA framing" step.** That instruction targets the server-first packs. This pack **is** a SPA, so the base framing in root `CLAUDE.md`, `apps/frontend/CLAUDE.md`, and the **What's included** "Frontend SPA" row is already correct — leave every one of them as shipped. + +## Suggested toolchain (pnpm workspaces, ESM, Node 22) + +pnpm workspaces over `apps/*`; root `"type": "module"`; pin `packageManager` in the root manifest and the Node major (22) in `engines`. + +**Dev block → root `CLAUDE.md` "Common commands":** + +```bash +pnpm bootstrap # install + start local MongoDB (fixed-name docker container, single-node replica set, shared across worktrees) + migrate +pnpm dev # both dev servers in parallel: Express watch (:4000) + Vite dev (:5173, proxying /api → :4000) +pnpm lint # ESLint (flat config) in both apps +pnpm typecheck # frontend tsc --noEmit (backend is plain JS — explicit no-op script) +pnpm test # backend node --test suite (Playwright e2e is separate: pnpm test:e2e) +pnpm build # vite build → apps/frontend/dist (backend is plain JS — its build/typecheck scripts are explicit no-ops) +pnpm migrate # migrate-mongo up (rollback: pnpm migrate:down) +``` + +**CI block → `.github/workflows/ci.yml` (non-interactive):** a scratch MongoDB started as a **single-node replica set** (a `docker run … mongod --replSet rs0` step — GitHub service containers can't pass the flag, and transactions refuse a standalone); `pnpm install --frozen-lockfile`; lint; frontend `tsc --noEmit`; backend `node --test`; `vite build`; the frontend **i18n key-parity** check (the base gate stands — this pack changes nothing about it); **migration round-trip up → down → up on the scratch database** — migrate-mongo has real downs, so keep the base `ci.yml` round-trip TODO's wording as written, with drift asserted the document-store way per `db.md` → *CI checks*; run the seed twice (idempotency). Playwright e2e is not part of this job — it runs against a deployed or locally served stack via `E2E_BASE_URL`. + +**Validation:** **Zod** on both sides — backend edge DTOs and boot-time env schemas, frontend form/response shapes; a shape shared across the two is defined once and reused. Details in `backend.md` / `frontend.md`. + +## Add-ons + +Bindings for the shipped add-ons: **test-mode** and **otp-auth** in `backend.md`. **saas-billing** carries its own bindings file inside the add-on (`add-ons/saas-billing/bindings.md`), with a bound section for this pack. **seo** carries one too (`add-ons/seo/bindings.md`), where this pack is recorded **unbound**: its S1 seam asks for a rendering mechanism that serves indexable routes complete without client-side scripts, and a client-only SPA has none — adopting `seo` means adopting a server-rendered pack instead, or serving the crawlable surface outside this app. **llm-calls**, **premium-design**, **enterprise-compliance**, and **multi-tenancy** are left unbound by this pack — adopting one means supplying its *Binds to a stack* answers in the matching appendix as part of adoption. + +## Deploy seam + +Deployment goes through the base contract — CI/CD workflows under `.github/workflows/`, with `ci.yml` as the merge gate and the cloud pipeline `infra/CLAUDE.md` stands up doing the shipping. The two workloads it must run: the Express API as a **long-lived Node process** (any container/Node host target), and the SPA's `dist/` behind a static host or reverse proxy meeting the serving requirements in `frontend.md` → *Serving `dist/`*. The root `migrate` verb runs in the pipeline **before** the rollout that reads the new shape (`db.md` → *Rolling out*). diff --git a/stacks/mern/backend.md b/stacks/mern/backend.md new file mode 100644 index 0000000..b61b7ec --- /dev/null +++ b/stacks/mern/backend.md @@ -0,0 +1,61 @@ +# Express 5 — backend appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds `apps/backend/CLAUDE.md` (the onion) and the root `CLAUDE.md` to **Express 5, plain JavaScript (ESM), run as a long-lived Node process**. Read those first; this file does not restate them. Data layer → `./db.md`; where the process runs → the base `infra/CLAUDE.md` contract (this pack ships no `infra.md`). + +## Stack binding at a glance + +- **HTTP layer: Express 5, used directly** — no framework on top. The base's illustrative "Express/Fastify-style" layer is bound to real Express: routers and middleware are the aspect mechanism the base already names for Node. Pack decision — rejected alternative: Fastify (the Vercel siblings' pick): the E in MERN is the identity this pack exists to offer, and Express 5 forwards a rejected async handler to the error middleware natively — the historic reason to leave Express is gone. +- **Language: plain JavaScript, ESM** (`"type": "module"`), with **no build or typecheck step** — the `build`/`typecheck` scripts are explicit no-ops, not omissions. Pack decision — rejected alternative: TypeScript (see conflict register). +- **Folder layout: the base shape verbatim** — `modules//{domain,service,repo,controller,dtos}`, `shared/{aspects,utils}`, `container.js`. No mapping table needed; this stack is the layout the base illustrates. +- **Validation: Zod** — `dtos/` are Zod schemas at the controller edge; one Zod schema parses env at boot. + +## Composition root — `container.js`, manual wiring (base default) + +- The base's manual factory wiring stands — no DI container. One exported `buildContainer({ env, overrides })` wires every repo, gateway, and use case as plain factory calls (`makeCreateOrder({ orderRepo, withTransaction })`), resolved once at boot; tests replace any dependency by name via `overrides` without touching a ring. + +## Controller ring — one Express router per module + +- Each module exposes `controller/routes.js` — a factory receiving its use cases from the container and returning an `express.Router`. `app.js` mounts every module router under **one `/internal/v1` prefix**, plus an unversioned `/health` — the base visibility/versioning rule as a mount path; add an `/external/v1` mount only when a genuinely third-party consumer exists. +- A handler parses with the module's Zod DTO, invokes **one** use case, and maps the result to a response DTO — base rules, bound to Express handler signatures. +- **Async errors need no wrapper.** Express 5 routes a rejected handler promise to the error middleware; a hand-rolled `asyncHandler`/`try-catch` wrapper is the greppable smell. + +## Aspects — `shared/aspects/` as Express middleware + +| Base concern | Binding | +|---|---| +| Config | `env.js` — one Zod schema, parsed once at boot (`loadEnv()`), **fail fast** on missing/invalid keys; values passed inward — no `process.env` reads in rings | +| DB access | `db.js` — opens the single Mongoose connection and exports `withTransaction` (mechanics in `./db.md`) | +| Request context | `request-context.js` — correlation id seeded per request at the edge, passed inward as a plain value, echoed as the `x-correlation-id` header | +| Errors | `errors.js` — **one** 4-arg error middleware, registered last, mapping domain errors (plain error helpers in `shared/utils/`) → the base *Error responses* envelope; inner rings never shape an HTTP response | +| Auth | `auth.js` — cookie-session guard middleware applied to the routers that need it | + +**Scope-to-subtree = router-level `use`.** Apply an aspect on the module router (or route) that needs it — not `app.use` by reflex; only the error middleware, request context, session/cookie handling, helmet, and the JSON body parser register app-wide at bootstrap. + +## Sessions & edge hardening + +- **Sessions are signed HTTP-only cookies** (`cookie-session` with `SESSION_SECRET`) — stateless, no session store to run; binds the base cookie-session default. +- **Security headers via `helmet`**, registered once at bootstrap — binds the base *Security baseline* header rule. Helmet's default `Content-Security-Policy` is fine for a JSON API; tune it only if the API ever serves HTML. +- **`trust proxy` from validated config** — deployed, the API sits behind the SPA origin's `/api` reverse proxy (`./frontend.md`), so without it rate limiting and logging key on the proxy IP, not the client. +- **Rate limiting: `express-rate-limit`** — its in-memory store is per-process; fine for a single long-lived instance. A limit that must survive restarts or span instances keeps its counters in MongoDB (the otp-auth binding below does). +- **SSRF guard + write-only secrets bind the base *Security baseline*.** The URL guard is a shared helper — public `https` only; **resolve the host and reject loopback/private/link-local/cloud-metadata IPs** — called at config-save *and* immediately before the outbound `fetch`. Admin-managed secrets: the read DTO masks each secret and adds a `…Set` flag; the update handler preserves a blank field over the stored value. + +## Entrypoint — `src/server.js` + +- `buildApp({ container })` assembles middleware + routers and returns the app; `src/server.js` builds it and calls `listen(PORT)`. Dev: `node --watch --env-file=../../.env src/server.js`. +- One long-lived process: in-process schedulers and memos are legitimate here — but anything correctness-bearing still lives in MongoDB, so a restart or a second instance never corrupts state. + +## Testing + +- **Runner: `node:test`** (`node --test tests/`; `tests/` mirrors `src/`). Pack decision — rejected alternative: Jest/Vitest (plain ESM JavaScript needs no transform; the built-in runner is zero-dependency). +- Base per-ring kinds, bound: **domain** — plain units; **service** — `buildContainer` with `overrides` fakes; **controller** — **supertest** against the `buildApp` instance (no listener needed); **repo** — integration against the real local MongoDB replica set (`--test-concurrency=1` where suites share it). + +## Add-on bindings (if adopted) + +- **test-mode** (`add-ons/test-mode/`): a `shared/aspects/` middleware resolves the mode signal from an inbound header onto the request context — fail closed: missing or unknown means production. In test mode the flag-gated integrations (the base default-off booleans) route to their stdout/no-op sinks. The test-user picker is a route gated on the same signal; it returns `[]` in production, and a test asserts that. +- **otp-auth** (`add-ons/otp-auth/`): model A (self-managed) — an `otp_challenges` collection via a repo-ring Mongoose model; its migrate-mongo migration ships the **unique index on (target, purpose)** — a duplicate-key error (`E11000`) resolves the double-submit race to `409` per the base status table — and a **TTL index** on the expiry field (TTL deletion is a lazy ~60s sweep, so verify still checks expiry itself). Hashing + timing-safe verify in `shared/utils/`; delivery through gateway adapters behind domain ports, gated by the default-off flags; phone numbers canonicalised to E.164 with `libphonenumber-js`. Attempt rate limits keep their counters in MongoDB; in test mode delivery is sinked to the structured log — the tester reads the real code there, and verify is never stubbed. + +## Conflict register + +- **Base says:** The stack is unchosen; the JS-style filenames and Express/Fastify-style HTTP layer are illustrative, not mandates. **In this stack:** bound for real — Express 5, plain JavaScript ESM, and no build/typecheck step. **Because:** a long-lived Node process runs source directly; with duck-typed ports (base default), Zod-guarded edges, and JSDoc `@typedef`s, a transpile step adds weight without payoff here. **Concretely:** DON'T add a `tsconfig`/transpiler under `apps/backend/`; keep `build`/`typecheck` as explicit no-op scripts so workspace-wide commands stay green. diff --git a/stacks/mern/db.md b/stacks/mern/db.md new file mode 100644 index 0000000..09b9796 --- /dev/null +++ b/stacks/mern/db.md @@ -0,0 +1,58 @@ +# MongoDB (Mongoose) + migrate-mongo — db appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds the base `db/CLAUDE.md` (+ the backend repo ring) to **MongoDB** — a fixed-name Docker container locally, whatever the base `infra/` contract stands up in production — with **Mongoose** as the schema/query layer and **migrate-mongo** for migrations. Read the base files first. + +**Scope.** This file owns migrations, seed, schema conventions, and the repo-ring model/session mechanics. Connection wiring lives in `./backend.md`; where `migrate` runs in the pipeline is the base `infra/CLAUDE.md` contract's call. + +## Tool picks + +- **Mongoose** — pack decision; rejected alternative: the raw `mongodb` driver. A document store has no server-side DDL, so the shape must live *somewhere* in code — Mongoose models are that single home (typed casting, schema validation at the app boundary), instead of an untyped shape implied by scattered queries. +- **migrate-mongo** — migrations with real paired `up`/`down` under `db/migrations/`, so the base reversibility and up→down→up round-trip rules apply. Pack decision; rejected alternative: no migration tool (Mongoose `autoIndex` + ad-hoc scripts) — index builds and document rewrites need ordered, tracked, reversible history exactly as the base demands. + +## What a migration *is* here + +No DDL: collections and fields materialise on first write. A migration is what the server holds durable state on — **index create/drop, collection/validator setup, and rewrites of existing documents** (shape changes, backfills). The document shape itself lives in the Mongoose models — see the conflict register for both deltas. + +## Migrations (migrate-mongo mechanics) + +- Live under `db/migrations/`, run via the root `migrate` verb (`migrate-mongo up`; rollback `migrate:down`). `migrate-mongo-config.js` pins `migrationsDir: 'db/migrations'`, the changelog collection, and **`moduleSystem: 'esm'`** — the workspace is `"type": "module"`, and without the option migrate-mongo `require()`s migrations and fails to load them. +- Create with `migrate-mongo create ` — the generated datetime prefix satisfies the base timestamp rule. Each file exports async `up(db, client)` / `down(db, client)`. +- **Reversibility, bound:** every `up` ships its real `down` — drop the index it created, reverse the rename, unset the field it set. A genuinely irreversible change has a `down` that **throws**, carrying the base's justification comment (migrate-mongo has no `down = false` convention) — never a silent empty `down`. +- **Not transactional** — write every migration idempotent and resumable instead; see the conflict register. + +## Schema conventions (bound) + +- Base conventions bound: `timestamps: true` on every schema (`createdAt`/`updatedAt` — camelCase per the register below); BSON dates are UTC by construction; money as integer minor units or `Decimal128` — never a plain float; a **unique index** encodes the business invariant, and a duplicate-key error (`E11000`, code `11000`) maps to the domain conflict → `409`. +- Index every reference field and every frequent filter/sort field — **by migration** (register entry below); MongoDB indexes nothing but `_id` for you. +- `_id` stays the default `ObjectId` (opaque, non-enumerable) — exposed as an opaque string at the edge; don't mint a second id field without a reason. + +## Repo ring binding (Mongoose) + +- **Only the repo ring imports `mongoose`.** Models are repo-ring artifacts — defined in each module's `repo/`, registered on the single connection the backend's db aspect opens at boot; repos receive their models via the container. +- **Reads are `.lean()` + mapper.** A hydrated Mongoose document carries `save()` and a live connection — leaking one inward hands an inner ring database access. Repos return domain objects mapped from lean docs; use explicit field projections where a subset suffices. +- **Query filters are built from validated scalars, never a request-supplied object.** Operator injection (`{"$gt": ""}` arriving in a JSON body) is the document-store shape of SQL injection — this binds the base "never interpolate request data" rule. Spreading `req.query`/`req.body` into a filter is the greppable violation. +- **Transactions:** the db aspect exports `withTransaction(work)` over a Mongoose session; repos accept an optional session argument so a multi-document use case shares one transaction (requires the replica set, below). A single-document write is atomic by construction — don't open a transaction for one `updateOne`; prefer modelling an aggregate as one document so its invariants commit atomically. + +## Local dev & seed + +- **Local MongoDB is one fixed-name Docker container** (`mongo:7`), started by the root `bootstrap` script with start-or-run semantics, shared across worktrees per the root `CLAUDE.md` — reuse it, never start a second copy. Run it as a **single-node replica set** (`mongod --replSet rs0`, one-time `rs.initiate()` in bootstrap): multi-document transactions refuse a standalone, so a plain container silently breaks `withTransaction`. +- **One shared mongod, one database per worktree.** Derive the name deterministically (sanitize the branch to `[a-z0-9_]`, prefix `app_` — `feature/x` → `app_feature_x`) and re-point the db-name path segment of `MONGODB_URL` after copying `.env` in; nothing to create — MongoDB materialises a database on first write. Drop it (`dropDatabase`) on worktree teardown. Round-trip and destructive checks run against your own worktree database — scratch by construction. (Register entry below.) +- **Seed:** `db/seed-dev.js`, idempotent (`updateOne(..., { upsert: true })` by business key), run explicitly via a root script — non-production only (base rule stands). + +## CI checks (drop into `.github/workflows/ci.yml`) + +Against a scratch MongoDB started as a **single-node replica set** (a `docker run … mongod --replSet rs0` step — GitHub service containers can't pass the flag): migrations apply from zero (`up`); **round-trip `up → down → up`** — the base gate stands, migrate-mongo has real downs, so keep the base `ci.yml` round-trip TODO's wording — with drift asserted the document-store way: there is no schema dump to diff, so capture each collection's `listIndexes` output plus the changelog collection before and after the cycle and fail on any difference; run the seed twice (idempotency). + +## Rolling out (platform-neutral) + +This pack ships no `infra.md` — where `migrate` runs is the base `infra/CLAUDE.md` pipeline's call. Two rules survive any pipeline: run `migrate` **before** the rollout that reads the new shape, and keep each migration **backward-compatible** (expand → migrate → contract, base `db/CLAUDE.md`) — during a rolling deploy old code meets new documents and new code meets old ones, so a Mongoose model must tolerate both (additive fields with defaults; contract only after nothing reads the old shape). + +## Conflict register + +- **Base says:** schema changes are reversible migrations under `db/`; application code never alters the schema (`apps/backend/CLAUDE.md` *Coding standards*). **In this stack:** the document shape lives in the repo ring's Mongoose models — an additive shape change is a model edit with no migration; migrations own indexes, collection/validator setup, and rewrites of existing documents. **Because:** a document store has no server-side schema object for a migration to alter; the model is the shape's single home, and a parallel "schema" migration would be an empty ritual that drifts. **Concretely:** DO ship every index and every rewrite of existing documents as a migrate-mongo up/down pair under `db/migrations/`; DON'T let app code build indexes — `autoIndex: false` on the connection, and no `schema.index()` calls in models. +- **Base says:** snake_case table and column names. **In this stack:** collection names stay snake_case plural; document fields are camelCase. **Because:** fields are JS object keys end to end (schema → document → mapper) with no storage-side SQL reader to serve; forcing snake_case adds a rename layer with no consumer. **Concretely:** DO pin each collection's snake_case name via the schema's `collection` option; DON'T introduce snake_case field keys — the repo mapper still translates document ↔ domain shapes. +- **Base says:** run each migration in a transaction where the engine supports it, so a failure rolls back cleanly. **In this stack:** migrations are not transactional — migrate-mongo doesn't wrap them, and index builds (most Mongo migrations) can't run inside a MongoDB transaction. **Because:** the engine's transaction support excludes exactly the DDL-like operations migrations mostly perform. **Concretely:** DO write every migration idempotent and resumable (existence-checked creates, batched rewrites keyed on a filter excluding already-rewritten documents) so a failed run re-runs safely; DON'T assume a failed migration left nothing behind. +- **Base says:** keep schema migrations apart from data backfills; backfills live under `db/backfills/`, never inside a schema migration. **In this stack:** a document shape change *is* a data rewrite — a bounded rewrite rides in the migration itself, written to the base's backfill bar (batched, idempotent, resumable); only large or long-running backfills land under `db/backfills/` and run explicitly. **Because:** with no DDL half to separate from, splitting a small rewrite out leaves an empty migration and an orphaned script. **Concretely:** DO keep any in-migration rewrite batched and idempotent; DON'T put an unbounded full-collection scan in a migration — that goes to `db/backfills/`. +- **Base says:** the shared local DB's schema is global state across worktrees; destructive checks go to a throwaway DB (root `CLAUDE.md`; `db/CLAUDE.md`). **In this stack:** the mongod stays shared by its fixed name, but each worktree uses its own `app_` database on it. **Because:** parallel worktrees applying different branches' migrations to one database break each other; per-worktree databases remove the hazard and make the round-trip safe by construction, with no second container. **Concretely:** after copying `.env` in, re-point the db-name path segment of `MONGODB_URL` at this worktree's database; DON'T run `migrate`/`migrate:down`/reset/seed against the shared default database or another worktree's. diff --git a/stacks/mern/frontend.md b/stacks/mern/frontend.md new file mode 100644 index 0000000..4dd06d5 --- /dev/null +++ b/stacks/mern/frontend.md @@ -0,0 +1,91 @@ +# React SPA (Vite) — frontend appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds `apps/frontend/CLAUDE.md` to **React + TypeScript, built by Vite into static assets**, served by whatever the base `infra/` contract stands up (see *Serving `dist/`*) and consuming the Express backend (`./backend.md`) over REST. Read the base file first; this only adds the bindings and the marked overrides. + +## Rendering model: client-only. No SSR. (load-bearing) + +**This pack is a single-page app. There is no server-side rendering, and adding any is a defect, not an improvement.** The base contract's SPA framing (`apps/frontend/CLAUDE.md`, root `CLAUDE.md`, root `README.md`) applies **verbatim** — skip the root `README.md` "soften the SPA framing" step; it targets the server-first packs. + +- **`vite build` emits `dist/`: one static `index.html` shell plus hashed JS/CSS.** Every route returns that same shell; React renders the page in the browser. **No render-to-HTML step anywhere** — not per request (SSR), not at build (SSG/prerender). HTML is authored once, by hand, in `index.html`. +- **Entry is `createRoot(...).render(...)` in `src/main.tsx`** — never `hydrateRoot`, which exists only to attach to server-rendered markup. +- **Routing is React Router in library mode** — `createBrowserRouter` + `` fed by the central registry (see *Routing*). If the project ever moves to React Router **framework mode**, `react-router.config.ts` **must** set `ssr: false`; framework mode defaults to SSR on. + +**Forbidden — each one is greppable, and any hit is a violation:** + +| Grep for | Why it's a violation | +|---|---| +| `next`, `next/*` in `apps/frontend/package.json` | Next.js belongs to the server-rendered packs (`vercel-ssr`, `enterprise`), not this one | +| `'use client'` / `'use server'` | React Server Component directives — there is no server component tree here | +| `renderToString`, `renderToPipeableStream`, `renderToReadableStream`, `hydrateRoot` | server rendering / hydration | +| `vike`, `vite-plugin-ssr`, any prerender/SSG plugin | build-time HTML generation | +| `*.server.ts(x)`, an `api/` directory under `apps/frontend/` | a server tier on the web project | + +**If a requirement genuinely needs server-rendered HTML — public search indexability above all — that is a pack change, not a patch.** Switch deliberately to a server-rendered pack (or serve the crawlable surface outside this app); do not bolt a render step onto this one. See `add-ons/seo/bindings.md`, where this pack is recorded **unbound** for exactly that reason. + +## Stack binding at a glance + +- **Vite + React + TypeScript**, sources under `src/` in the base's shape (below). `@vitejs/plugin-react`. +- **REST-only data flow.** Every read and mutation goes through `services/` to the backend's `/internal/v1` API. Pack decision — rejected alternative: any client-side direct-DB or BFF layer; the Express backend owns the domain and its aspects. +- **Plain `fetch` through the services layer — no react-query/SWR by default.** Pack decision — rejected alternative: react-query (add it only when client-side cache invalidation genuinely appears; don't pre-install). +- **State: React Context providers under `src/store/`** — one provider per domain. Pack decision — rejected alternative: an external store library (Redux/Zustand); context + props cover this architecture's needs. +- **Config: only `VITE_`-prefixed vars reach the bundle**, read once through one typed `src/lib/env.ts` parsed by Zod at module load — binds the base *Configuration* fail-fast rule. Everything in the bundle is **public**; a secret in a `VITE_` var is the base *No secrets in the bundle* violation. + +## Serving `dist/` — deploy-seam requirements (load-bearing) + +This pack ships no `infra.md`, so the serving layer is whatever the base `infra/` contract stands up — and it **must** provide all four; infra work is measured against them: + +1. **Catch-all → `index.html`.** Without it, a deep link or a refresh on any route but `/` returns a 404 — the canonical SPA-on-a-static-host bug. +2. **`/api/:path*` reverse proxy → the API origin's `/internal/v1/:path*`.** The browser only ever talks to the web origin, so session cookies stay first-party and CORS never enters the picture. +3. **Hardening headers** — the base *Security baseline* set (`Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, a CSP shipped report-only first). A static build has no framework header layer; the serving layer is it. `connect-src` needs only `'self'`, because the API is reached through the same-origin `/api` proxy. +4. **`public/version.json` answered `no-store`** (see *Versioning*). + +- **Locally, the Vite dev server covers 1–2** through `server.proxy` (`/api` → `http://localhost:4000/internal/v1`), so relative `/api/...` paths and first-party cookies behave identically in dev and deployed. Keep the dev and deployed proxy destinations in step; a drift between them is the "works locally, 404s deployed" bug. +- **Browser path:** one fetch wrapper `services/http.ts` calling relative `/api/...` paths. It parses responses with Zod and maps the backend's error envelope to a typed `ApiError { code, status, message, correlationId }` so failures feed the base error state and carry the correlation id. Both the wrapper and every domain module live in `services/` — the base "all network access lives here" rule, bound; a `fetch` inlined in a component is the greppable smell. + +## Folder mapping (base `src/` shape) + +The base shape holds **as written** — `store/`, `services/`, `pages/`, `components/{atoms,molecules,organisms/,templates}`, `i18n/`, `lib/`. Two file bindings only: + +| Base file | Here | +|---|---| +| `routes.` | `src/routes.tsx` — the React Router route objects **and** the link helpers, one module (see *Routing*) | +| `tokens.` | CSS variables declared via Tailwind 4 `@theme` in the global stylesheet (see *Styling*) | + +## Routing + +`src/routes.tsx` is the base's single central registry, bound: the `createBrowserRouter` route array plus a typed link-helper per parameterized route. `` is the default navigation primitive; `useNavigate()` for programmatic cases. Never hand-concatenate a path string — resolve it through the helper. **Code-split at the route and nowhere else**: each entry loads its page through the route object's own `lazy` field, so one chunk maps to one screen. + +## Four data states → React Router + +Binding only — the base owns the why. **Loading** → the shared skeletons, driven by the router's pending navigation state while a route chunk is in flight and by the store's per-domain `status` for in-page fetches. **Error** → a route `errorElement` wired into the shared ``, surfacing `ApiError.correlationId`; one top-level `errorElement` on the root route catches what a leaf doesn't. **Empty** → the shared `` primitive. **Missing resource** → a 404 route element (the SPA answers HTTP 200 with a not-found *screen*; a real 404 status is impossible without a server — see the SEO note above). + +## Styling & primitives + +- **Tailwind CSS 4** (CSS-first config, `@tailwindcss/vite`): design tokens are CSS variables declared in `@theme` in the global stylesheet — that declaration **is** the base's single token source; components consume semantic tokens through Tailwind utilities. No `tailwind.config`. +- **Foundation: Radix UI primitives**, wrapped as **atoms** in `components/atoms/`. Variants via `class-variance-authority`; class composition via `clsx` + `tailwind-merge` (one `cn()` helper in `lib/`). Icons: `lucide-react`. Fonts **self-hosted** (`@fontsource-variable/*`) with `font-display: swap`. +- Don't import a prebuilt styled component kit on top — compose Radix + tokens in `atoms/`/`molecules/`. Swapping the headless library is allowed only by recording the choice in `apps/frontend/CLAUDE.md`; don't mix two. + +## Responsive idioms (Tailwind 4) + +The base *Responsive layout* rules own the why; the Tailwind bindings: mobile-first stepped utilities (`sm:`/`md:`/`lg:`); prefer intrinsic sizing (`clamp()`, `grid-cols-[repeat(auto-fit,minmax(…,1fr))]`, `flex-wrap`) before adding a breakpoint; a component that adapts to its container uses `@container` + `@sm:` variants, keeping viewport breakpoints for page-level layout. Header clearance and screen gutter are semantic `@theme` tokens (`--header-clearance`, `--gutter-screen`); one ``/`
` atom owns the gutter idiom — hand-composing `container mx-auto px-*` per page is the greppable smell. Never `h-screen`/`100vh` — content-driven `min-h-[Xrem]`, and `min-h-[100svh]` where something must truly fill the viewport. Wide tables/code scroll in their own `overflow-x-auto` box (`truncate max-w-*` needs a `min-w-0` parent); `text-balance` is the heading atom's default. + +## Versioning / build identity + +**The base *Versioning / build identity* rule applies in full** — this is a cached static bundle, exactly the case it was written for. + +- `vite.config` injects `VITE_APP_VERSION` from `npm_package_version`; render the unobtrusive `v` tag from `import.meta.env.VITE_APP_VERSION`. +- Emit a build-stamped `public/version.json` (served `no-store` — deploy-seam requirement 4) and poll it on launch/foreground; a mismatch shows the dismissible "Refresh to update" banner. Never force the reload. +- **One exception to "never force a reload":** a failed lazy-route `import()` after a redeploy (the old hashed chunk is gone) is unrecoverable — catch it in the route `errorElement` and offer an explicit reload as the retry action. + +## Testing — typecheck + build + Playwright e2e + +- The frontend suite is `tsc --noEmit`, `vite build`, and **Playwright** specs under `apps/frontend/e2e/` exercising the real app — every screen's four states and primary flows. `E2E_BASE_URL` selects the target: the local dev stack or a deployed environment. +- **Run the specs at a narrow viewport as well as desktop** (base *Testing* rule): a second Playwright project on a mobile device — `{ name: 'mobile', use: { ...devices['Pixel 7'] } }` — beside the desktop one. +- **One spec asserts the SPA fallback**: request a deep route path directly (not by in-app navigation) and assert the screen renders. Run it against a *served* target — the Vite dev server always falls back, so only a deployed/preview run proves deploy-seam requirement 1. +- See the conflict register for what this replaces. The moment a store slice or service accrues branching logic worth isolating, add a unit runner for that code — don't scaffold one speculatively. + +## Conflict register + +- **Base says:** test store slices and `lib/` helpers as plain units, and services with the network mocked at the edge. **In this stack:** the default frontend suite is typecheck + build + Playwright e2e covering the four-state contract; per-unit suites are added on demand, not scaffolded. **Because:** this architecture keeps business logic behind the REST API, so frontend units would mostly re-test glue — e2e against the real contract catches what matters, and it is the only kind that exercises the proxy and fallback this pack's deploy seam depends on. **Concretely:** DO cover every new screen's states in an e2e spec in the same change; DON'T add a unit runner until a slice/service holds real branching logic — then test that unit per the base rules. From 4f72b537463dc10acc0a3516a2a7b45fa268b7c1 Mon Sep 17 00:00:00 2001 From: DeyangChan Date: Wed, 29 Jul 2026 14:15:12 +0800 Subject: [PATCH 04/21] =?UTF-8?q?feat(stacks):=20add=20django=20pack=20?= =?UTF-8?q?=E2=80=94=20React=20SPA=20+=20Django=205/DRF=20+=20Postgres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform-neutral identity-named pack (triple react-django-postgres): Vite/React SPA over a Django 5 + DRF API managed with uv, Postgres via Django's per-app migrations (registered against the base db/ contract). Binds test-mode and otp-auth in backend.md; saas-billing bound and seo recorded unbound (CSR SPA, S1 unmeetable) in the add-ons' bindings files. Co-Authored-By: Claude Fable 5 --- add-ons/saas-billing/bindings.md | 4 ++ add-ons/seo/bindings.md | 8 ++++ stacks/django/README.md | 53 ++++++++++++++++++++++++ stacks/django/backend.md | 71 ++++++++++++++++++++++++++++++++ stacks/django/db.md | 57 +++++++++++++++++++++++++ stacks/django/frontend.md | 44 ++++++++++++++++++++ 6 files changed, 237 insertions(+) create mode 100644 stacks/django/README.md create mode 100644 stacks/django/backend.md create mode 100644 stacks/django/db.md create mode 100644 stacks/django/frontend.md diff --git a/add-ons/saas-billing/bindings.md b/add-ons/saas-billing/bindings.md index e5e036a..ec7291a 100644 --- a/add-ons/saas-billing/bindings.md +++ b/add-ons/saas-billing/bindings.md @@ -21,3 +21,7 @@ Pre-written entries for the stack packs this template ships, kept **inside the a ## `mern` — bound - **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` SDK). The gateway is a repo-ring adapter behind the domain's billing gateway port, wired in `container.js`; `BILLING_ENABLED=false` binds the stub gateway there instead (tests swap it via `overrides`). Webhook seam: signature verification needs the **raw body**, and the app-wide `express.json()` consumes it — mount `express.raw({ type: 'application/json' })` on the webhook route only and verify the Stripe signature against the raw buffer before anything parses. Jobs: the API is a long-lived process, so the reconciliation sweep, usage rollover, and trial expiry run on an in-process scheduler (e.g. `node-cron`) started from the composition root — keep each run idempotent; if the deployment scales past one instance, move the schedule to the pipeline the base `infra/CLAUDE.md` contract stands up (this pack ships no `infra.md`) rather than letting instances double-fire. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` join the boot-time Zod env schema. + +## `django` — bound + +- **saas-billing** (`add-ons/saas-billing/`): provider = **Stripe** (`stripe` Python SDK). The gateway is an adapter module (`billing/gateway.py`) behind one service-facing interface — services call it, never the SDK (that pack's gateway-adapter seam, `stacks/django/backend.md`); `BILLING_ENABLED=False` in the validated settings binds the stub gateway instead, selected once in the gateway factory — tests swap it at that same seam. Webhook seam: a dedicated view with authentication/permission/throttle classes cleared and **CSRF exempt** reads `request.body` — Django keeps the raw bytes, so the raw payload is available by construction — and verifies the signature via `stripe.Webhook.construct_event` **before** any serializer parses. Jobs: the reconciliation sweep, usage-period rollover, and trial-expiry handling are idempotent management commands (`manage.py billing_reconcile`, …) scheduled by whatever the base `infra/` contract stands up (cron/scheduler) — no resident queue by default; adopt Celery only when a job outgrows a command, as a recorded decision. `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` join the validated `settings.py` seam (django-environ, required in production) and `.env.example`. diff --git a/add-ons/seo/bindings.md b/add-ons/seo/bindings.md index 777c232..9e01e8b 100644 --- a/add-ons/seo/bindings.md +++ b/add-ons/seo/bindings.md @@ -51,3 +51,11 @@ Unmeetable seam item: **S1** — that pack is a **client-rendered SPA with no SS Workable alternative: this pack has no same-stack SSR sibling — adopt a server-rendered pack instead (**`vercel-ssr`** or **`enterprise`**, both bound above), or keep the SPA and serve the crawlable surface **outside** the app bundle (its own server-rendered origin — a marketing site, a docs site) and bind the add-on there. Residual posture: **R10 survives unbinding** — the SPA origin is publicly reachable, so it still serves a refuse-indexing response. Bind that much: a static `public/robots.txt` disallowing all, plus `X-Robots-Tag: noindex` from the serving layer's headers on every non-production environment (the pack's deploy seam — `stacks/mern/frontend.md` → *Serving `dist/`*, requirement 3's header home), and a Playwright assertion on the non-production response (gate G3). No platform answers noindex for this pack by default — the serving layer must, and the suite assertion, not platform behaviour, is the gate. + +## django — unbound + +Unmeetable seam item: **S1** — that pack's frontend is a **client-rendered SPA with no SSR by deliberate design** (`stacks/django/frontend.md` → *Rendering model*). Every route is served as the same static `index.html` and painted by JavaScript — Django serves only the JSON API — so there is no mechanism that makes an indexable route complete without client-side scripts, and nothing to hang S2–S9 off either: no server render to emit per-route `` tags, no server response on app routes to carry a 301 or a real 404 status. Adding one is a pack change, not a patch — do not reach for a prerender/SSG plugin to close this gap; that is precisely what the pack's forbidden list rules out. + +Workable alternative: adopt a server-rendered pack (**`vercel-ssr`** or **`enterprise`**, both bound above) — or serve the crawlable surface **outside** the SPA bundle. On this stack that second path is unusually cheap: Django itself can host server-rendered template pages (a marketing/docs surface) on their own public URLs beside the API, and the add-on binds to *those* pages — but that is a separate, deliberate adoption with its own seam answers, not a default this pack supplies. + +Residual posture: **R10 survives unbinding** — the SPA origin is publicly reachable, so it still serves a refuse-indexing response. Bind that much: a `robots.txt` disallowing all, plus `X-Robots-Tag: noindex` on every non-production response, emitted from the serving layer (Django middleware where Django serves the files, otherwise the reverse proxy the infra contract stands up), keyed on validated environment config — never request inference — with a test asserting the non-production response (gate G3). diff --git a/stacks/django/README.md b/stacks/django/README.md new file mode 100644 index 0000000..bbe2568 --- /dev/null +++ b/stacks/django/README.md @@ -0,0 +1,53 @@ +# Stack pack: django + +Frontend **React SPA** (Vite, TypeScript) — **client-rendered, no SSR** · Backend **Django 5 + Django REST Framework** (Python 3.12, managed with **uv**) · DB **Postgres 16** via the **Django ORM** (Django migrations). Platform-neutral: the product deploys through whatever the base `infra/` contract stands up. This is the **manifest** — it wires the pack onto a project; bindings and conflict registers live in the appendices. For what a pack is and the invariants every appendix follows, see `../README.md`. + +> **Naming.** Named for its backend framework — the dominant Python full-stack combo: a Django + DRF API behind a React SPA; the underlying triple is `react-django-postgres` (per `../README.md`). Like `enterprise`, this pack is platform-neutral — it ships no `infra.md`. Unlike it, the frontend is a client-rendered SPA with a separate API — the same frontend shape as `vercel-csr`, minus the platform. + +> **Rendering model.** The frontend is a **single-page app**: one static `index.html`, rendered entirely in the browser. **There is no server-side rendering, and none may be added.** The enforceable rules and the greppable forbidden list are in `frontend.md` → *Rendering model*. A requirement that genuinely needs server-rendered HTML (public search indexability above all) is a **pack change** — adopt a server-rendered pack, or serve the crawlable surface outside this app (see *Add-ons* on `seo`). + +## Appendix → base mapping + +| Appendix | Binds onto | Scope | +|---|---|---| +| `frontend.md` | `apps/frontend/CLAUDE.md` | client-only rendering (no SSR), Vite + React Router, same-origin `/api` proxy + SPA fallback, Tailwind 4 + Radix, Zod at the edge | +| `backend.md` | `apps/backend/CLAUDE.md` | Django apps as feature modules, `services.py`/`selectors.py` discipline, DRF edge, the `settings.py` config seam, middleware aspects, uv/ruff/mypy/pytest | +| `db.md` | `db/CLAUDE.md` + data access | Django migrations (per-app home, `sqlmigrate` review, drift gate), ORM schema conventions, the fixed-name local Postgres | + +Each appendix opens with the verbatim precedence line and ends with its conflict register (see `../README.md`). Conflicts live in the appendices, not here. + +## Day-1 wiring + +Run as part of the root `README.md` `## Day-1 checklist`: delete every other `stacks/*` directory so this pack is the only one left — each area's `CLAUDE.md` then points agents at the matching appendix here (mechanism: `../README.md` *Activation*). Then copy the **dev** block below over the root `CLAUDE.md` "Common commands" placeholder (delete the banner) and apply the **CI** notes to `.github/workflows/ci.yml` — never the same block in both. Finally record in root `CLAUDE.md` **Learnings**: `Stack: django; appendices under stacks/django/`. + +> **Skip the root `README.md`'s "soften the SPA framing" step.** That instruction targets the server-first packs. This pack **is** a SPA, so the base framing in root `CLAUDE.md`, `apps/frontend/CLAUDE.md`, and the **What's included** "Frontend SPA" row is already correct — leave every one of them as shipped. + +## Suggested toolchain (uv + pnpm, fanned out by a root Makefile) + +Two ecosystems, one verb set: **uv** manages `apps/backend` (`pyproject.toml` + committed `uv.lock`), **pnpm** manages `apps/frontend`; each root command verb exists **once**, in a root **Makefile**, and fans out to both apps. Pack decision — rejected alternative: a root `package.json` whose scripts shell into uv (it makes Node a hard dependency of backend-only work and hides the Python half behind npm scripts; `make` is language-neutral and already on every dev/CI image). + +**Dev block → root `CLAUDE.md` "Common commands":** + +```bash +make bootstrap # uv sync + pnpm install + start local Postgres (fixed-name docker container, shared across worktrees) + migrate +make dev # both dev servers: Django runserver (:8000) + Vite dev (:5173, proxying /api → :8000) +make lint # ruff check + ruff format --check (backend); ESLint (frontend) +make typecheck # mypy with django-stubs (backend); tsc --noEmit (frontend) +make test # pytest (backend); vitest run (frontend; Playwright e2e separate: make test-e2e) +make build # vite build → apps/frontend/dist (Django has no build step — explicit no-op; collectstatic is a deploy concern) +make migrate # python manage.py migrate (rollback: manage.py migrate ) +``` + +**CI block → `.github/workflows/ci.yml` (non-interactive):** a `postgres:16` service container; `uv sync --frozen` + `pnpm install --frozen-lockfile`; lint (ruff + ESLint); typecheck (mypy + `tsc --noEmit`); `pytest`; `vitest run`; `vite build`; the frontend **i18n key-parity** check (the base gate stands — this pack changes nothing about it); plus the db appendix's **§CI checks** — migrations apply from zero on the scratch DB, the `makemigrations --check --dry-run` drift gate (**replacing** the base `ci.yml` round-trip TODO, per `db.md`'s register), and the seed run twice (idempotency). + +**Validation:** **DRF serializers** at the backend edge (request DTOs + response shaping) and **Zod** on the frontend (env at boot, response parsing). The shared contract is the OpenAPI schema DRF emits via **drf-spectacular** — generate frontend types from it rather than hand-copying shapes (details in `backend.md` / `frontend.md`). + +**Pack decisions recorded here** (referenced by the appendices): **DRF** (rejected: Django Ninja, a FastAPI sidecar); **uv** (rejected: Poetry, pip-tools); **mypy + django-stubs** — the `typecheck` verb is real for both apps (rejected: pyright, whose Django support lacks the mypy plugin; rejected: skipping backend typecheck); **`services.py`/`selectors.py`** as the use-case convention (rejected: a hexagonal ports-and-adapters layer over the ORM — see `backend.md`'s register); the **Makefile fan-out** (rejected above); **Django session auth** — cookie-based, CSRF on (rejected: JWT held by the SPA). + +## Add-ons + +Bindings for the shipped add-ons: **test-mode** and **otp-auth** in `backend.md`. **saas-billing** carries its own bindings file inside the add-on (`add-ons/saas-billing/bindings.md`), with a bound section for this pack. **seo** carries one too (`add-ons/seo/bindings.md`), where this pack is recorded **unbound**: its S1 seam asks for a rendering mechanism that serves indexable routes complete without client-side scripts, and a client-only SPA has none — adopting `seo` means adopting a server-rendered pack instead, or serving the crawlable surface outside this app. **llm-calls**, **premium-design**, **enterprise-compliance**, and **multi-tenancy** are left unbound by this pack — adopting one means supplying its *Binds to a stack* answers in the matching appendix as part of adoption. + +## Deploy seam + +Platform-neutral, like `enterprise`: the SPA's `dist/`, the Django app (ASGI/WSGI behind its process server), and `manage.py migrate` deploy through the pipeline the base `infra/CLAUDE.md` contract stands up — this pack ships no `infra.md` (infra is cloud-shaped, not app-stack-shaped). Two seams the pipeline must honour: serve `dist/` and the API from **one origin** with the SPA fallback (`frontend.md`); run `manage.py migrate` before the code that reads the new schema goes live, keeping each migration backward-compatible (expand → migrate → contract, `db.md`). `deploy.yml` stays the base stub until the infra work fills it in. diff --git a/stacks/django/backend.md b/stacks/django/backend.md new file mode 100644 index 0000000..678920a --- /dev/null +++ b/stacks/django/backend.md @@ -0,0 +1,71 @@ +# Django 5 + DRF — backend appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds `apps/backend/CLAUDE.md` (the onion) and the root `CLAUDE.md` to **Django 5 + Django REST Framework, Python 3.12, managed with uv**. Read those first; this file does not restate them. Data layer → `./db.md`. + +## Stack binding at a glance + +- **API layer: Django REST Framework** on Django 5 — views, routers, serializers, permissions, throttles. Pack decision — rejected alternatives: Django Ninja, a FastAPI sidecar (DRF is the ecosystem default with the deepest auth/permission/throttle integration). +- **Language: Python 3.12, dependencies via uv** (`uv sync`, `uv run`, committed `uv.lock`). Pack decision — rejected: Poetry, pip-tools. +- **Lint: ruff (check + format). Typecheck: mypy with django-stubs + djangorestframework-stubs** — the root `typecheck` verb is real here, not a no-op. +- **Validation: DRF serializers at the API edge** — a module's serializers are its `dtos/` in Django form: request parsing/validation in, response shaping out. Business rules stay in services/models, never in a serializer's `validate_*`. + +## Layout — Django apps are the feature modules + +The base `modules//{domain,service,repo,controller,dtos}` shape is **replaced** by the idiomatic layout real Django codebases use (see conflict register): + +``` +apps/backend/ +├─ manage.py +├─ pyproject.toml / uv.lock +├─ config/ # the Django project: settings.py, urls.py, asgi.py +└─ / # one Django app per domain area (e.g. users, billing) + ├─ models.py # ORM models + model-level invariants (constraints, clean()) + ├─ services.py # write use cases — one function per use case; owns transaction.atomic + ├─ selectors.py # read paths — queries composed for the API, no side effects + ├─ api/ # serializers.py, views.py, urls.py — the delivery edge + ├─ migrations/ # this app's Django migrations (→ ./db.md) + └─ tests/ +``` + +- The dependency direction still points one way: `api/ → services.py / selectors.py → models.py`. A view never calls the ORM directly and never holds business logic — it validates with a serializer, invokes **one** service/selector, and shapes the response. +- Cross-app use goes through the other app's `services.py`/`selectors.py`, never its models or managers directly. +- **External gateways** (mail, SMS, payments) are adapter modules (`/gateway.py`, or a shared `gateways/` package) selected by the base default-off validated flags; services call the adapter, never an SDK. Tests swap the adapter at that seam. + +## Config — `settings.py` is the single seam + +- All env reads live in `config/settings.py` via **django-environ**, validated at import: required keys declared with types and no production defaults, so a missing or malformed value fails at boot with a named error — the base *Configuration* rule, bound. Pack decision — rejected: pydantic-settings (a second config object beside `settings` buys nothing). +- App code reads `django.conf.settings` (see conflict register); `os.environ` appears **only** in `settings.py`. `.env.example` stays canonical. + +## Cross-cutting — middleware + DRF hooks are the aspects + +| Base concern | Binding | +|---|---| +| Errors | one custom DRF `EXCEPTION_HANDLER` mapping domain exceptions → the base envelope (`error.code`/`message`/`correlationId`); `IntegrityError` unique violations → `409` | +| Request context | one middleware seeds the correlation id per request, sets the `x-correlation-id` response header, and binds it into logging | +| Auth | Django **session auth** (signed HTTP-only cookies — the base default, unchanged) via DRF `SessionAuthentication` + permission classes; CSRF stays on (the SPA sends `X-CSRFToken` — `./frontend.md`) | +| Transactions | `transaction.atomic` wrapping each write use case in `services.py` — one per use case; **not** `ATOMIC_REQUESTS`, which moves the boundary to the view (wrong ring) | +| Logging | structured JSON logging (dictConfig or structlog) carrying the correlation id | +| Security headers | `SecurityMiddleware` settings (HSTS, nosniff, referrer policy, frame deny); CSP only where Django itself serves HTML | +| Rate limiting | DRF throttle classes on the views that need them, backed by Django's cache framework — **database cache backend** by default, no Redis until a limit must be globally exact and fast | + +- **Routing:** each app's `api/urls.py` is included under one `internal/v1/` prefix in `config/urls.py`, plus one unauthenticated `/health` — the base visibility/versioning rule as URLconf structure. The browser reaches it as `/api/...` through the frontend proxy (`./frontend.md`). +- **SSRF guard + write-only secrets** bind the base *Security baseline*: one shared validator (resolve the host; reject loopback/private/link-local/metadata IPs) called at config-save *and* immediately before the outbound request; secret-bearing serializers mask on read (value → `…Set` flag) and preserve the stored value when the update field is blank. +- **Contract artifact:** **drf-spectacular** generates the OpenAPI schema from the serializers/views — the shared contract the frontend generates types from; never hand-copy shapes. + +## Testing + +- **Runner: pytest + pytest-django** (`uv run pytest`). Pack decision — rejected: Django's unittest runner (pytest fixtures/parametrize are the ecosystem default). +- Base per-ring kinds, bound: **model invariants + pure helpers** — plain units, no DB where possible; **services/selectors** — `pytest.mark.django_db` tests against real Postgres (the ORM *is* the persistence layer — faking it tests nothing); **API** — DRF `APIClient` contract tests asserting status codes, envelope shape, and auth guards. Gateways are faked at the adapter seam, never by patching ORM or framework internals. + +## Add-on bindings (if adopted) + +- **test-mode** (`add-ons/test-mode/`): one middleware resolves the mode signal from an inbound header onto the request — fail closed: missing or unknown means production. In test mode the flag-gated gateways (the base default-off booleans) route to their sinks — Django's `console.EmailBackend` is the canonical email sink; other gateways ship a structured-log/no-op adapter. The test-user picker is an unauthenticated DRF view gated on the same signal; it returns `[]` in production, and an `APIClient` test asserts that. +- **otp-auth** (`add-ons/otp-auth/`): model A (self-managed) — an `OtpChallenge` model (hashed code, short TTL, `purpose` field) in its own Django app with its own migrations; hashing in a shared util with `django.utils.crypto.constant_time_compare` for the timing-safe verify; delivery through the gateway adapters behind the default-off flags; phone numbers canonicalised to E.164 with the **`phonenumbers`** library; a unique constraint on (target, purpose) resolves the double-submit race — `IntegrityError` → `409` through the shared exception handler. Send/verify rate limits are DRF throttles on those views (database cache backend — no separate store on this pack); the per-challenge attempt cap lives on the challenge row. In test mode delivery sinks to the structured log — the tester reads the real code there, and verify is never stubbed. + +## Conflict register + +- **Base says:** each feature module holds four rings — `domain/`, `service/`, `repo/`, `controller/`, `dtos/` — with a pure, framework-free domain at the centre. **In this stack:** a feature is a **Django app** — `models.py` / `services.py` / `selectors.py` / `api/` — and the centre is not framework-free: invariants live on ORM models (constraints, `clean()`, model methods) and in service functions. **Because:** Django's app registry, ORM, admin, and migrations all key off the app layout; a parallel pure-domain layer over active-record models duplicates every entity and drifts — the services/selectors convention is how real Django shops keep the discipline without a fake hexagon. **Concretely:** DO put every write use case in `services.py` and every read composition in `selectors.py`; DON'T create `domain/`/`repo/` folders or entity classes that mirror models. +- **Base says:** inner rings define ports, the repo ring implements them, and the composition root (`container.js`) wires implementations at boot. **In this stack:** there is no repo ring and no container — the **ORM manager/QuerySet is the persistence API**, called directly from `services.py`/`selectors.py`; only external gateways keep a swap seam (an adapter module selected by validated flags, faked at that seam in tests). **Because:** the Django ORM is active record; wrapping managers in hand-rolled repositories plus a DI container re-implements what the framework already centralises, at a cost the base's YAGNI rule forbids. **Concretely:** DON'T add a `container.py` or `*Repository` classes that proxy managers; DO route every external side effect through a gateway module so tests swap one factory, not framework internals. +- **Base says:** no inner layer reads config directly — it is passed inward as values, read from the environment in one place (root *Configuration*; backend *Security baseline*). **In this stack:** the one place is `config/settings.py` (django-environ, validated at import), and code anywhere may read `django.conf.settings` — the framework-blessed global. **Because:** Django and every third-party app read `settings` by construction; threading values as parameters would fork a second config path alongside an unavoidable one. **Concretely:** DO read config only via `django.conf.settings`; an `os.environ` read anywhere outside `settings.py` is the greppable violation. diff --git a/stacks/django/db.md b/stacks/django/db.md new file mode 100644 index 0000000..4b597fb --- /dev/null +++ b/stacks/django/db.md @@ -0,0 +1,57 @@ +# Postgres 16 + Django migrations — db appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds the base `db/CLAUDE.md` (+ the backend's data access) to **Postgres 16 via the Django ORM**, with **Django migrations** as the migration tool. Read the base files first. App layout and the services/selectors discipline live in `./backend.md`; this file owns migrations, seed, schema conventions, and query/transaction mechanics. + +## Migrations — Django-owned, per-app + +- Generated by `manage.py makemigrations`, applied by the root `migrate` verb (`manage.py migrate`; rollback: `manage.py migrate `). They live **per app under `apps/backend//migrations/`, not under `db/migrations/`** — see conflict register. +- **Name every migration:** `makemigrations --name ` — never commit an auto-named `0007_auto_…`. One logical schema change per migration. +- **Ordering is Django's per-app numeric prefix + explicit `dependencies` graph**, not a timestamp — see conflict register for the parallel-branch rule. +- **Reversibility, bound:** Django's schema operations auto-reverse; `migrate ` is the down path. Every `RunPython` ships a real `reverse_code` (or `RunPython.noop` where the forward op is purely additive). A genuinely irreversible migration leaves the reverse raising `IrreversibleError` **and** carries the base's justification comment at the top of the file — never silently. +- **Review as SQL before committing:** `manage.py sqlmigrate ` — confirm no unintended `DROP`, no unguarded `NOT NULL` on a populated table, no lock-heavy DDL on a hot table. A generated migration is a reviewed artifact, not generated-and-forgotten. +- **Transactional by default** (`atomic = True` — base rule satisfied). Set `atomic = False` only for DDL that demands it (`AddIndexConcurrently` from `django.contrib.postgres`), with a comment saying so. +- Never edit an applied migration (base rule); fix forward. `squashmigrations` only as a deliberate, separate change. +- **Expand → migrate → contract** for destructive changes (base rule stands). Backfills are **not** `RunPython` inside schema migrations — they are idempotent, batched management commands invoked explicitly (below). + +## Schema conventions (bound) + +- **`USE_TZ = True`** — timestamps stored UTC as `timestamptz`; every model carries `created_at = DateTimeField(auto_now_add=True)` and `updated_at = DateTimeField(auto_now=True)`. +- Table names: Django's `_` default is already snake_case — keep it; set `db_table` only when binding to a pre-existing schema. +- **uuid primary keys:** `UUIDField(primary_key=True, default=uuid4)` — opaque, non-enumerable. Pack decision — rejected: the default `BigAutoField` (enumerable in URLs). +- **Django auto-indexes every `ForeignKey`** (unlike raw Postgres) — the base FK-index rule holds by construction; frequent filter/sort columns still need explicit `Meta.indexes`. +- Money and quantities: `DecimalField(max_digits, decimal_places)` or integer minor units — never `FloatField`. +- **Fixed value sets: `TextChoices` + a `CheckConstraint`** in `Meta.constraints` — enforced in the database, widened by a plain reversible migration. Pack decision — rejected: native Postgres enums (`ALTER TYPE … ADD VALUE` is non-transactional and effectively one-way). +- Unique business invariants: `UniqueConstraint` in `Meta.constraints` (conditional where soft delete applies); the violation maps to the domain conflict → `409` via the shared exception handler (`./backend.md`). + +## Query & transaction mechanics + +- `transaction.atomic` wraps each write use case in `services.py` — one per use case (bound in `./backend.md`); `select_for_update()` inside `atomic` for read-then-write races. +- **The ORM parameterises by construction** — the base "never interpolate request data" rule holds; `raw()` / `cursor.execute` take a params argument, never an f-string. An f-string building SQL is the greppable violation. +- **N+1:** load relations with `select_related`/`prefetch_related` in the selector, never a per-row loop; `only()`/`values()` where a subset suffices. +- **Pagination:** one custom DRF pagination class app-wide, bound to the base envelope (`page`/`recordsPerPage`/`totalRecords`, `sortBy` validated against an allow-list of sortable columns) — never fetch-all-then-slice. + +## Local dev & seed + +- **Local Postgres is one fixed-name Docker container** (`postgres:16`), started by `make bootstrap` with start-or-run semantics, shared across worktrees per the root `CLAUDE.md` — reuse it, never start a second copy. +- **One shared container, one database per worktree.** Derive the name deterministically (sanitize the branch to `[a-z0-9_]`, truncate to Postgres's 63-byte identifier limit, prefix `app_` — `feature/x` → `app_feature_x`), re-point the db-name segment of `DATABASE_URL` after copying `.env` in, and have `bootstrap` create the database if missing. Drop it on worktree teardown. See conflict register. +- **Seed:** an idempotent management command (`manage.py seed_dev`, upsert via `update_or_create` by business key) with realistic, named accounts (they also back the test-mode picker, base `db/CLAUDE.md`); non-production only — the base rule stands. Backfills: idempotent, batched, resumable management commands under the owning app, invoked explicitly. +- **pytest-django creates its own `test_*` database** — the suite never touches the dev schema; `--reuse-db` is a local speed-up only. + +## CI checks (drop into `.github/workflows/ci.yml`) + +Against a scratch `postgres:16` service container: + +- **Apply from zero:** `manage.py migrate` on a clean database. +- **Drift gate:** `manage.py makemigrations --check --dry-run` — fails when a model change has no migration (also catches an edited model with a forgotten migration after rebase). +- **Seed idempotency:** run `seed_dev` twice on the freshly-migrated database — both must succeed. + +These replace the base `ci.yml` round-trip TODO — see conflict register; the reverse proof is a pre-merge step on your worktree database, not a CI job. + +## Conflict register + +- **Base says:** `db/` is the shared home — migrations under `db/migrations/`, backfills under `db/backfills/`, seed/reset scripts beside them (`db/CLAUDE.md`; backend *Coding standards*). **In this stack:** migrations live per app under `apps/backend//migrations/`, and seed/backfills are management commands inside the backend — all still invoked through the root verbs (`make migrate`, the seed script). **Because:** Django's migration loader, dependency resolver, and `makemigrations` operate only on per-app `migrations/` packages; relocating them fights the framework and breaks autodetection, with no auditable gain. **Concretely:** DON'T create `db/migrations/` or hand-write DDL outside a generated migration; DO run every schema change through `makemigrations` in the owning app and apply it via the root `migrate` verb. +- **Base says:** migration files carry a timestamp prefix, never a hand-incremented sequence — parallel branches must never both claim the same number (`db/CLAUDE.md`). **In this stack:** Django names migrations with a per-app incrementing number plus an explicit `dependencies` graph — sequences by design, with collisions *detected* (`InconsistentMigrationHistory` / conflicting leaf nodes) rather than prevented. **Because:** the numbering is machine-managed and the dependency graph, not the filename, is the real order; Django refuses to run a forked history, so a collision is loud, not silent. **Concretely:** after rebasing, if trunk gained a migration in the same app, delete and regenerate yours so it renumbers after trunk's leaf — DON'T commit a `makemigrations --merge` migration to keep history linear. +- **Base says:** prove the down path — run up → down → up on a throwaway scratch DB before merging, and the `ci.yml` round-trip TODO wires that gate (`db/CLAUDE.md`; root Day-1). **In this stack:** a whole-project `migrate zero` round-trip is impractical (cross-app dependencies, prior irreversible migrations unrelated to the change), so the standing CI gates are apply-from-zero + the `makemigrations --check --dry-run` drift gate + seed-twice; the reverse proof is per-change: before merge, run `migrate ` then `migrate` for each migration you added, on your worktree database, and state the evidence observed. **Concretely:** when pasting the pack CI block, replace the `ci.yml` "Migration round-trip" TODO with §CI checks above; DON'T merge a migration whose own reverse you haven't run (or that lacks the irreversible justification). +- **Base says:** the shared local DB's schema is global state across worktrees — reuse the fixed-name container; destructive checks go to a throwaway DB (root `CLAUDE.md`; `db/CLAUDE.md`). **In this stack:** the *container* stays shared by its fixed name, but each worktree migrates its own `app_` database, created on bootstrap and dropped on teardown. **Because:** parallel worktrees applying different branches' migrations to one schema corrupt each other's `django_migrations` history; per-worktree databases remove the hazard and make the per-change reverse proof safe by construction, with no second container. **Concretely:** after copying `.env` in, re-point the db-name segment of `DATABASE_URL` at this worktree's database; DON'T run `migrate`/`migrate `/seed against the shared default database or another worktree's. diff --git a/stacks/django/frontend.md b/stacks/django/frontend.md new file mode 100644 index 0000000..68d7080 --- /dev/null +++ b/stacks/django/frontend.md @@ -0,0 +1,44 @@ +# React SPA (Vite) over a Django API — frontend appendix + +> Rides on top of the base contract; this file only adds stack bindings and resolves conflicts. Where this appendix and a base file disagree, the conflict register below wins — for this stack only. + +Binds `apps/frontend/CLAUDE.md` to **React + TypeScript, built by Vite into static assets**, consuming the Django/DRF backend (`./backend.md`) over REST. Read the base file first; this only adds the bindings. + +## Rendering model: client-only. No SSR. (load-bearing) + +**This pack is a single-page app. There is no server-side rendering, and adding any is a defect, not an improvement.** The base contract's SPA framing applies **verbatim** — do not soften it on Day 1; that instruction in the root `README.md` is for the server-first packs only. + +- **`vite build` emits `dist/`: one `index.html` shell plus hashed JS/CSS.** Every route serves that same shell; React renders the page in the browser. No render-to-HTML step anywhere — not per request (SSR), not at build (SSG/prerender). HTML is authored once, by hand, in `index.html`. +- **Entry is `createRoot(...).render(...)` in `src/main.tsx`** — never `hydrateRoot`, which exists only to attach to server-rendered markup. +- **Routing is React Router in library mode** — `createBrowserRouter` + `` fed by `src/routes.tsx`, the base's single central registry: the route array plus a typed link-helper per parameterized route. Code-split at the route and nowhere else, via each route object's `lazy`. If the project ever moves to framework mode, `react-router.config.ts` **must** set `ssr: false` — framework mode defaults to SSR on. +- **Forbidden — each greppable, any hit a violation:** `next` in `apps/frontend/package.json`; `'use client'` / `'use server'`; `renderToString` / `renderToPipeableStream` / `renderToReadableStream` / `hydrateRoot`; `vike` or any prerender/SSG plugin; `*.server.ts(x)` files. +- A requirement that genuinely needs server-rendered HTML — public search indexability above all — is a **pack change**, not a patch. See `add-ons/seo/bindings.md`, where this pack is recorded **unbound** for exactly that reason. + +## Data flow — one origin, `/api` + +- **REST-only, through `services/`,** to the backend's `/internal/v1` API. One fetch wrapper (`services/http.ts`) calls relative `/api/...` paths, parses responses with Zod, and maps the backend's error envelope to a typed `ApiError { code, status, message, correlationId }` feeding the base error state. +- **CSRF:** session-cookie auth keeps Django's CSRF protection on (`./backend.md`) — the wrapper reads the `csrftoken` cookie and sends `X-CSRFToken` on every mutation. This lives in the one wrapper, never per call site. +- **Dev:** Vite `server.proxy` sends `/api` → `http://localhost:8000/internal/v1`, so relative paths and first-party cookies behave identically in dev and deployed. +- **Deployed:** the same shape — `dist/` and the API served from **one origin** by whatever the infra contract stands up (a reverse proxy fronting Django and the static files), with two routing rules: `/api` → the Django `internal/v1` prefix, and a catch-all serving `index.html` (the SPA fallback — without it a deep link or refresh 404s). Keep the dev proxy and the deployed rewrite pointing at the same destination; drift between them is the "works locally, 404s deployed" bug. +- **Plain `fetch`, no react-query/SWR by default.** Pack decision — rejected: react-query (add only when client-side cache invalidation genuinely appears). **State: React Context providers under `src/store/`.** Pack decision — rejected: Redux/Zustand. +- **Contract types are generated**, not hand-copied: the backend's drf-spectacular OpenAPI schema → `openapi-typescript` — the base "prefer a generated contract artifact" rule, bound. + +## Config, styling, headers + +- **Only `VITE_`-prefixed vars reach the bundle**, read once through a Zod-parsed `src/lib/env.ts` at module load — the base fail-fast rule. Everything in the bundle is public; a secret in a `VITE_` var is the base *No secrets in the bundle* violation. +- **Tailwind CSS 4** (CSS-first: design tokens as CSS variables in `@theme` — that declaration *is* the base's single token source) + **Radix UI** primitives wrapped as atoms; variants via `class-variance-authority`, composition via one `cn()` helper (`clsx` + `tailwind-merge`); fonts self-hosted (`@fontsource-variable/*`). Pack decision — rejected: a prebuilt styled component kit on top. +- **Security headers come from the serving layer** — Django's `SecurityMiddleware` where Django serves the files, otherwise the reverse proxy the infra contract stands up (`./backend.md`); static `dist/` carries none of its own. Same base rules: report-only CSP first, then enforce. + +## Versioning / build identity + +The base rule applies in full — this is a cached static bundle, exactly the case it was written for. `VITE_APP_VERSION` from `npm_package_version` renders the `v` tag; a build-stamped `public/version.json` is served `no-store` by the serving layer and polled for the dismissible "Refresh to update" banner. One exception to "never force a reload": a failed lazy-route `import()` after a redeploy is unrecoverable — catch it in the route `errorElement` and offer an explicit reload as the retry action. + +## Testing + +- **Vitest + React Testing Library** for store slices, `lib/` helpers, services (network mocked at the edge — msw), and organisms' four states — the base testing rules bound, unchanged. +- **Playwright** e2e under `apps/frontend/e2e/`, run at desktop **and** a mobile device project (base narrow-viewport rule). One spec requests a deep route path directly (not by in-app navigation) and asserts the screen renders — the regression test for the SPA fallback. +- `tsc --noEmit` and `vite build` complete the suite (the root `typecheck`/`build` verbs). + +## Conflict register + +_No conflicts — this appendix only adds bindings; the base contract is unchanged._ From 24cea1c4916504eeee0488eb053df5375f9e24f3 Mon Sep 17 00:00:00 2001 From: DeyangChan Date: Wed, 29 Jul 2026 14:39:20 +0800 Subject: [PATCH 05/21] =?UTF-8?q?chore:=20remove=20Spec=20Kit=20=E2=80=94?= =?UTF-8?q?=20no=20spec=20tool=20installed=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes .specify/ and the speckit-* skills; the spec-first workflow and specs/ conventions stay, with specs/README.md's tool guidance now purely conditional. Learnings keeps the consumed feature-number registry (001–004). Co-Authored-By: Claude Fable 5 --- .claude/skills/speckit-analyze/SKILL.md | 262 ------- .claude/skills/speckit-checklist/SKILL.md | 376 ---------- .claude/skills/speckit-clarify/SKILL.md | 288 -------- .claude/skills/speckit-constitution/SKILL.md | 159 ----- .claude/skills/speckit-converge/SKILL.md | 279 -------- .claude/skills/speckit-implement/SKILL.md | 226 ------- .claude/skills/speckit-plan/SKILL.md | 170 ----- .claude/skills/speckit-specify/SKILL.md | 348 ---------- .claude/skills/speckit-tasks/SKILL.md | 217 ------ .claude/skills/speckit-taskstoissues/SKILL.md | 112 --- .specify/feature.json | 3 - .specify/init-options.json | 9 - .specify/integration.json | 15 - .specify/integrations/claude.manifest.json | 17 - .specify/integrations/speckit.manifest.json | 17 - .specify/memory/constitution.md | 55 -- .specify/scripts/bash/check-prerequisites.sh | 195 ------ .specify/scripts/bash/common.sh | 640 ------------------ .specify/scripts/bash/create-new-feature.sh | 301 -------- .specify/scripts/bash/setup-plan.sh | 84 --- .specify/scripts/bash/setup-tasks.sh | 91 --- .specify/templates/checklist-template.md | 40 -- .specify/templates/constitution-template.md | 50 -- .specify/templates/plan-template.md | 113 ---- .specify/templates/spec-template.md | 131 ---- .specify/templates/tasks-template.md | 252 ------- .specify/workflows/speckit/workflow.yml | 72 -- .specify/workflows/workflow-registry.json | 13 - CLAUDE.md | 4 +- 29 files changed, 2 insertions(+), 4537 deletions(-) delete mode 100644 .claude/skills/speckit-analyze/SKILL.md delete mode 100644 .claude/skills/speckit-checklist/SKILL.md delete mode 100644 .claude/skills/speckit-clarify/SKILL.md delete mode 100644 .claude/skills/speckit-constitution/SKILL.md delete mode 100644 .claude/skills/speckit-converge/SKILL.md delete mode 100644 .claude/skills/speckit-implement/SKILL.md delete mode 100644 .claude/skills/speckit-plan/SKILL.md delete mode 100644 .claude/skills/speckit-specify/SKILL.md delete mode 100644 .claude/skills/speckit-tasks/SKILL.md delete mode 100644 .claude/skills/speckit-taskstoissues/SKILL.md delete mode 100644 .specify/feature.json delete mode 100644 .specify/init-options.json delete mode 100644 .specify/integration.json delete mode 100644 .specify/integrations/claude.manifest.json delete mode 100644 .specify/integrations/speckit.manifest.json delete mode 100644 .specify/memory/constitution.md delete mode 100755 .specify/scripts/bash/check-prerequisites.sh delete mode 100755 .specify/scripts/bash/common.sh delete mode 100755 .specify/scripts/bash/create-new-feature.sh delete mode 100755 .specify/scripts/bash/setup-plan.sh delete mode 100755 .specify/scripts/bash/setup-tasks.sh delete mode 100644 .specify/templates/checklist-template.md delete mode 100644 .specify/templates/constitution-template.md delete mode 100644 .specify/templates/plan-template.md delete mode 100644 .specify/templates/spec-template.md delete mode 100644 .specify/templates/tasks-template.md delete mode 100644 .specify/workflows/speckit/workflow.yml delete mode 100644 .specify/workflows/workflow-registry.json diff --git a/.claude/skills/speckit-analyze/SKILL.md b/.claude/skills/speckit-analyze/SKILL.md deleted file mode 100644 index 4e4acce..0000000 --- a/.claude/skills/speckit-analyze/SKILL.md +++ /dev/null @@ -1,262 +0,0 @@ ---- -name: "speckit-analyze" -description: "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation." -argument-hint: "Optional focus areas for analysis" -compatibility: "Requires spec-kit project structure with .specify/ directory" -metadata: - author: "github-spec-kit" - source: "templates/commands/analyze.md" -user-invocable: true -disable-model-invocation: false ---- - - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Pre-Execution Checks - -**Check for extension hooks (before analysis)**: -- Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.before_analyze` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Goal. - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Goal - -Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit-tasks` has successfully produced a complete `tasks.md`. - -## Operating Constraints - -**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). - -**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit-analyze`. **Local note:** if the constitution file is still the unfilled template (placeholder tokens like `[PROJECT_NAME]` present), skip constitution-specific checks gracefully — as `/speckit-converge` does — and treat root `CLAUDE.md` (*Principles*, *Definition of Done*, *Testing*) as the constitution of record; never derive gates from placeholders. - -## Execution Steps - -### 1. Initialize Analysis Context - -Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: - -- SPEC = FEATURE_DIR/spec.md -- PLAN = FEATURE_DIR/plan.md -- TASKS = FEATURE_DIR/tasks.md - -Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). -For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). - -### 2. Load Artifacts (Progressive Disclosure) - -Load only the minimal necessary context from each artifact: - -**From spec.md:** - -- Overview/Context -- Functional Requirements -- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) -- User Stories -- Edge Cases (if present) - -**From plan.md:** - -- Architecture/stack choices -- Data Model references -- Phases -- Technical constraints - -**From tasks.md:** - -- Task IDs -- Descriptions -- Phase grouping -- Parallel markers [P] -- Referenced file paths - -**From constitution:** - -- Load `.specify/memory/constitution.md` for principle validation - -### 3. Build Semantic Models - -Create internal representations (do not include raw artifacts in output): - -- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). -- **User story/action inventory**: Discrete user actions with acceptance criteria -- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) -- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements - -### 4. Detection Passes (Token-Efficient Analysis) - -Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary. - -#### A. Duplication Detection - -- Identify near-duplicate requirements -- Mark lower-quality phrasing for consolidation - -#### B. Ambiguity Detection - -- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria -- Flag unresolved placeholders (TODO, TKTK, ???, ``, etc.) - -#### C. Underspecification - -- Requirements with verbs but missing object or measurable outcome -- User stories missing acceptance criteria alignment -- Tasks referencing files or components not defined in spec/plan - -#### D. Constitution Alignment - -- Any requirement or plan element conflicting with a MUST principle -- Missing mandated sections or quality gates from constitution - -#### E. Coverage Gaps - -- Requirements with zero associated tasks -- Tasks with no mapped requirement/story -- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks - -#### F. Inconsistency - -- Terminology drift (same concept named differently across files) -- Data entities referenced in plan but absent in spec (or vice versa) -- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) -- Conflicting requirements (e.g., one requires Next.js while other specifies Vue) - -### 5. Severity Assignment - -Use this heuristic to prioritize findings: - -- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality -- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion -- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case -- **LOW**: Style/wording improvements, minor redundancy not affecting execution order - -### 6. Produce Compact Analysis Report - -Output a Markdown report (no file writes) with the following structure: - -## Specification Analysis Report - -| ID | Category | Severity | Location(s) | Summary | Recommendation | -|----|----------|----------|-------------|---------|----------------| -| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | - -(Add one row per finding; generate stable IDs prefixed by category initial.) - -**Coverage Summary Table:** - -| Requirement Key | Has Task? | Task IDs | Notes | -|-----------------|-----------|----------|-------| - -**Constitution Alignment Issues:** (if any) - -**Unmapped Tasks:** (if any) - -**Metrics:** - -- Total Requirements -- Total Tasks -- Coverage % (requirements with >=1 task) -- Ambiguity Count -- Duplication Count -- Critical Issues Count - -### 7. Provide Next Actions - -At end of report, output a concise Next Actions block: - -- If CRITICAL issues exist: Recommend resolving before `/speckit-implement` -- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions -- Provide explicit command suggestions: e.g., "Run /speckit-specify with refinement", "Run /speckit-plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" - -### 8. Offer Remediation - -Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) - -### 9. Check for extension hooks - -After reporting, check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.after_analyze` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Operating Principles - -### Context Efficiency - -- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation -- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis -- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow -- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts - -### Analysis Guidelines - -- **NEVER modify files** (this is read-only analysis) -- **NEVER hallucinate missing sections** (if absent, report them accurately) -- **Prioritize constitution violations** (these are always CRITICAL) -- **Use examples over exhaustive rules** (cite specific instances, not generic patterns) -- **Report zero issues gracefully** (emit success report with coverage statistics) - -## Context - -$ARGUMENTS diff --git a/.claude/skills/speckit-checklist/SKILL.md b/.claude/skills/speckit-checklist/SKILL.md deleted file mode 100644 index 927ef4a..0000000 --- a/.claude/skills/speckit-checklist/SKILL.md +++ /dev/null @@ -1,376 +0,0 @@ ---- -name: "speckit-checklist" -description: "Generate a custom checklist for the current feature based on user requirements." -argument-hint: "Domain or focus area for the checklist" -compatibility: "Requires spec-kit project structure with .specify/ directory" -metadata: - author: "github-spec-kit" - source: "templates/commands/checklist.md" -user-invocable: true -disable-model-invocation: false ---- - - -## Checklist Purpose: "Unit Tests for English" - -**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain. - -**NOT for verification/testing**: - -- ❌ NOT "Verify the button clicks correctly" -- ❌ NOT "Test error handling works" -- ❌ NOT "Confirm the API returns 200" -- ❌ NOT checking if code/implementation matches the spec - -**FOR requirements quality validation**: - -- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness) -- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity) -- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency) -- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage) -- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases) - -**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works. - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Pre-Execution Checks - -**Check for extension hooks (before checklist generation)**: -- Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.before_checklist` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Execution Steps. - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Execution Steps - -1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. - - All file paths must be absolute. - - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). - -2. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints. - -3. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST: - - Be generated from the user's phrasing + extracted signals from spec/plan/tasks - - Only ask about information that materially changes checklist content - - Be skipped individually if already unambiguous in `$ARGUMENTS` - - Prefer precision over breadth - - Generation algorithm: - 1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts"). - 2. Cluster signals into candidate focus areas (max 4) ranked by relevance. - 3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit. - 4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria. - 5. Formulate questions chosen from these archetypes: - - Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?") - - Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?") - - Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?") - - Audience framing (e.g., "Will this be used by the author only or peers during PR review?") - - Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?") - - Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?") - - Question formatting rules: - - If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters - - Limit to A–E options maximum; omit table if a free-form answer is clearer - - Never ask the user to restate what they already said - - Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope." - - Defaults when interaction impossible: - - Depth: Standard - - Audience: Reviewer (PR) if code-related; Author otherwise - - Focus: Top 2 relevance clusters - - Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more. - -4. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers: - - Derive checklist theme (e.g., security, review, deploy, ux) - - Consolidate explicit must-have items mentioned by user - - Map focus selections to category scaffolding - - Infer any missing context from spec/plan/tasks (do NOT hallucinate) - -5. **Load feature context**: Read from FEATURE_DIR: - - spec.md: Feature requirements and scope - - plan.md (if exists): Technical details, dependencies - - tasks.md (if exists): Implementation tasks - - **Context Loading Strategy**: - - Load only necessary portions relevant to active focus areas (avoid full-file dumping) - - Prefer summarizing long sections into concise scenario/requirement bullets - - Use progressive disclosure: add follow-on retrieval only if gaps detected - - If source docs are large, generate interim summary items instead of embedding raw text - -6. **Generate checklist** - Create "Unit Tests for Requirements": - - Create `FEATURE_DIR/checklists/` directory if it doesn't exist - - Generate unique checklist filename: - - Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`) - - Format: `[domain].md` - - File handling behavior: - - If file does NOT exist: Create new file and number items starting from CHK001 - - If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016) - - Never delete or replace existing checklist content - always preserve and append - - **CORE PRINCIPLE - Test the Requirements, Not the Implementation**: - Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for: - - **Completeness**: Are all necessary requirements present? - - **Clarity**: Are requirements unambiguous and specific? - - **Consistency**: Do requirements align with each other? - - **Measurability**: Can requirements be objectively verified? - - **Coverage**: Are all scenarios/edge cases addressed? - - **Category Structure** - Group items by requirement quality dimensions: - - **Requirement Completeness** (Are all necessary requirements documented?) - - **Requirement Clarity** (Are requirements specific and unambiguous?) - - **Requirement Consistency** (Do requirements align without conflicts?) - - **Acceptance Criteria Quality** (Are success criteria measurable?) - - **Scenario Coverage** (Are all flows/cases addressed?) - - **Edge Case Coverage** (Are boundary conditions defined?) - - **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?) - - **Dependencies & Assumptions** (Are they documented and validated?) - - **Ambiguities & Conflicts** (What needs clarification?) - - **HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**: - - ❌ **WRONG** (Testing implementation): - - "Verify landing page displays 3 episode cards" - - "Test hover states work on desktop" - - "Confirm logo click navigates home" - - ✅ **CORRECT** (Testing requirements quality): - - "Are the exact number and layout of featured episodes specified?" [Completeness] - - "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity] - - "Are hover state requirements consistent across all interactive elements?" [Consistency] - - "Are keyboard navigation requirements defined for all interactive UI?" [Coverage] - - "Is the fallback behavior specified when logo image fails to load?" [Edge Cases] - - "Are loading states defined for asynchronous episode data?" [Completeness] - - "Does the spec define visual hierarchy for competing UI elements?" [Clarity] - - **ITEM STRUCTURE**: - Each item should follow this pattern: - - Question format asking about requirement quality - - Focus on what's WRITTEN (or not written) in the spec/plan - - Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.] - - Reference spec section `[Spec §X.Y]` when checking existing requirements - - Use `[Gap]` marker when checking for missing requirements - - **EXAMPLES BY QUALITY DIMENSION**: - - Completeness: - - "Are error handling requirements defined for all API failure modes? [Gap]" - - "Are accessibility requirements specified for all interactive elements? [Completeness]" - - "Are mobile breakpoint requirements defined for responsive layouts? [Gap]" - - Clarity: - - "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]" - - "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]" - - "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]" - - Consistency: - - "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]" - - "Are card component requirements consistent between landing and detail pages? [Consistency]" - - Coverage: - - "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]" - - "Are concurrent user interaction scenarios addressed? [Coverage, Gap]" - - "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]" - - Measurability: - - "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]" - - "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]" - - **Scenario Classification & Coverage** (Requirements Quality Focus): - - Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios - - For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?" - - If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]" - - Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]" - - **Traceability Requirements**: - - MINIMUM: ≥80% of items MUST include at least one traceability reference - - Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]` - - If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]" - - **Surface & Resolve Issues** (Requirements Quality Problems): - Ask questions about the requirements themselves: - - Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]" - - Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]" - - Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]" - - Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]" - - Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]" - - **Content Consolidation**: - - Soft cap: If raw candidate items > 40, prioritize by risk/impact - - Merge near-duplicates checking the same requirement aspect - - If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]" - - **🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test: - - ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior - - ❌ References to code execution, user actions, system behavior - - ❌ "Displays correctly", "works properly", "functions as expected" - - ❌ "Click", "navigate", "render", "load", "execute" - - ❌ Test cases, test plans, QA procedures - - ❌ Implementation details (frameworks, APIs, algorithms) - - **✅ REQUIRED PATTERNS** - These test requirements quality: - - ✅ "Are [requirement type] defined/specified/documented for [scenario]?" - - ✅ "Is [vague term] quantified/clarified with specific criteria?" - - ✅ "Are requirements consistent between [section A] and [section B]?" - - ✅ "Can [requirement] be objectively measured/verified?" - - ✅ "Are [edge cases/scenarios] addressed in requirements?" - - ✅ "Does the spec define [missing aspect]?" - -7. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### ` lines with globally incrementing IDs starting at CHK001. - -8. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize: - - Focus areas selected - - Depth level - - Actor/timing - - Any explicit user-specified must-have items incorporated - -**Important**: Each `/speckit-checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows: - -- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`) -- Simple, memorable filenames that indicate checklist purpose -- Easy identification and navigation in the `checklists/` folder - -To avoid clutter, use descriptive types and clean up obsolete checklists when done. - -## Example Checklist Types & Sample Items - -**UX Requirements Quality:** `ux.md` - -Sample items (testing the requirements, NOT the implementation): - -- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]" -- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]" -- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]" -- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]" -- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]" -- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]" - -**API Requirements Quality:** `api.md` - -Sample items: - -- "Are error response formats specified for all failure scenarios? [Completeness]" -- "Are rate limiting requirements quantified with specific thresholds? [Clarity]" -- "Are authentication requirements consistent across all endpoints? [Consistency]" -- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]" -- "Is versioning strategy documented in requirements? [Gap]" - -**Performance Requirements Quality:** `performance.md` - -Sample items: - -- "Are performance requirements quantified with specific metrics? [Clarity]" -- "Are performance targets defined for all critical user journeys? [Coverage]" -- "Are performance requirements under different load conditions specified? [Completeness]" -- "Can performance requirements be objectively measured? [Measurability]" -- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]" - -**Security Requirements Quality:** `security.md` - -Sample items: - -- "Are authentication requirements specified for all protected resources? [Coverage]" -- "Are data protection requirements defined for sensitive information? [Completeness]" -- "Is the threat model documented and requirements aligned to it? [Traceability]" -- "Are security requirements consistent with compliance obligations? [Consistency]" -- "Are security failure/breach response requirements defined? [Gap, Exception Flow]" - -## Anti-Examples: What NOT To Do - -**❌ WRONG - These test implementation, not requirements:** - -```markdown -- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001] -- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003] -- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010] -- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005] -``` - -**✅ CORRECT - These test requirements quality:** - -```markdown -- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001] -- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003] -- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010] -- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005] -- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap] -- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001] -``` - -**Key Differences:** - -- Wrong: Tests if the system works correctly -- Correct: Tests if the requirements are written correctly -- Wrong: Verification of behavior -- Correct: Validation of requirement quality -- Wrong: "Does it do X?" -- Correct: "Is X clearly specified?" - -## Post-Execution Checks - -**Check for extension hooks (after checklist generation)**: -Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.after_checklist` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.claude/skills/speckit-clarify/SKILL.md b/.claude/skills/speckit-clarify/SKILL.md deleted file mode 100644 index 1715752..0000000 --- a/.claude/skills/speckit-clarify/SKILL.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -name: "speckit-clarify" -description: "Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec." -argument-hint: "Optional areas to clarify in the spec" -compatibility: "Requires spec-kit project structure with .specify/ directory" -metadata: - author: "github-spec-kit" - source: "templates/commands/clarify.md" -user-invocable: true -disable-model-invocation: false ---- - - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Pre-Execution Checks - -**Check for extension hooks (before clarification)**: -- Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.before_clarify` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Outline - -Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. - -Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit-plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases. - -Execution steps: - -1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: - - `FEATURE_DIR` - - `FEATURE_SPEC` - - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) - - If JSON parsing fails, abort and instruct user to re-run `/speckit-specify` or verify feature branch environment. - - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). - -2. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints. - -3. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). - - Functional Scope & Behavior: - - Core user goals & success criteria - - Explicit out-of-scope declarations - - User roles / personas differentiation - - Domain & Data Model: - - Entities, attributes, relationships - - Identity & uniqueness rules - - Lifecycle/state transitions - - Data volume / scale assumptions - - Interaction & UX Flow: - - Critical user journeys / sequences - - Error/empty/loading states - - Accessibility or localization notes - - Non-Functional Quality Attributes: - - Performance (latency, throughput targets) - - Scalability (horizontal/vertical, limits) - - Reliability & availability (uptime, recovery expectations) - - Observability (logging, metrics, tracing signals) - - Security & privacy (authN/Z, data protection, threat assumptions) - - Compliance / regulatory constraints (if any) - - Integration & External Dependencies: - - External services/APIs and failure modes - - Data import/export formats - - Protocol/versioning assumptions - - Edge Cases & Failure Handling: - - Negative scenarios - - Rate limiting / throttling - - Conflict resolution (e.g., concurrent edits) - - Constraints & Tradeoffs: - - Technical constraints (language, storage, hosting) - - Explicit tradeoffs or rejected alternatives - - Terminology & Consistency: - - Canonical glossary terms - - Avoided synonyms / deprecated terms - - Completion Signals: - - Acceptance criteria testability - - Measurable Definition of Done style indicators - - Misc / Placeholders: - - TODO markers / unresolved decisions - - Ambiguous adjectives ("robust", "intuitive") lacking quantification - - For each category with Partial or Missing status, add a candidate question opportunity unless: - - Clarification would not materially change implementation or validation strategy - - Information is better deferred to planning phase (note internally) - -4. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: - - Maximum of 5 total questions across the whole session. - - Each question must be answerable with EITHER: - - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR - - A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words"). - - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. - - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. - - Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness). - - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. - - If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. - -5. Sequential questioning loop (interactive): - - Present EXACTLY ONE question at a time. - - For multiple‑choice questions: - - **Analyze all options** and determine the **most suitable option** based on: - - Best practices for the project type - - Common patterns in similar implementations - - Risk reduction (security, performance, maintainability) - - Alignment with any explicit project goals or constraints visible in the spec - - Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice). - - Format as: `**Recommended:** Option [X] - ` - - Then render all options as a Markdown table: - - | Option | Description | - |--------|-------------| - | A |