App reorganization
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
# Backend work — linear tickets
|
||||
|
||||
Copy each block into Linear (or your tracker) as a separate issue, **in order**. Earlier tickets are prerequisites for later ones.
|
||||
|
||||
**Foundation already in the repo (no ticket needed unless you are onboarding a greenfield clone):** Prisma schema ([prisma/schema.prisma](prisma/schema.prisma)), migrations, `lib/server/*`, Route Handlers under `app/api/*`, [docker-compose.yml](docker-compose.yml), [Dockerfile](Dockerfile), [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example), [lib/create/api.ts](lib/create/api.ts), create-flow draft **PUT** via `useCreateFlowExit` / `PostLoginDraftTransfer` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`.
|
||||
|
||||
### Review sync (relevant feedback only)
|
||||
|
||||
A backend review was merged into **[docs/backend-roadmap.md](backend-roadmap.md)** after checking the repo. **Incorporated:** custom session lifecycle follow-ups (not a mandate to adopt Auth.js/Lucia), **passwordless email (magic-link request)** rate limits in-memory until multi-instance + shared store, `RuleDraft` already has `updatedAt` (no migration to add it), **prefer external web vitals** over product Postgres by default, API error shape + request-id observability targets, **authorization v1** aligned with `app/api/rules`, Prisma **never edit applied migrations**, **profile / my rules / account** scope from Figma profile (`22143:900069`) as **Ticket 15** (change email deferred). **Excluded:** requiring NextAuth/Lucia; “add `updatedAt` on drafts”; hard ban on DB for vitals (softened to default external). **Parallel Linear issues:** **CR-84** (API errors — **unblocked** now that **CR-73** is Done), **CR-85** (session lifecycle — **unblocked** now that **CR-75** is Done)—see **Linear** table at the end of this doc.
|
||||
|
||||
---
|
||||
|
||||
## When you need server / admin access (and for what)
|
||||
|
||||
Use this if you **do not** have SSH or hosting access yet. Most engineering tickets are **local-only** until you deploy somewhere shared.
|
||||
|
||||
### You do **not** need the server admin for
|
||||
|
||||
- **Tickets 1–8, 10:** Everything runs on your machine: `docker compose up -d postgres mailhog`, `.env`, `npm run dev`, `npx prisma migrate dev`. **Magic-link** sign-in email can use Mailhog or **dev server logs** (verify URL) when `SMTP_URL` is unset—no real SMTP required locally.
|
||||
- **Verifying APIs:** Use `localhost` and the same Docker Postgres—no production host.
|
||||
|
||||
### The **first** time you need someone with hosting access
|
||||
|
||||
That is when you deploy to **staging** or **production** (a URL other people use, or a persistent DB not on your laptop). Until then, you can finish the core product slice without server credentials.
|
||||
|
||||
Ask the admin to provide (or do for you) the items below—**Ticket 12** turns this into a written runbook.
|
||||
|
||||
| What | Why you need it |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Postgres** | Managed instance or container; a **`DATABASE_URL`** you can plug into the deployed app. |
|
||||
| **Run migrations** | Someone runs **`npx prisma migrate deploy`** against that database **before** the new app version serves traffic (or gives you a secure way to run it in CI/CD). |
|
||||
| **`SESSION_SECRET`** | Long random string in production env (sessions **+ hashed magic-link tokens**). |
|
||||
| **SMTP** | **`SMTP_URL`** + **`SMTP_FROM`** for real **sign-in link** email; not required on laptop if you use logs/Mailhog. |
|
||||
| **DNS for mail** | Often **SPF/DKIM** so **magic-link** messages are not spam—admin or whoever owns DNS. |
|
||||
| **TLS + hostname** | HTTPS URL for the site; reverse proxy (nginx, Caddy, etc.) in front of Node. |
|
||||
| **Health check** | Load balancer or platform should probe **`GET /api/health`** (or your chosen path). |
|
||||
| **Secrets storage** | Env vars or secret manager—never commit `.env` with secrets. |
|
||||
| **Backups** | Postgres backup/restore for production (and ideally staging). |
|
||||
|
||||
Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admin builds/pushes/runs the container with the env vars above.
|
||||
|
||||
### Ticket-by-ticket: admin involvement
|
||||
|
||||
| Ticket | Need server admin? | What for |
|
||||
| ------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1–2 | **No** | Docs and app code only. |
|
||||
| 3 | **No** to build/test; **Yes** when **magic-link email** must work on a **deployed** env | Real **SMTP** + DNS on staging/prod (same as table above). |
|
||||
| 4–8 | **No** | Local or staging URL is still “your” deploy—admin only if that URL is on their infra. |
|
||||
| 9 | **No** to implement; **Yes** when **production** uses multiple instances or read-only FS | **Default** is external RUM/log drain; Postgres vitals only if ops explicitly wants one datastore—may need vendor keys for SaaS. |
|
||||
| 10 | **No** to code | Same deploy pipeline as the rest of the app. |
|
||||
| 11 | **Maybe** | Whoever owns **Gitea runners**: can they run Postgres in CI? Not the same as production server, but often the same “infra” person. |
|
||||
| 12 | **Yes—this is the handoff ticket** | You (or admin) write **`docs/ops-backend-deploy.md`** so deploy steps are explicit; **you need admin input** to fill in hostnames, DB provider, SMTP, backup policy. |
|
||||
|
||||
### One-line summary
|
||||
|
||||
**You only need the server admin when you move off your laptop to a shared staging/production host**—for database, secrets, TLS, SMTP/DNS, migrations on that DB, health checks, and backups. Until then, **Tickets 1–8 are unblocked** with Docker Compose locally.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 1 — Align `docs/backend-roadmap.md` with the current codebase
|
||||
|
||||
**Depends on:** nothing.
|
||||
|
||||
**Goal:** Remove stale statements so the roadmap matches reality and stays a trustworthy reference until you delete it.
|
||||
|
||||
**Context:** Section 1 still says there is no DB and only web-vitals API; the app now has Postgres models, auth, drafts, rules, templates API, etc.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Rewrite **§1 Where we are** to list: Prisma + Postgres, existing `app/api/*` routes, create-flow persistence (anonymous `localStorage` + optional server draft PUT when sync is on), web-vitals still file-based.
|
||||
2. In **§9 Build order** (build steps were renumbered from old §5), mark what is **operator/manual**, what is **already shipped in the repo**, and what is **still product/frontend** (publish wiring, templates in UI, etc.).
|
||||
3. Add **HTTP API (implemented in repo)** — table mirroring [CONTRIBUTING.md](CONTRIBUTING.md), plus note for `/api/web-vitals`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [x] A new contributor reading only the roadmap does not think the backend is unbuilt.
|
||||
- [x] **§13 Optional later** (old §9) unchanged in intent — optional Redis, session-library spike, draft versioning, standalone API, OpenAPI, fourth env.
|
||||
|
||||
**Status:** [CR-72](https://linear.app/community-rule/issue/CR-72/backend-align-docsbackend-roadmapmd-with-current-codebase) **Done**.
|
||||
|
||||
**Files:** [docs/backend-roadmap.md](docs/backend-roadmap.md) only.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 2 — Formalize `CreateFlowState` and validate API payloads
|
||||
|
||||
**Depends on:** Ticket 1 (optional but keeps docs honest).
|
||||
|
||||
**Goal:** Replace the open `[key: string]: unknown` shape in [app/(app)/create/types.ts](app/(app)/create/types.ts) with real fields (or nested objects) agreed with design/product, and validate JSON on the server for drafts and publish.
|
||||
|
||||
**Context:** `PUT /api/drafts/me` and `POST /api/rules` accept loose objects today; oversized or malformed payloads are a stability and security concern.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Document intended fields per create-flow step (can start minimal: e.g. `title`, `sections`, `stakeholders` placeholders) in `CreateFlowState`.
|
||||
2. Add **Zod** (or reuse **Ajv** if you prefer consistency with [lib/validation.ts](lib/validation.ts)) schemas:
|
||||
- `createFlowStateSchema` for draft `payload`.
|
||||
- `publishedRuleDocumentSchema` for `document` on `POST /api/rules`.
|
||||
3. In [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts) and [app/api/rules/route.ts](app/api/rules/route.ts), parse with schema; on failure return `400` with a small `{ error, details? }` body.
|
||||
4. Enforce a **max payload size** (e.g. reject bodies > 512KB) via route handler check or Next config if applicable.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [x] TypeScript reflects the real shape of `CreateFlowState` (no unnecessary `unknown` for known keys).
|
||||
- [x] Invalid draft/publish requests return 400, not 500.
|
||||
- [x] Unit tests for schemas (Vitest) or route tests with MSW.
|
||||
|
||||
**Status:** [CR-73](https://linear.app/community-rule/issue/CR-73/backend-formalize-createflowstate-validate-draftpublish-api-payloads) **Done**.
|
||||
|
||||
**Files:** [app/(app)/create/types.ts](app/(app)/create/types.ts), [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/server/validation/](lib/server/validation/) (Zod + plain-JSON checks), [package.json](package.json) (`zod`).
|
||||
|
||||
**Note:** Repo-wide **API error JSON shape** and **request-id logging** are **Ticket 13 / CR-84**—coordinate 400 response bodies with that issue so validation errors match the agreed `{ error: { code, message } }` pattern.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 3 — Email magic-link sign-in UI (end-to-end with existing APIs)
|
||||
|
||||
**Depends on:** Ticket 2 (soft dependency: types help name fields you might store post-login; can start in parallel if needed).
|
||||
|
||||
**Server / admin:** **Not required** to build and test (Mailhog or verify URL in server logs locally). **Required** when **magic-link email** must work on **staging/production**: admin provides **SMTP** + usually **DNS (SPF/DKIM)** and sets env on the host (see top table). **Residual:** links in email use the app origin—reverse proxy / `Host` must match the URL users open.
|
||||
|
||||
**Goal:** Let a user request a **sign-in link** and complete sign-in in the browser using existing endpoints.
|
||||
|
||||
**Context:** APIs: `POST /api/auth/magic-link/request`, `GET /api/auth/magic-link/verify`, `GET /api/auth/session`, `POST /api/auth/logout`. Prisma: `MagicLinkToken`. Client: [`requestMagicLink`](lib/create/api.ts).
|
||||
|
||||
**Implementation (shipped):**
|
||||
|
||||
1. **`/login`** route **and** **header modal** — primary **Log in** entry is [`AuthModalProvider`](app/contexts/AuthModalContext.tsx) + [app/components/modals/Login/](app/components/modals/Login/); [app/(app)/login/page.tsx](app/(app)/login/page.tsx) (solid shell, `usePortal={false}`) remains for verify **error** redirects and bookmarks.
|
||||
2. Flow: email → “Send link” → user opens link (email, Mailhog, or dev log) → `GET /api/auth/magic-link/verify?token=...` sets session and redirects; optional `next` for post-login path.
|
||||
3. Surface API errors: invalid email, 429 `retryAfterMs`, expired/invalid token, network failure (accessible copy).
|
||||
4. Ensure `fetch` calls use `credentials: "include"` where needed (see [lib/create/api.ts](lib/create/api.ts)).
|
||||
5. **Dev:** without `SMTP_URL`, verify URL is logged; with Mailhog, use [docker-compose.yml](docker-compose.yml) and `SMTP_URL=smtp://localhost:1025`.
|
||||
6. **Marketing header:** When signed in (`fetchAuthSession`), **Log in** becomes **Profile** linking to [`/profile`](app/(app)/profile/page.tsx) (placeholder until Ticket 15 / CR-86). Implemented in [TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx) + [TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx).
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [x] Happy path: user completes magic-link verify and `GET /api/auth/session` returns `user` in the same browser session.
|
||||
- [x] Keyboard + screen-reader friendly forms (labels, errors associated with fields).
|
||||
- [x] No secrets in client bundle.
|
||||
- [x] Header shows **Profile** → placeholder `/profile` when session present; **Log in** when anonymous (opens modal, not only `/login`).
|
||||
|
||||
**Status:** [CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done) **Done** for shipped UI/APIs. **Residual checklist** below: repo doc items are **done**; use Linear (CR-74 or child issue) to track **per-environment** staging URL checks.
|
||||
|
||||
**Files:** [app/(app)/login/](app/(app)/login/), [app/(app)/profile/](app/(app)/profile/) (placeholder), [app/components/modals/Login/](app/components/modals/Login/), [messages/en/pages/login.json](messages/en/pages/login.json), [messages/en/pages/profile.json](messages/en/pages/profile.json), [messages/en/components/header.json](messages/en/components/header.json), [app/components/navigation/TopNav/TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx), [app/components/navigation/TopNav/TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx), [lib/create/api.ts](lib/create/api.ts), [app/api/auth/magic-link/request/route.ts](app/api/auth/magic-link/request/route.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), [prisma/schema.prisma](prisma/schema.prisma) (`MagicLinkToken`), [lib/server/mail.ts](lib/server/mail.ts). Onboarding: [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example).
|
||||
|
||||
### Residual / before CR-75 (create-flow session UI)
|
||||
|
||||
**Intent:** [Ticket 4](#ticket-4--session-affordances-in-the-create-flow-signed-in-state--sign-out) (**CR-75**) needs a reliable signed-in story across marketing + `/create`. Below: what is **done in repo** vs what to **verify per environment**.
|
||||
|
||||
1. **Contributor / onboarding** — **Done:** [CONTRIBUTING.md](CONTRIBUTING.md) API table and sign-in section describe **magic-link** request/verify, dev log URL, and Mailhog. [`.env.example`](.env.example) comments match.
|
||||
2. **Smoke checklist** — **Done:** **Email magic link (sign-in)** in [CONTRIBUTING.md](CONTRIBUTING.md); build-order §9 in [docs/backend-roadmap.md](backend-roadmap.md) includes the same happy path + session check.
|
||||
3. **Staging / production URLs** — **Verify on each deploy:** emails use `request.nextUrl.origin`; confirm reverse proxy and **`Host`** so links in mail match the public site (CONTRIBUTING + roadmap §9 spell this out).
|
||||
4. **Docs alignment** — **Done:** [docs/backend-roadmap.md](backend-roadmap.md) and this doc treat magic link as primary; CR-72/CR-73 schema work is not a blocker for CR-75.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 4 — Session affordances in the create flow (signed-in state + sign out)
|
||||
|
||||
**Depends on:** Ticket 3.
|
||||
|
||||
**Goal:** In `/create/*`, **Exit** / **Save & Exit** (from `select` onward for signed-in users) is the only top-nav chrome—no email or Sign out in the create shell. **Anonymous:** progress in **`create-flow-anonymous`** localStorage; **Exit** opens the global **Save your progress?** auth modal (magic link + `?syncDraft=1` return); after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) **PUT**s to `/api/drafts/me` when sync is on. **Signed-in:** **Save & Exit** **PUT**s via [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC`**. **Sign out** for QA lives on **[ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx)**. Site **Log in** opens the same modal overlay ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)), not only `/login`.
|
||||
|
||||
**Context:** **`saveDraftOnExit`** is gated on **session + step ≥ select**. Layout **`fetchAuthSession`** drives anonymous vs authenticated persistence and exit behavior. **Save & Exit** styling: Figma [20907:212637](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20907-212637). Save-progress exit modal: Figma `22398:23743`.
|
||||
|
||||
**Implementation (repo):**
|
||||
|
||||
1. [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx): session + `enableAnonymousPersistence`; anonymous exit → `openLogin({ variant: 'saveProgress', nextPath })`; signed-in exit → `useCreateFlowExit`.
|
||||
2. [CreateFlowTopNav](app/components/utility/CreateFlowTopNav/): i18n [`messages/en/create/topNav.json`](messages/en/create/topNav.json); logo + Share/Export/Edit (completed) + Exit/Save & Exit only.
|
||||
3. [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts): `saveDraftToServer` when sync + signed in; `clearState` + home.
|
||||
4. [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx): optional anonymous localStorage mirror via `enableAnonymousPersistence`.
|
||||
5. **QA:** [ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx) Sign out when session present.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [x] Completed step still works; **Save & Exit** gating uses session + step (not conflated with `completed` only).
|
||||
- [x] Signed in + sync: Save & Exit persists server-side; anonymous: localStorage + exit modal + transfer after magic link. Sign out on profile clears session. _(Re-verify on staging/prod as needed.)_
|
||||
|
||||
**Files:** [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/components/utility/CreateFlowTopNav/](app/components/utility/CreateFlowTopNav/), [app/(app)/create/context/CreateFlowContext.tsx](app/(app)/create/context/CreateFlowContext.tsx), [messages/en/create/topNav.json](messages/en/create/topNav.json), [app/(app)/profile/ProfilePageClient.tsx](app/(app)/profile/ProfilePageClient.tsx).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 5 — Harden server draft sync (UX + edge cases)
|
||||
|
||||
**Depends on:** Tickets 2–4.
|
||||
|
||||
**Goal:** Server draft **PUT** path is production-grade when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (Save & Exit, post-login transfer from anonymous draft).
|
||||
|
||||
**Context:** Auto-hydrate / debounced autosave component was removed; signed-in resume uses `GET /api/drafts/me` in the create layout.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. **Hydration:** **Done:** [SignedInDraftHydration](app/(app)/create/SignedInDraftHydration.tsx) + [messages/en/create/draftHydration.json](messages/en/create/draftHydration.json); skips `?syncDraft=1` / transfer-pending (PostLogin owns that). Wired in [layout](app/(app)/create/layout.tsx).
|
||||
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/(app)/create/utils/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
|
||||
3. **Save failures (API surface):** **Done (CR-76):** [saveDraftToServer](lib/create/api.ts) returns `SaveDraftResult` with parsed API `message`; wired in [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts) and [PostLoginDraftTransfer](app/(app)/create/PostLoginDraftTransfer.tsx).
|
||||
4. **Save failures (UX):** **Done (CR-76):** Dismissible banner with server `message` (no second confirm to leave); post-login transfer shows reason; unit tests in `tests/unit/saveDraftToServer.test.ts`. Retry/backoff remains optional.
|
||||
5. **Tests:** `saveDraftToServer` unit tests; [draftHydrationUtils](lib/create/draftHydrationUtils.ts) unit tests. Playwright against Next standalone + route mocks for `/api/auth/session` was flaky here; cover hydration with **manual QA** (signed in + sync on + server draft) or add a future E2E with a dedicated auth fixture.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [x] No silent data loss when server save fails (user sees reason in banner; stays in flow to retry Save & Exit or leave via e.g. logo).
|
||||
- [x] User understands when server draft replaced local state (if applicable) — conflict `window.confirm` when both browser anonymous draft and account draft exist; otherwise silent apply of single source.
|
||||
|
||||
**Files:** [lib/create/api.ts](lib/create/api.ts), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/(app)/create/PostLoginDraftTransfer.tsx](app/(app)/create/PostLoginDraftTransfer.tsx), [app/(app)/create/SignedInDraftHydration.tsx](app/(app)/create/SignedInDraftHydration.tsx), [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx), tests under `tests/`.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 6 — Wire “Publish rule” from the create flow to `POST /api/rules`
|
||||
|
||||
**Depends on:** Tickets 2–4 (Ticket 5 optional).
|
||||
|
||||
**Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts).
|
||||
|
||||
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on the `final-review` / `completed` steps (see [app/(app)/create/screens/CreateFlowScreenView.tsx](app/(app)/create/screens/CreateFlowScreenView.tsx) and `app/(app)/create/screens/`) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Map `useCreateFlow().state` → `title` / `summary` / `document` (document likely mirrors [CommunityRuleDocument](app/components/sections/CommunityRuleDocument/) shape or raw JSON).
|
||||
2. Call `publishRule` on explicit user action (“Publish” / “Finalize”) or on transition to `completed` (product decision—prefer explicit button to avoid double-submit).
|
||||
3. Handle **401**: redirect or modal to sign-in (Ticket 3).
|
||||
4. Success: navigate to `completed` with rule id in query or state; optional confetti per design.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Published row appears in Postgres (`PublishedRule`) and `GET /api/rules` lists it.
|
||||
- [ ] User sees clear success/failure.
|
||||
|
||||
**Files:** relevant `app/(app)/create/*/page.tsx`, [lib/create/api.ts](lib/create/api.ts) if request shape changes, types from Ticket 2.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 7 — Seed `RuleTemplate` data and document how to re-run
|
||||
|
||||
**Depends on:** none (API exists at [app/api/templates/route.ts](app/api/templates/route.ts)).
|
||||
|
||||
**Goal:** Curated templates exist in DB for recommendations (v1 = static curated list, no ML).
|
||||
|
||||
**Not in v1 (this ticket):** **Spreadsheet-authored matrices**, multi-axis **facet filtering**, or **ranked** recommendations from user answers — that is **Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** after the flat list ships.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add [Prisma seed](https://www.prisma.io/docs/guides/migrate/seed-database): `prisma/seed.ts` with `upsert` on `slug` for idempotent runs.
|
||||
2. In [package.json](package.json), set `"prisma": { "seed": "tsx prisma/seed.ts" }` or `node --loader ts-node/esm` per your preference.
|
||||
3. Seed 3–10 rows aligned with marketing copy today ([messages/en/components/ruleStack.json](messages/en/components/ruleStack.json) or home cards) — `title`, `category`, `description`, `body` JSON, `sortOrder`, `featured`.
|
||||
4. Document: `npx prisma db seed` in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] `GET /api/templates` returns non-empty `templates` after seed on empty DB.
|
||||
- [ ] Re-running seed does not duplicate rows.
|
||||
|
||||
**Files:** `prisma/seed.ts`, [package.json](package.json), [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 8 — Load rule templates from the API in the UI
|
||||
|
||||
**Depends on:** Ticket 7.
|
||||
|
||||
**Goal:** Home or create entry surfaces use live template data instead of only static i18n JSON.
|
||||
|
||||
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and create entry surfaces reference future template work. Wizard URLs are static segments under `app/(app)/create/`; see [`docs/create-flow.md`](create-flow.md) and **Ticket 17** for the canonical custom flow.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add a small client or server data fetch to `GET /api/templates` (RSC `fetch` with cache tags, or client `useEffect` with loading skeleton—match existing data-fetch patterns in the app).
|
||||
2. Map API rows to existing card components; keep i18n for chrome strings (“See all templates”).
|
||||
3. Empty state: if API returns `[]`, fall back to static copy or hide section per design.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Changing a template row in Prisma Studio reflects after refresh (or revalidate).
|
||||
- [ ] No layout shift regression on LCP-critical pages (use skeletons).
|
||||
|
||||
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), create-flow entry routes under [app/(app)/create/](app/(app)/create/), possibly new `lib/templates/fetchTemplates.ts`.
|
||||
|
||||
**Follow-up:** **Ticket 16** — dynamic recommendations from authoring spreadsheets and create-flow answers.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 16 — Template recommendation matrix + spreadsheet ingestion
|
||||
|
||||
**Depends on:** Tickets 7–8 (templates exist in DB and UI can fetch them). Can overlap **Ticket 6** (create flow) for wizard steps that POST answers.
|
||||
|
||||
**Goal:** Support **dynamic** template selection driven by **authoring spreadsheets** (e.g. Excel / Google Sheets exported to `.xlsx`): each **row** is a template variant with long-form copy (title, description, principles, steps, objections); **columns** encode **matching dimensions** (group size bands, organization type, location, maturity, etc.) with symbols or weights (✓/✗, 0–1 scores). The create flow (or home) should **narrow or rank** options from **user-supplied facets** or a short questionnaire.
|
||||
|
||||
**Context:** The current [`RuleTemplate`](prisma/schema.prisma) model is a **flat** list (`slug`, `title`, `category`, `description`, `body` JSON). It does **not** model dimension columns, matrix versioning, or import from sheets. Example product shape: a “Decision-making” workbook → many governance patterns, each row tied to applicability across org context.
|
||||
|
||||
**Implementation (phased — product can stop after any phase):**
|
||||
|
||||
1. **Authoring contract:** Document required columns / sheet tabs (per domain: decision-making, meetings, etc.), validation rules, and how ✓/✗ or numeric cells map to API filters or scores.
|
||||
2. **Storage:** Either extend `RuleTemplate` / `body` with a structured `recommendationMatrix` blob **or** add normalized tables (`TemplateDimension`, `TemplateFacetValue`, `TemplateApplicability`) — pick based on query needs and reporting.
|
||||
3. **Import:** Script or internal admin path: `.xlsx` → parse (e.g. `xlsx` / SheetJS) → validate → upsert DB rows or generate seed JSON checked into repo. **Default:** batch job on export, **not** live Sheets API in prod unless explicitly required.
|
||||
4. **API:** Extend `GET /api/templates` with optional query params (`?facet.orgType=nonprofit&facet.size=6-12`) **or** add `POST /api/templates/recommend` with a JSON body of answers; return ranked `templates` + optional `scores` / `reasons` for UI.
|
||||
5. **UI:** Create-flow step(s) collect facets; call API; prefill `CreateFlowState` or document JSON from chosen row’s `body`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Importing an updated workbook (or running the importer) changes recommendations without hand-editing Prisma rows in Studio.
|
||||
- [ ] API behavior is documented (params or POST body) and covered by tests for at least one reference matrix.
|
||||
- [ ] Invalid / partial facet combinations degrade gracefully (empty list vs fallback featured templates).
|
||||
|
||||
**Files (expected):** `prisma/schema.prisma`, `lib/templates/*` or `scripts/import-templates-xlsx.ts`, `app/api/templates/*`, create-flow pages, tests.
|
||||
|
||||
**Linear:** [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) (**Backlog**). **Parallel** to much of the core chain; **blocked** only by having **CR-78**/**CR-79** far enough along that a template list exists (or stub rows).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 17 — Canon custom create-rule wizard (routes, resume, progress) + docs
|
||||
|
||||
**Depends on:** none for documentation; soft optional **CR-73**, **CR-76**, **CR-77** for payload/resume/publish alignment.
|
||||
|
||||
**Goal:** Establish the **official custom** create-rule flow (ordered steps, URLs, persistence, entry points, **Figma three-stage framing**) in repo docs and close gaps between that spec and the implementation (routing clutter, progress UI, step source of truth, resume vs URL).
|
||||
|
||||
**Context:** Step order lives in [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts). Wizard screens render from [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) plus [`CREATE_FLOW_SCREEN_REGISTRY`](app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node + layout family per slug). [`docs/create-flow.md`](create-flow.md) is the **canonical** URL/persistence summary: **Create Community** (eight semantic steps ending at `review`) → **Create Custom CommunityRule** → **Review and complete**. **Full create-from-template** will likely use **additional route(s)** when product defines it; **`/create/review-template/[slug]`** remains auxiliary preview only. **Template → `final-review` or mid-wizard prefill** is **out of scope** here (future ticket); `/create/informational?template=` is a **no-op** until then.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Keep [`docs/create-flow.md`](create-flow.md) in sync with product/Figma (stage ↔ step mapping, future template routes).
|
||||
2. ~~Remove legacy [`app/(app)/create/[step]/page.tsx`](app/(app)/create/[step]/page.tsx)~~ — replaced by [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) with real screens; unknown slugs `notFound()`.
|
||||
3. Unify **step source of truth**: URL via [`useCreateFlowNavigation`](app/(app)/create/hooks/useCreateFlowNavigation.ts) vs unused [`CreateFlowContext`](app/(app)/create/context/CreateFlowContext.tsx) `currentStep` — pick one model; align [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) / draft payload if needed.
|
||||
4. **Resume:** After [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx), decide redirect to `/create/${state.currentStep}` vs stay on current URL; test or document.
|
||||
5. Wire [`CreateFlowFooter`](app/components/utility/CreateFlowFooter/) `ProportionBar` to step progress from `FLOW_STEP_ORDER` (and `review-template` / `completed` exceptions per design); optional **two-level progress** (stage + step within stage) when design specifies.
|
||||
6. When Figma hands off, surface **stage labels** in create shell (top nav, footer, or step chrome) using the mapping in `create-flow.md`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] [`docs/create-flow.md`](create-flow.md) matches shipped behavior or lists known gaps, including **Figma three-stage** mapping and **future template route** note.
|
||||
- [ ] No misleading dynamic step placeholder for valid wizard URLs.
|
||||
- [ ] Footer progress reflects step index **or** doc/issue records a deliberate deferral with design sign-off.
|
||||
- [ ] Hydration + `currentStep` behavior is verified (redirect vs stay).
|
||||
- [ ] `?template=` documented as deferred; no implied “template customize → full wizard” parity.
|
||||
|
||||
**Files:** [`docs/create-flow.md`](create-flow.md), [`app/(app)/create/`](app/(app)/create/), [`app/components/utility/CreateFlowFooter/`](app/components/utility/CreateFlowFooter/), optionally [`docs/backend-roadmap.md`](backend-roadmap.md) §12 cross-links.
|
||||
|
||||
**Linear:** [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) (**Backlog**). **Parallel** to templates (7–8) and publish (6); not part of **CR-72 → CR-83**.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 9 — Persist web vitals outside `.next` (prefer external RUM)
|
||||
|
||||
**Depends on:** none (orthogonal).
|
||||
|
||||
**Server / admin:** **Not required** to implement. **Relevant** when production is **multi-instance** or **read-only filesystem**; external tools may need **vendor API keys** in env.
|
||||
|
||||
**Goal:** [app/api/web-vitals/route.ts](app/api/web-vitals/route.ts) stops relying on ephemeral files under `.next/web-vitals` in production.
|
||||
|
||||
**Context:** Multi-instance / Docker loses file-based metrics. [docs/backend-roadmap.md](backend-roadmap.md) §7: **default** is **external** analytics or log drain—keep product Postgres for product data.
|
||||
|
||||
**Implementation (pick one — prefer A or B first):**
|
||||
|
||||
- **A (preferred):** Integrate **external RUM / logging** (host metrics, Vercel Web Analytics, OpenTelemetry export, Datadog, etc.); stop or thin local aggregation; `app/(admin)/monitor/page.tsx` links out or shows reduced scope.
|
||||
- **B:** Forward events from the route to a **log drain** or APM; trim custom dashboard if redundant.
|
||||
- **C (fallback only):** New Prisma model `WebVitalEvent` + migrate + read path in monitor—**only** if ops explicitly chooses a single-store tradeoff (document why).
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Production with read-only filesystem does not break vitals collection path.
|
||||
- [ ] Monitor page still useful or intentionally replaced with a doc link.
|
||||
|
||||
**Files:** [app/api/web-vitals/route.ts](app/api/web-vitals/route.ts), [app/(admin)/monitor/](<app/(admin)/monitor/page.tsx>) (adjust paths as needed), optionally `prisma/schema.prisma` **only if** option C.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 10 — Public rule detail (optional product scope)
|
||||
|
||||
**Depends on:** Ticket 6.
|
||||
|
||||
**Goal:** Shareable link for a published rule.
|
||||
|
||||
**Note:** Complements **Ticket 15** profile cards: users can open a **public** detail URL from a rule listed on their dashboard; the profile page does **not** replace this ticket.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add `GET /api/rules/[id]/route.ts` returning `{ rule }` or 404 (public read; no secrets).
|
||||
2. Add `app/(marketing)/rules/[id]/page.tsx` (or under `create` if private) rendering `document` JSON with existing document components.
|
||||
3. Consider soft-delete flag later; out of scope unless product requires hide.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] UUID/cuid from Ticket 6 opens a readable page for anonymous users.
|
||||
- [ ] Invalid id returns 404.
|
||||
|
||||
**Files:** new route handler, new page, optional layout.
|
||||
|
||||
**Linear:** [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional). **Related in Linear:** [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) (Ticket 15 — profile cards linking to public detail).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 11 — CI: database migration smoke (optional, runner-dependent)
|
||||
|
||||
**Depends on:** existing [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml).
|
||||
|
||||
**Server / admin:** **Not production server**—but you may need whoever runs **Gitea/self-hosted runners** to allow **Postgres in CI** (Docker service / sidecar) or to accept a **manual migrate** process documented instead.
|
||||
|
||||
**Goal:** Catch broken SQL migrations before merge.
|
||||
|
||||
**Context:** Lint job already runs `prisma validate` with a dummy `DATABASE_URL`. **Migrate** needs a real Postgres reachable from the runner.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. If Gitea runners support **Docker sidecar** or **postgres service**, add a job: start Postgres, set `DATABASE_URL`, `npx prisma migrate deploy`, then run a minimal test that hits `/api/health` with DB connected (may require `next build` + short `next start` + curl).
|
||||
2. If **macOS self-hosted** runners cannot run service containers easily, document in CONTRIBUTING: “run `migrate deploy` against staging before prod” and keep validate-only in CI.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Broken migration fails CI **or** documented alternative process is explicit.
|
||||
|
||||
**Files:** [.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml), [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 12 — Staging / production runbook (operator checklist)
|
||||
|
||||
**Depends on:** Tickets 1–8 complete enough to deploy a vertical slice.
|
||||
|
||||
**Server / admin:** **This is the main ticket where you need the admin.** You draft the runbook; **admin fills in real hostnames, DB endpoint, SMTP, backup tooling, and who runs `migrate deploy`.** Without their input, you cannot complete production-ready deploy steps.
|
||||
|
||||
**Goal:** Single doc for admin: env vars, TLS, DB backups, migrations, Docker, SMTP, health checks.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add `docs/ops-backend-deploy.md` (or similar) with numbered steps:
|
||||
- Required env: `DATABASE_URL`, `SESSION_SECRET`, `SMTP_URL`, `SMTP_FROM`, optional `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`.
|
||||
- `docker compose` vs `Dockerfile` deploy; `prisma migrate deploy` before traffic.
|
||||
- Reverse proxy: `GET /api/health` for LB health.
|
||||
- Backups and restore drill for Postgres.
|
||||
- SMTP DNS (SPF/DKIM).
|
||||
2. Cross-link [docs/backend-roadmap.md](docs/backend-roadmap.md) §11 (environments) and §8 (migrations policy); note **never rewrite applied migrations** and where application logs go.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Someone who did not write the code can deploy and roll back migrations with only the doc.
|
||||
|
||||
**Files:** new `docs/ops-backend-deploy.md`.
|
||||
|
||||
---
|
||||
|
||||
## Ticket 13 — API error contract + structured logging
|
||||
|
||||
**Depends on:** Ticket 2 (validation work defines many 400s).
|
||||
|
||||
**Server / admin:** None.
|
||||
|
||||
**Goal:** Standardize JSON errors and lightweight observability per [docs/backend-roadmap.md](backend-roadmap.md) §7.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Document target shape `{ error: { code, message }, details? }` and map validation failures into `details` where useful.
|
||||
2. Add a small **route helper** or wrapper: generate or forward **`x-request-id`**, log errors with `lib/logger` + id.
|
||||
3. Migrate high-traffic or auth routes first; follow-up pass for remaining `app/api/*`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] At least auth + draft + rules routes return the agreed shape for new code paths.
|
||||
- [ ] Errors in logs include request id when available.
|
||||
|
||||
**Files:** `lib/server/` (new helper), selected `app/api/**/route.ts`, optional tests.
|
||||
|
||||
**Linear:** [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) (**CR-73** Done — ready to pick up).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 14 — Custom session lifecycle (rotation, cleanup, policy)
|
||||
|
||||
**Depends on:** Ticket 4 (session visible in create flow).
|
||||
|
||||
**Server / admin:** None for implementation; production cron may need admin if cleanup runs as a job.
|
||||
|
||||
**Goal:** Make custom Prisma sessions maintainable: rotation, invalidation policy, expired-row cleanup—per [docs/backend-roadmap.md](backend-roadmap.md) §4–5.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. **Policy:** On **new sign-in** (magic-link verification / session creation), decide whether to **delete other `Session` rows** for that user (single active session) or allow multiple devices (document choice).
|
||||
2. **Rotation (optional v1.1):** Issue new token on privilege-sensitive actions if product requires.
|
||||
3. **Cleanup:** Delete or mark expired sessions (scheduled job, or prune on read with occasional batch).
|
||||
4. **Docs:** Add short ADR or comment block in `lib/server/session.ts`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Documented behavior matches implementation.
|
||||
- [ ] Expired sessions do not accumulate unbounded in production over months.
|
||||
|
||||
**Files:** [lib/server/session.ts](lib/server/session.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), optional `prisma` migration if new columns (unlikely).
|
||||
|
||||
**Linear:** [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) (**unblocked** — **CR-75** Done).
|
||||
|
||||
---
|
||||
|
||||
## Ticket 15 — Profile dashboard + account (Figma profile)
|
||||
|
||||
**Depends on:** Ticket 3 (auth), **Ticket 4** (session in UI), **Ticket 6** (publish so users have rules to list). Soft optional: Tickets 7–8 for “create from template” CTA parity.
|
||||
|
||||
**Goal:** Signed-in **profile** experience matching [Figma — Profile](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069): **Your CommunityRules** (list **own** published rules), **duplicate** / **delete** per rule, CTAs into create flow (custom + from template), **logout** (existing API), **delete account** (policy + API + confirmation UX).
|
||||
|
||||
**Out of scope for this ticket**
|
||||
|
||||
- **Change your account email** (shown in Figma options): **deferred**—no backend in this slice. Product may **hide** the row, show **“Coming soon,”** or backlog until a **future ticket** (verified email change, conflicts, sessions).
|
||||
- **`displayName` / new `User` fields:** not required—use **static** welcome copy, generic greeting, or **email local-part in UI only** until a later schema/product decision.
|
||||
|
||||
**Context:** Today `GET /api/rules` is a **public** list of all published rules; there is no authenticated **my rules** endpoint, no owner **DELETE** / **duplicate**, and no **delete user** API. See [docs/backend-roadmap.md](backend-roadmap.md) §1 “profile / account — not implemented yet” and §6.
|
||||
|
||||
**Implementation (sketch):**
|
||||
|
||||
1. **API:** Authenticated route(s) to list rules **where `userId` = session user**; owner-only `DELETE` (and duplicate via `POST` reuse or dedicated handler); `DELETE` user (or equivalent) with explicit Prisma policy—cascade vs orphan `PublishedRule` (today `onDelete: SetNull` on user) and cleanup of `Session` / `RuleDraft`.
|
||||
2. **UI:** Marketing route (e.g. `/profile`), rule cards (title, summary, artwork from `document` as needed), **IN PROGRESS** badge per roadmap §4 (derive from JSON / future `status` / UI-only).
|
||||
3. **Nav:** Link from header when signed in if design requires.
|
||||
4. **i18n** for strings; legal/product review for **delete account** copy.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [ ] Signed-in user sees **only their** published rules (not the global public list).
|
||||
- [ ] Duplicate and delete actions work for **owner** only; errors are clear.
|
||||
- [ ] Logout still works from profile context.
|
||||
- [ ] Delete account flow matches agreed policy and is confirmed in UI.
|
||||
- [ ] No verified **email change** shipped in this ticket; Figma row handled per product (hide/disabled/backlog).
|
||||
|
||||
**Files:** new `app/` routes and components, `app/api/rules/...` (or new segment handlers), [lib/create/api.ts](lib/create/api.ts) as needed, [prisma/schema.prisma](prisma/schema.prisma) only if account-delete policy requires schema tweaks, [messages/en/](messages/en/) for copy.
|
||||
|
||||
**Linear:** [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) (**Backlog**). **Blocked by** **CR-77** (publish) only — **CR-75** Done. **Related:** [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) (public rule detail for deep links from profile cards). **Not** part of the sequential **CR-72 → CR-83** chain—parallel after publish + session, similar to CR-84/CR-85.
|
||||
|
||||
---
|
||||
|
||||
## Summary order
|
||||
|
||||
| Order | Ticket | Short name |
|
||||
| ----: | ------ | --------------------------------- |
|
||||
| 1 | 1 | Refresh backend-roadmap |
|
||||
| 2 | 2 | CreateFlowState + API validation |
|
||||
| 3 | 3 | Magic-link sign-in UI |
|
||||
| 4 | 4 | Create flow session UI |
|
||||
| 5 | 5 | Draft sync hardening |
|
||||
| 6 | 6 | Publish wiring |
|
||||
| 7 | 7 | Template seed |
|
||||
| 8 | 8 | Templates in UI |
|
||||
| 9 | 9 | Web vitals persistence |
|
||||
| 10 | 10 | Public rule detail (optional) |
|
||||
| 11 | 11 | CI migrate smoke (optional) |
|
||||
| 12 | 12 | Ops runbook |
|
||||
| 13 | 13 | API errors + request-id logging |
|
||||
| 14 | 14 | Session lifecycle + cleanup |
|
||||
| 15 | 15 | Profile + account (Figma profile) |
|
||||
| 16 | 16 | Template matrix + xlsx ingestion |
|
||||
| 17 | 17 | Canon create-flow (custom path) |
|
||||
|
||||
Tickets **10–11** can be deferred without blocking the core “auth + drafts + publish + templates” vertical slice. **Ticket 16** is also **deferrable** until after **7–8** (flat template list + UI); it adds **spreadsheet-driven** recommendations and facet APIs. **Ticket 17** (**[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)**) canonizes the **custom** wizard in [`docs/create-flow.md`](create-flow.md) and tracks UX/code alignment (progress bar, resume URL, `[step]` cleanup); **parallel** to publish and templates. **Tickets 13–14** are parallel to that chain (**CR-73** / **CR-75** prerequisites are **Done** — **CR-84** / **CR-85** are unblocked), not sequential after CR-83. **Ticket 15** is also **parallel** (blocked by **publish (CR-77)** once session/auth are shipped); Linear: **CR-86**.
|
||||
|
||||
---
|
||||
|
||||
## Linear (Community-rule team)
|
||||
|
||||
**Main chain:** **CR-72 → CR-83** (each blocks the next). **Parallel:** **CR-84** (**CR-73** Done — ready to pick up), **CR-85** (**CR-75** Done — ready to pick up), **CR-86** / Ticket 15 (blocked by **CR-77** publish only; **CR-75** Done), **CR-88** / Ticket 16 (template matrix + `.xlsx` ingestion — after **CR-78**/**CR-79**), **CR-89** / Ticket 17 (canon create-flow + implementation gaps), not in the CR-72–83 sequence.
|
||||
|
||||
| Doc ticket | Linear | Title (short) |
|
||||
| ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| 1 | [CR-72](https://linear.app/community-rule/issue/CR-72/backend-align-docsbackend-roadmapmd-with-current-codebase) | Align backend-roadmap |
|
||||
| 2 | [CR-73](https://linear.app/community-rule/issue/CR-73/backend-formalize-createflowstate-validate-draftpublish-api-payloads) | CreateFlowState + API validation |
|
||||
| 3 | [CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done) | Magic-link sign-in UI (Ticket 3; Done) |
|
||||
| 4 | [CR-75](https://linear.app/community-rule/issue/CR-75/backend-create-flow-session-ui-sign-out-ticket-4-done) | Create flow session UI (Ticket 4; Done) |
|
||||
| 5 | [CR-76](https://linear.app/community-rule/issue/CR-76/backend-harden-server-draft-sync-save-and-exit-post-login-transfer) | Draft sync hardening (PUT UX / errors) |
|
||||
| 6 | [CR-77](https://linear.app/community-rule/issue/CR-77/backend-wire-publish-rule-from-create-flow-post-apirules) | Publish wiring |
|
||||
| 7 | [CR-78](https://linear.app/community-rule/issue/CR-78/backend-prisma-seed-ruletemplate-document) | Template seed |
|
||||
| 8 | [CR-79](https://linear.app/community-rule/issue/CR-79/backend-load-rule-templates-from-get-apitemplates-in-ui) | Templates in UI |
|
||||
| 9 | [CR-80](https://linear.app/community-rule/issue/CR-80/backend-persist-web-vitals-outside-next-db-or-external-rum) | Web vitals (prefer external) |
|
||||
| 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) |
|
||||
| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | CI migrate smoke (optional) |
|
||||
| 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops runbook / admin handoff |
|
||||
| 13 | [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) | API errors + request-id logging |
|
||||
| 14 | [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) | Session lifecycle + cleanup |
|
||||
| 15 | [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) | Profile + account (Figma 22143:900069) |
|
||||
| 16 | [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) | Template matrix + xlsx ingestion |
|
||||
| 17 | [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) | Canon create-flow (custom wizard + docs) |
|
||||
|
||||
---
|
||||
|
||||
## Linear sync notes (CR-74 / CR-75)
|
||||
|
||||
**[CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done)** and **[CR-75](https://linear.app/community-rule/issue/CR-75/backend-create-flow-session-ui-sign-out-ticket-4-done)** are kept in sync with **Ticket 3** / **Ticket 4** above (magic link, `AuthModalProvider`, anonymous draft + transfer, etc.). **Residual:** staging/prod `Host` / magic-link URL verification (per-environment).
|
||||
|
||||
To refresh other issues from this doc, use Linear MCP `save_issue` or paste the matching **Ticket N** section into the issue body.
|
||||
@@ -0,0 +1,262 @@
|
||||
# Backend roadmap (reference)
|
||||
|
||||
Temporary working notes for building the backend. Safe to delete once the stack is stable.
|
||||
|
||||
---
|
||||
|
||||
## 1. Where we are
|
||||
|
||||
- **Next.js 16** single repo ([`package.json`](package.json)).
|
||||
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/magic-link, session, drafts, rules, templates, web-vitals).
|
||||
- **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.).
|
||||
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per “Create rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-structure` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts).
|
||||
- **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts) still use **file-based** storage under `.next` (not suitable for multi-instance production).
|
||||
- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition.
|
||||
|
||||
### HTTP API (implemented in repo)
|
||||
|
||||
Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers live under `app/api/*/route.ts`.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ---------- | ------------------------------ | --------------------------------------------- |
|
||||
| GET | `/api/health` | Liveness / DB check |
|
||||
| GET | `/api/auth/session` | Current user or null |
|
||||
| POST | `/api/auth/magic-link/request` | Send sign-in link email |
|
||||
| GET | `/api/auth/magic-link/verify` | Validate token, set session cookie, redirect |
|
||||
| POST | `/api/auth/logout` | Clear session |
|
||||
| GET / PUT | `/api/drafts/me` | Load or save create-flow JSON (authenticated) |
|
||||
| GET / POST | `/api/rules` | List or publish rules |
|
||||
| GET | `/api/templates` | List curated templates |
|
||||
|
||||
**Product sign-in** uses **magic link** (`/api/auth/magic-link/*`).
|
||||
|
||||
**Also present (not in CONTRIBUTING table):** `POST` / `GET` [`/api/web-vitals`](../app/api/web-vitals/route.ts) — file-based store today; production path TBD (§7).
|
||||
|
||||
### HTTP API (profile / account — not implemented yet)
|
||||
|
||||
Planned for the signed-in profile/dashboard ([Figma profile frame](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069); [docs/backend-linear-tickets.md](backend-linear-tickets.md) Ticket 15; Linear **[CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile)**):
|
||||
|
||||
- Authenticated list of **own** `PublishedRule` rows (e.g. `GET /api/rules/me` or a strictly scoped query—**not** the same as public `GET /api/rules`).
|
||||
- Owner-only **delete** and **duplicate** (clone) for published rules.
|
||||
- **Delete account** (authenticated), with an explicit policy for drafts, sessions, and linked rules.
|
||||
|
||||
**Future (separate ticket):** **Change email** with verification (e.g. magic link to a new address, conflict handling)—**out of scope** for the profile milestone above.
|
||||
|
||||
---
|
||||
|
||||
## 2. What we’re building
|
||||
|
||||
**Step 1.** Treat this as **greenfield**: new **PostgreSQL** database and new tables. Do **not** migrate data from the old Community Rule backend.
|
||||
|
||||
**Step 2.** Keep the backend **inside this Next app**:
|
||||
|
||||
- HTTP handlers under `app/api/…`
|
||||
- Shared server code under `lib/server/…`
|
||||
|
||||
**Step 3.** Use the old backend only as a **product hint** (passwordless email sign-in, saving rules, listing rules). Do **not** copy its Express layout or MySQL schema.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stack choices
|
||||
|
||||
**Step 1.** Use **PostgreSQL** everywhere (local Docker, staging, production).
|
||||
|
||||
**Step 2.** Use **Prisma** — `schema.prisma`, `npx prisma migrate dev` / `migrate deploy`.
|
||||
|
||||
**Step 3.** Add **SMTP** (or Mailhog locally) for **magic-link** sign-in email in deployed environments; when `SMTP_URL` is unset in dev, the app can log the **verify URL** to the console (same pattern as [`lib/server/mail.ts`](lib/server/mail.ts)).
|
||||
|
||||
**Step 4.** **Redis / queues / Kubernetes** — not required for v1. **Exception:** before running **multiple app instances**, plan a **shared rate-limit store** (often Redis) for **passwordless email (magic-link request)**; the current limiter is in-memory per process ([`lib/server/rateLimit.ts`](lib/server/rateLimit.ts)).
|
||||
|
||||
---
|
||||
|
||||
## 4. Data to model (first pass)
|
||||
|
||||
Plain-English entities (names can evolve):
|
||||
|
||||
| Area | Purpose |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **User** | Identified by email after **magic link verification** (primary v1 path). An optional **display name** (or preferred name) could be added later for richer greetings; it does **not** block the profile page—no schema commitment in this roadmap pass alone. |
|
||||
| **Session** | **Custom v1:** HttpOnly cookie; opaque token; **hash** stored in DB ([`lib/server/session.ts`](lib/server/session.ts)). Not NextAuth/Lucia. |
|
||||
| **MagicLinkToken** | Short-lived **hashed** token for email sign-in links; optional `nextPath` for post-login redirect. |
|
||||
| **RuleDraft** | **One** JSON blob per user (create-flow state). Schema already has **`updatedAt`**; no draft **versioning** or **multiple named drafts** in v1. |
|
||||
| **PublishedRule** | Saved rule after publish (title, summary, document JSON). Profile UI badges such as **IN PROGRESS** may be **derived from `document` JSON**, a future `status` column, or UI-only—product decision when implementing Ticket 15. |
|
||||
| **RuleTemplate** | Curated templates (slug, category, ordering, `body` JSON). **v1 API** lists rows for cards / create entry; **not** yet a recommendation engine (see below). |
|
||||
|
||||
**RuleTemplate — recommendation matrix (after v1 list):** Product may author templates in **spreadsheets** (e.g. one row per governance pattern, columns for **matching dimensions** such as group size, organization type, location, maturity, plus long-form fields for create-flow prefill). That implies: **normalized schema or versioned JSON** for dimensions × template fit (✓/✗, weights, or scores), an **import path** (export `.xlsx` / Sheets → validate → DB or build-time artifact), and **`GET /api/templates` (or a sibling route)** that accepts **user- or wizard-selected facets** and returns a **ranked or filtered** set. **Out of scope for first ship** of Tickets 7–8 (seed + display list); tracked as **Ticket 16** in [docs/backend-linear-tickets.md](backend-linear-tickets.md) and Linear **[CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)**. Prefer **batch import** over live Google Sheets API in production unless ops explicitly wants sync.
|
||||
|
||||
**Session follow-ups to implement or decide:** token **rotation** on sensitive events, whether **new login invalidates other sessions**, and **cleanup** of expired `Session` rows (job or lazy delete). Revisit a small auth library (e.g. Auth.js, Lucia) only if maintaining custom code becomes costly.
|
||||
|
||||
**RuleDraft future (not v1):** versioning, multiple drafts per user, easier corruption recovery—only if product needs them.
|
||||
|
||||
Align JSON shapes with `app/(app)/create/types.ts` as it matures.
|
||||
|
||||
---
|
||||
|
||||
## 5. Session and authentication (v1)
|
||||
|
||||
- **Decision:** **Custom** database-backed sessions + **email magic link**; cookies are **httpOnly**; session and magic-link tokens are hashed at rest.
|
||||
- **Rate limiting (magic-link request):** **In-memory** is acceptable for a **single Node process**. It does **not** coordinate across instances—**add a shared limiter (e.g. Redis)** before horizontal scaling or serious abuse exposure.
|
||||
- Do **not** treat “switch to NextAuth/Lucia” as required for v1; document the custom lifecycle above instead.
|
||||
|
||||
---
|
||||
|
||||
## 6. Authorization (v1)
|
||||
|
||||
Match the current API behavior; tighten as product evolves:
|
||||
|
||||
- **`GET /api/drafts/me` / `PUT /api/drafts/me`:** Authenticated user only; draft is **scoped to that user** (`userId`).
|
||||
- **`POST /api/rules`:** Authenticated user only; rule is stored with **`userId`** (owner).
|
||||
- **`GET /api/rules`:** **Public list** of published rules (metadata: id, title, summary, timestamps)—no auth required today. **Not** a private “my rules” feed unless you add a separate route later (see §1 “profile / account — not implemented yet” and Ticket 15).
|
||||
- **Profile / owner scope (planned):** Authenticated **list own rules**, **delete own rule**, **duplicate own rule**—required for the signed-in dashboard in design; **v1 shipped handlers** may not include these until that work lands.
|
||||
- **Delete account (planned):** Authenticated endpoint + UX to remove the user record per policy (cascade vs orphan `PublishedRule`, drafts, sessions)—Ticket 15. **Change email** is **not** part of that milestone; plan a **future ticket** for verified email updates.
|
||||
- **v1 (shipped today):** No **editing** or **deleting** published rules via API in current handlers; no **sharing** or **collaborative ownership**—treat each rule as **owned by one user** until product defines more.
|
||||
|
||||
---
|
||||
|
||||
## 7. API responses, errors, and observability
|
||||
|
||||
**Error JSON (target):** Prefer a stable shape, e.g. `{ "error": { "code": "string", "message": "string" }, "details"?: ... }` for 4xx/5xx, instead of only `{ "error": "string" }`. Validation errors can map into `details`. Implement gradually in route handlers.
|
||||
|
||||
**Logging:** Use the shared [`lib/logger.ts`](../lib/logger.ts) where possible. Include a **request correlation id** (reuse `x-request-id` if present, else generate) on API routes and log it with errors so support can tie logs together.
|
||||
|
||||
**Metrics:** No vendor required for v1; optional later: request duration, error counts.
|
||||
|
||||
**Web vitals:** **Default** is **external** RUM or log drain (e.g. host analytics, Vercel Analytics, OpenTelemetry, SaaS APM)—keep **product Postgres** focused on product entities. Storing vitals in Postgres is an **explicit tradeoff** only if ops strongly wants a single datastore.
|
||||
|
||||
---
|
||||
|
||||
## 8. Prisma migrations policy
|
||||
|
||||
- **Never edit** migration files that have **already been applied** to **staging or production** (or any shared database). Fixing schema drift = **add a new migration**.
|
||||
- **Local dev:** `prisma migrate dev` creates migrations; **deployed envs:** `prisma migrate deploy` before serving new code that depends on the schema.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build order (implementation steps)
|
||||
|
||||
**Operator / local (always manual):** Steps 1–4 — env file, Docker Postgres, `npm ci`, `prisma migrate dev`, `npm run dev`.
|
||||
|
||||
**Backend behavior already in the repo:** Steps **5–10** match implemented Route Handlers and middleware (`lib/server/*`). **Step 11** (web vitals) is **not** production-ready (files under `.next`); treat as follow-up work aligned with §7.
|
||||
|
||||
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumption (flat list first), **canon create-flow alignment** (Ticket 17 / [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) — progress bar, resume URL, `[step]` cleanup; spec in [`docs/create-flow.md`](create-flow.md)), **spreadsheet-driven template recommendations** (Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) — after v1 templates), **profile / my rules dashboard** (Ticket 15)—see §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
|
||||
|
||||
---
|
||||
|
||||
**Step 1.** Copy `.env.example` to `.env`. Set `DATABASE_URL` and secrets (see file comments).
|
||||
|
||||
**Step 2.** Start Postgres locally:
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
**Step 3.** Install dependencies and apply migrations:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
**Step 4.** Run the app:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Step 5.** Confirm **health**: `GET /api/health` should return JSON.
|
||||
|
||||
**Step 6.** **Magic-link sign-in** (happy path):
|
||||
|
||||
1. `POST /api/auth/magic-link/request` with `{ "email": "you@example.com" }` (optional `"next"` for redirect after verify).
|
||||
2. Open the link from email, Mailhog, or **server logs** when `SMTP_URL` is unset (dev).
|
||||
3. Browser hits `GET /api/auth/magic-link/verify?token=...` (and optional `next=...`); response sets the session cookie and redirects.
|
||||
4. `GET /api/auth/session` should show your user in the same browser.
|
||||
|
||||
**Before wiring create-flow session UI:** Confirm the same browser that completed verify gets `user` from `GET /api/auth/session` (cookie + same-site). On **staging/production**, magic-link emails embed the app origin—misconfigured **`Host`** or TLS termination can produce broken links; align reverse proxy with the public site URL.
|
||||
|
||||
**Step 7.** **Drafts**: With a session, `GET /api/drafts/me` and `PUT /api/drafts/me` with `{ "payload": { ... } }` (create flow state object).
|
||||
|
||||
**Step 8.** **Publish**: `POST /api/rules` with `{ "title", "summary?", "document" }`.
|
||||
|
||||
**Step 9.** **Templates** (when ready): seed `RuleTemplate` rows; `GET /api/templates` is implemented.
|
||||
|
||||
**Step 10.** **Frontend draft sync:** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` in `.env` so **Save & Exit** and **post-login anonymous → account transfer** can **PUT** `/api/drafts/me`. Without sync, drafts are **not** written to the server (anonymous progress still lives in `localStorage` only).
|
||||
|
||||
**Step 11.** **Web vitals:** Move off `.next` files—**prefer an external analytics or logging pipeline** (see §7). Use Postgres for vitals only as a deliberate ops choice.
|
||||
|
||||
---
|
||||
|
||||
## 10. Security checklist
|
||||
|
||||
- **HTTPS** in staging/production; session cookie **Secure**.
|
||||
- **Rate-limit** magic-link **request** — in-memory OK for one instance; **shared store before multi-instance** (see §5).
|
||||
- **Hash** magic-link tokens and session tokens before storing; short **magic-link** TTL (align with implementation, e.g. 15 minutes).
|
||||
- **Secrets** only in env / secret store — never commit `.env` with real values.
|
||||
- **CORS:** prefer **same-origin** `/api/*`; if cross-origin, configure CORS and CSRF carefully.
|
||||
|
||||
---
|
||||
|
||||
## 11. Environments
|
||||
|
||||
| Environment | Purpose | Notes |
|
||||
| -------------- | ------------------------------- | --------------------------------------------------------------------- |
|
||||
| **Local** | Daily development | Docker Compose: Postgres + optional Mailhog (`docker compose up -d`). |
|
||||
| **Staging** | Rehearse deploys and migrations | Match prod as closely as possible; test SMTP. |
|
||||
| **Production** | Users | Backups, monitoring, migration job before traffic. |
|
||||
|
||||
**Optional QA:** Run automated tests against an **ephemeral** database in CI instead of maintaining a fourth long-lived server.
|
||||
|
||||
**Admin / infra (coordinate with whoever runs the server):**
|
||||
|
||||
1. TLS certificates and hostnames.
|
||||
2. PostgreSQL backups and restore drill.
|
||||
3. SMTP DNS (SPF, DKIM).
|
||||
4. Health check URL for reverse proxy (`/api/health`).
|
||||
5. Log retention and alerts for 5xx errors.
|
||||
|
||||
---
|
||||
|
||||
## 12. Frontend hook-up
|
||||
|
||||
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** users: when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**, the create layout may **hydrate** in-memory flow state from **`GET /api/drafts/me`** once per session ([`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx)), including conflict handling if anonymous storage also has data. Without sync, signed-in progress stays **in memory** until **Save & Exit** (no automatic server read on entry). **Canonical wizard step order, URLs, and Figma product stages** (**Create Community** → **Create Custom CommunityRule** → **Review and complete**) are documented in [`docs/create-flow.md`](create-flow.md). The route **`/create/review-template/[slug]`** is an **auxiliary** template preview (not a numbered wizard step); a **full create-from-template** path will likely be **separate route(s)** when defined. **Prefilling the wizard or landing on `final-review` from a template** is **not** shipped yet — see **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** / Ticket 17 in [docs/backend-linear-tickets.md](backend-linear-tickets.md).
|
||||
|
||||
**Step 2.** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` to enable **PUT** on **Save & Exit** and after **magic-link transfer** from the save-progress exit modal.
|
||||
|
||||
**Step 3.** Sign-in: **Log in** in the header opens the **modal** ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** is still used for verify **error** redirects and bookmarks. Flow: request magic link → open verify URL → session cookie → `GET /api/auth/session` / `/api/drafts/me` as needed.
|
||||
|
||||
**Step 4.** On publish, call `POST /api/rules` from the completed step when the backend is required (wire when the final review UI is ready).
|
||||
|
||||
**Step 5.** **Profile / dashboard** (`/profile` or agreed path): signed-in hub for **my rules** (after Ticket 15 APIs exist), **duplicate** / **delete** rule actions, **logout**, **delete account**—aligned with [Figma profile](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069). **Change email** in design is **deferred** (hide, “coming soon,” or backlog) until a future account ticket; greeting copy can stay **static** or use **email local-part in UI only**—no `displayName` field required for MVP.
|
||||
|
||||
**Step 6.** **Templates:** **Tickets 7–8** — seed `RuleTemplate` and load **`GET /api/templates`** in home / create surfaces (flat list, optional `featured`). **Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** — add **facet-based recommendations** and **spreadsheet ingestion** when product is ready (matrix rows + dimension columns like the decision-making workbook).
|
||||
|
||||
---
|
||||
|
||||
## 13. Optional later
|
||||
|
||||
- **Template recommendation matrix** + `.xlsx` / Sheets import pipeline — see **Ticket 16** / **[CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** (also §4 `RuleTemplate` note); not bundled into v1 template list work.
|
||||
- **Session library** spike (Auth.js, Lucia) if custom lifecycle cost grows.
|
||||
- **Redis** (or similar) for **shared magic-link rate limits** and horizontal scale.
|
||||
- **RuleDraft** versioning or multiple drafts per user.
|
||||
- Standalone **API service** (Fastify/Hono) if scaling or workers demand it.
|
||||
- **OpenAPI** if external API clients appear.
|
||||
- **Fourth environment** or stricter rate limiting at the edge.
|
||||
|
||||
---
|
||||
|
||||
## 14. Useful commands
|
||||
|
||||
| Command | When |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `npx prisma studio` | Inspect/edit DB locally. |
|
||||
| `npx prisma migrate dev` | After changing `schema.prisma` in development. |
|
||||
| `npx prisma migrate deploy` | Apply migrations in staging/production. |
|
||||
| `docker compose up -d postgres mailhog` | Local DB + mail UI (http://localhost:8025). |
|
||||
| `docker build -t community-rule .` | Optional production image (Next **standalone** + `node server.js`; see repo `Dockerfile`). |
|
||||
|
||||
---
|
||||
|
||||
## External reading
|
||||
|
||||
- [Docker: development best practices](https://docs.docker.com/develop/dev-best-practices/)
|
||||
- [Docker Compose in production](https://docs.docker.com/compose/how-tos/production/)
|
||||
@@ -1,401 +0,0 @@
|
||||
# Container/Presentation Pattern
|
||||
|
||||
## Overview
|
||||
|
||||
The Container/Presentation pattern separates component logic from presentation, improving testability, reusability, and maintainability. This pattern is now the standard for complex components in this codebase.
|
||||
|
||||
## Motivation
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Testability**: Pure presentation components can be tested independently with simple prop assertions
|
||||
- **Reusability**: Presentation components can be reused with different data sources or logic
|
||||
- **Maintainability**: Clear separation makes it easier to locate and modify specific concerns
|
||||
- **Performance**: Easier to optimize rendering with React.memo on pure components
|
||||
|
||||
### When to Use
|
||||
|
||||
Use this pattern for components that have:
|
||||
|
||||
- Business logic or state management
|
||||
- Data fetching or API calls
|
||||
- Analytics tracking
|
||||
- Complex event handlers
|
||||
- Custom hooks usage
|
||||
- Dynamic imports or side effects
|
||||
|
||||
Simple presentational components (e.g., `Button`, `Avatar`) can remain as single files.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
Each component following this pattern should have this structure:
|
||||
|
||||
```
|
||||
app/components/[ComponentName]/
|
||||
├── index.tsx # Exports container as default
|
||||
├── [ComponentName].container.tsx # Logic, hooks, state management
|
||||
├── [ComponentName].view.tsx # Pure presentation component
|
||||
└── [ComponentName].types.ts # Shared TypeScript types
|
||||
```
|
||||
|
||||
### File Responsibilities
|
||||
|
||||
#### `index.tsx`
|
||||
|
||||
- Exports the container component as the default export
|
||||
- Optionally exports types for external use
|
||||
- Maintains backward compatibility with existing import paths
|
||||
|
||||
```typescript
|
||||
export { default } from "./AskOrganizer.container";
|
||||
export type { AskOrganizerProps } from "./AskOrganizer.types";
|
||||
```
|
||||
|
||||
#### `[ComponentName].container.tsx`
|
||||
|
||||
**Contains all logic:**
|
||||
|
||||
- React hooks (`useState`, `useEffect`, custom hooks)
|
||||
- Event handlers and business logic
|
||||
- Data fetching and API calls
|
||||
- Analytics tracking
|
||||
- State management
|
||||
- Computed values and derived state
|
||||
- Side effects
|
||||
|
||||
**Should NOT contain:**
|
||||
|
||||
- JSX layout details (beyond composing the view)
|
||||
- Inline styles or complex className logic (pass as props)
|
||||
- Direct DOM manipulation
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useAnalytics } from "../../hooks";
|
||||
import { AskOrganizerView } from "./AskOrganizer.view";
|
||||
import type { AskOrganizerProps } from "./AskOrganizer.types";
|
||||
|
||||
function AskOrganizerContainer(props: AskOrganizerProps) {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const handleContactClick = () => {
|
||||
trackEvent({
|
||||
event: "contact_button_click",
|
||||
category: "engagement",
|
||||
component: "AskOrganizer",
|
||||
});
|
||||
// ... additional logic
|
||||
};
|
||||
|
||||
// Compute derived props
|
||||
const variantStyles = computeVariantStyles(props.variant);
|
||||
|
||||
return (
|
||||
<AskOrganizerView
|
||||
{...props}
|
||||
onContactClick={handleContactClick}
|
||||
variantStyles={variantStyles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(AskOrganizerContainer);
|
||||
```
|
||||
|
||||
#### `[ComponentName].view.tsx`
|
||||
|
||||
**Pure presentation:**
|
||||
|
||||
- Receives all data via props
|
||||
- Renders JSX based on props
|
||||
- No hooks, no state, no side effects
|
||||
- Only imports other presentational components
|
||||
|
||||
**Should NOT contain:**
|
||||
|
||||
- `useState`, `useEffect`, or any hooks
|
||||
- Event handler implementations (receive as callbacks)
|
||||
- Data fetching or API calls
|
||||
- Analytics tracking
|
||||
- Business logic
|
||||
|
||||
```typescript
|
||||
import ContentLockup from "../ContentLockup";
|
||||
import Button from "../Button";
|
||||
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
|
||||
|
||||
export function AskOrganizerView({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
variant,
|
||||
onContactClick,
|
||||
variantStyles,
|
||||
...props
|
||||
}: AskOrganizerViewProps) {
|
||||
return (
|
||||
<section className={variantStyles.container}>
|
||||
<ContentLockup
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
/>
|
||||
<Button
|
||||
href={buttonHref}
|
||||
onClick={onContactClick}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### `[ComponentName].types.ts`
|
||||
|
||||
- Shared TypeScript interfaces and types
|
||||
- Public props interface (used by consumers)
|
||||
- Internal view props (used between container and view)
|
||||
- Any utility types specific to the component
|
||||
|
||||
```typescript
|
||||
export interface AskOrganizerProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
buttonText?: string;
|
||||
buttonHref?: string;
|
||||
variant?: "centered" | "left-aligned" | "compact" | "inverse";
|
||||
onContactClick?: (data: ContactClickData) => void;
|
||||
}
|
||||
|
||||
export interface AskOrganizerViewProps extends AskOrganizerProps {
|
||||
onContactClick: () => void;
|
||||
variantStyles: {
|
||||
container: string;
|
||||
buttonContainer: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Rules of Thumb
|
||||
|
||||
### Container Components
|
||||
|
||||
✅ **DO:**
|
||||
|
||||
- Use React hooks (`useState`, `useEffect`, custom hooks)
|
||||
- Handle all event handlers and business logic
|
||||
- Fetch data and manage loading states
|
||||
- Track analytics events
|
||||
- Compute derived values from props/state
|
||||
- Compose the view component with computed props
|
||||
|
||||
❌ **DON'T:**
|
||||
|
||||
- Include complex JSX layout (delegate to view)
|
||||
- Mix presentation logic with business logic
|
||||
- Access DOM directly (use refs when necessary)
|
||||
|
||||
### View Components
|
||||
|
||||
✅ **DO:**
|
||||
|
||||
- Receive all data via props
|
||||
- Render JSX based on props
|
||||
- Import only presentational components
|
||||
- Use simple conditional rendering
|
||||
- Accept callback props for user interactions
|
||||
|
||||
❌ **DON'T:**
|
||||
|
||||
- Use any React hooks
|
||||
- Manage state or side effects
|
||||
- Fetch data or make API calls
|
||||
- Track analytics directly
|
||||
- Implement business logic
|
||||
- Access browser APIs directly
|
||||
|
||||
## Example: AskOrganizer
|
||||
|
||||
### Before (Monolithic)
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useAnalytics } from "../hooks";
|
||||
import ContentLockup from "./ContentLockup";
|
||||
import Button from "./Button";
|
||||
|
||||
const AskOrganizer = memo(({ title, variant, ...props }) => {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const handleContactClick = () => {
|
||||
trackEvent({ event: "contact_click", component: "AskOrganizer" });
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ContentLockup title={title} />
|
||||
<Button onClick={handleContactClick}>Ask an organizer</Button>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### After (Container/Presentation)
|
||||
|
||||
**AskOrganizer.container.tsx:**
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useAnalytics } from "../../hooks";
|
||||
import { AskOrganizerView } from "./AskOrganizer.view";
|
||||
import type { AskOrganizerProps } from "./AskOrganizer.types";
|
||||
|
||||
function AskOrganizerContainer(props: AskOrganizerProps) {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const handleContactClick = () => {
|
||||
trackEvent({ event: "contact_click", component: "AskOrganizer" });
|
||||
};
|
||||
|
||||
return <AskOrganizerView {...props} onContactClick={handleContactClick} />;
|
||||
}
|
||||
|
||||
export default memo(AskOrganizerContainer);
|
||||
```
|
||||
|
||||
**AskOrganizer.view.tsx:**
|
||||
|
||||
```typescript
|
||||
import ContentLockup from "../ContentLockup";
|
||||
import Button from "../Button";
|
||||
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
|
||||
|
||||
export function AskOrganizerView({
|
||||
title,
|
||||
onContactClick,
|
||||
...props
|
||||
}: AskOrganizerViewProps) {
|
||||
return (
|
||||
<section>
|
||||
<ContentLockup title={title} />
|
||||
<Button onClick={onContactClick}>Ask an organizer</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When converting an existing component to this pattern:
|
||||
|
||||
- [ ] **Identify separation points**
|
||||
- [ ] List all hooks and state management
|
||||
- [ ] List all event handlers and business logic
|
||||
- [ ] List all data fetching and side effects
|
||||
- [ ] Identify pure presentation JSX
|
||||
|
||||
- [ ] **Create folder structure**
|
||||
- [ ] Create `[ComponentName]/` folder
|
||||
- [ ] Create `[ComponentName].types.ts` with shared types
|
||||
- [ ] Create `[ComponentName].view.tsx` with pure presentation
|
||||
- [ ] Create `[ComponentName].container.tsx` with all logic
|
||||
- [ ] Create `index.tsx` exporting container
|
||||
|
||||
- [ ] **Extract types**
|
||||
- [ ] Move component props interface to `types.ts`
|
||||
- [ ] Create view props interface extending container props
|
||||
- [ ] Export types from `index.tsx` for external use
|
||||
|
||||
- [ ] **Move presentation to view**
|
||||
- [ ] Copy JSX to view component
|
||||
- [ ] Remove all hooks and state
|
||||
- [ ] Replace event handlers with callback props
|
||||
- [ ] Replace computed values with props
|
||||
- [ ] Ensure view is a pure function
|
||||
|
||||
- [ ] **Move logic to container**
|
||||
- [ ] Move all hooks to container
|
||||
- [ ] Move event handlers to container
|
||||
- [ ] Move data fetching to container
|
||||
- [ ] Compute derived props in container
|
||||
- [ ] Render view component with computed props
|
||||
|
||||
- [ ] **Update exports**
|
||||
- [ ] Export container as default from `index.tsx`
|
||||
- [ ] Export types from `index.tsx`
|
||||
- [ ] Delete original component file
|
||||
- [ ] Verify import paths still work
|
||||
|
||||
- [ ] **Update tests**
|
||||
- [ ] Verify tests still pass (imports should resolve automatically)
|
||||
- [ ] Update any tests that relied on implementation details
|
||||
- [ ] Add tests for view component with mocked props if needed
|
||||
|
||||
- [ ] **Update Storybook**
|
||||
- [ ] Verify stories still work (imports should resolve automatically)
|
||||
- [ ] Optionally add view-only stories with mocked props
|
||||
|
||||
## Refactored Components
|
||||
|
||||
The following components have been refactored to use this pattern:
|
||||
|
||||
- ✅ **AskOrganizer** - Analytics tracking and event handlers
|
||||
- ✅ **NumberedCards** - Schema generation with `useSchemaData` hook
|
||||
- ✅ **FeatureGrid** - Memoized feature data structures
|
||||
- ✅ **WebVitalsDashboard** - Dynamic imports, data fetching, complex state
|
||||
- ✅ **Select** - Complex form state management with refs and keyboard navigation
|
||||
|
||||
These components serve as reference implementations for the pattern.
|
||||
|
||||
## Remaining Components
|
||||
|
||||
The following components are candidates for future conversion:
|
||||
|
||||
### High Priority (Complex Logic)
|
||||
|
||||
- `Header` / `HomeHeader` - Navigation state, conditional rendering logic
|
||||
- `MenuBar` - Menu state management, keyboard navigation
|
||||
- `ContextMenu` - Positioning logic, click outside handling
|
||||
- `RadioGroup` - Group state management
|
||||
- `ToggleGroup` - Group state management
|
||||
|
||||
### Medium Priority (Some Logic)
|
||||
|
||||
- `ContentContainer` - Data fetching or transformation
|
||||
- `RelatedArticles` - Data fetching, filtering logic
|
||||
- `RuleStack` - Complex rendering logic
|
||||
- `LogoWall` - Animation or interaction logic
|
||||
|
||||
### Low Priority (Mostly Presentational)
|
||||
|
||||
- `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components
|
||||
- `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation
|
||||
- `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with complex components** - Components with the most logic benefit most from separation
|
||||
2. **Keep it simple** - Don't over-engineer simple presentational components
|
||||
3. **Maintain backward compatibility** - Import paths should remain unchanged
|
||||
4. **Test both layers** - Test container for logic, view for presentation
|
||||
5. **Document the pattern** - Add comments explaining non-obvious prop flows
|
||||
6. **Use TypeScript strictly** - Leverage types to enforce the separation
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [React Container/Presenter Pattern](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
|
||||
- [Separation of Concerns in React](https://react.dev/learn/thinking-in-react)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: April 2025
|
||||
**Maintained by**: CommunityRule Development Team
|
||||
@@ -1,382 +1,81 @@
|
||||
# i18n Translation Workflow Guide
|
||||
# Translations & UI copy workflow
|
||||
|
||||
This guide explains how to work with translations in the CommunityRule application. The app uses a hybrid approach combining globalized, shared UI elements with context-aware, localized content pages, making it easy for content creators and contributors to update text without modifying component code.
|
||||
This guide is for **content editors** updating user-visible text. The
|
||||
implementation contract (file layout, `useMessages` access pattern, key
|
||||
casing rules) lives in `.cursor/rules/localization.mdc`.
|
||||
|
||||
## Overview
|
||||
## Where copy lives
|
||||
|
||||
All UI text is stored in JSON files under `messages/en/`. The structure follows best practices:
|
||||
All UI text is JSON under `messages/en/`:
|
||||
|
||||
- **Page-specific content** lives in `pages/` (varies by page context)
|
||||
- **Component defaults** live in `components/` (shared across pages)
|
||||
- **Common strings** live in `common.json` (shared UI elements)
|
||||
|
||||
Components reference these translations using keys, allowing content to be edited independently of the codebase.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
messages/
|
||||
en/
|
||||
pages/
|
||||
home.json # Home page specific content
|
||||
learn.json # Learn page specific content
|
||||
components/
|
||||
heroBanner.json # Component defaults (aria-labels, alt texts)
|
||||
numberedCards.json # Component defaults
|
||||
askOrganizer.json # Component defaults
|
||||
featureGrid.json # Component defaults
|
||||
footer.json # Shared across pages
|
||||
header.json # Shared across pages
|
||||
common.json # Shared UI strings (buttons, links, labels)
|
||||
navigation.json # Navigation items
|
||||
metadata.json # Page metadata (title, description)
|
||||
index.ts # Exports all messages
|
||||
```text
|
||||
messages/en/
|
||||
common.json # shared strings (buttons, links, generic labels)
|
||||
navigation.json # site nav items
|
||||
metadata.json # page <title> / description
|
||||
pages/<slug>.json # one file per page (home, learn, …)
|
||||
components/<name>.json # one file per shared component default
|
||||
create/<step>.json # one file per create-flow step
|
||||
index.ts # wires all bundles together
|
||||
```
|
||||
|
||||
## When to Use `pages/` vs `components/`
|
||||
The split is intentional:
|
||||
|
||||
### Use `pages/` for:
|
||||
- **`pages/`** — copy that varies by page context (titles, hero subheads).
|
||||
- **`components/`** — defaults that ride along with a component on every
|
||||
page (aria-labels, alt text patterns).
|
||||
- **`common.json`** — strings reused across many components (e.g. "Cancel",
|
||||
"Learn more").
|
||||
- **`create/`** — wizard step copy (mirrors the `screenId`).
|
||||
|
||||
- **Page-specific content**: Titles, subtitles, descriptions that vary by page
|
||||
- **Context-aware text**: Content that changes based on where the component is used
|
||||
- **User-facing content**: All text that users see on a specific page
|
||||
## Editing existing copy
|
||||
|
||||
**Example:** The home page hero banner title "Collaborate" goes in `pages/home.json`, not `components/heroBanner.json`
|
||||
1. Find the bundle: search `messages/en/` for the existing English string.
|
||||
2. Edit the value. Leave the key alone.
|
||||
3. Save and run `npm run dev` — text updates on reload.
|
||||
|
||||
### Use `components/` for:
|
||||
If you can't find the string, it may still be hard-coded. Open an issue or
|
||||
ping a developer; do not change the component file directly.
|
||||
|
||||
- **Component defaults**: Aria-labels, alt text patterns, shared behavior text
|
||||
- **Shared across pages**: Text that doesn't vary by page context
|
||||
- **Accessibility text**: Aria-labels and alt texts that are component-level
|
||||
## Adding a new key
|
||||
|
||||
**Example:** The hero banner image alt text "Hero illustration" stays in `components/heroBanner.json` because it's the same across all pages
|
||||
|
||||
### Use `common.json` for:
|
||||
|
||||
- **Shared UI strings**: Buttons, links, labels used across multiple components
|
||||
- **Global strings**: Text that appears in many places
|
||||
|
||||
## Page-Specific Translations
|
||||
|
||||
For page-specific content, use the `pages.*` namespace pattern:
|
||||
|
||||
**Server Components:**
|
||||
|
||||
```typescript
|
||||
import messages from "../../messages/en/index";
|
||||
import { getTranslation } from "../../lib/i18n/getTranslation";
|
||||
|
||||
export default function LearnPage() {
|
||||
const t = (key: string) => getTranslation(messages, key);
|
||||
|
||||
const contentLockupData = {
|
||||
title: t("pages.learn.contentLockup.title"),
|
||||
subtitle: t("pages.learn.contentLockup.subtitle"),
|
||||
};
|
||||
|
||||
return <ContentLockup {...contentLockupData} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Client Components:**
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
|
||||
export default function MyComponent() {
|
||||
const t = useTranslation("pages.home.heroBanner");
|
||||
return <h1>{t("title")}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Translation Keys
|
||||
|
||||
### 1. Identify the Component
|
||||
|
||||
Determine which component needs the translation. If it's a shared string (like a button label), add it to `common.json`. Otherwise, add it to the component-specific file.
|
||||
|
||||
### 2. Add the Key to the JSON File
|
||||
|
||||
Open the appropriate JSON file and add your translation key. Use descriptive, semantic keys:
|
||||
|
||||
**Good:**
|
||||
1. Decide which bundle owns the copy (page vs component vs common).
|
||||
2. Add a descriptive camelCase key. Group related copy in nested objects.
|
||||
3. If you created a new JSON file, register it in `messages/en/index.ts`
|
||||
(a developer will help if you're unsure).
|
||||
4. Reference the key from the component using `useMessages()` (see the
|
||||
localization rule for the snippet).
|
||||
|
||||
```json
|
||||
{
|
||||
"heroBanner": {
|
||||
"title": "Collaborate",
|
||||
"subtitle": "with clarity"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
|
||||
```json
|
||||
{
|
||||
"text1": "Collaborate",
|
||||
"text2": "with clarity"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Nested Objects for Organization
|
||||
|
||||
Group related translations together:
|
||||
|
||||
```json
|
||||
{
|
||||
"numberedCards": {
|
||||
"title": "How CommunityRule works",
|
||||
"buttons": {
|
||||
"createCommunityRule": "Create CommunityRule",
|
||||
"seeHowItWorks": "See how it works"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update the Component or Page
|
||||
|
||||
**For Page Components (Server Components):**
|
||||
|
||||
```typescript
|
||||
import messages from "../../messages/en/index";
|
||||
import { getTranslation } from "../../lib/i18n/getTranslation";
|
||||
|
||||
export default function MyPage() {
|
||||
const t = (key: string) => getTranslation(messages, key);
|
||||
|
||||
// Use page-specific keys
|
||||
const data = {
|
||||
title: t("pages.home.heroBanner.title"),
|
||||
subtitle: t("pages.home.heroBanner.subtitle"),
|
||||
};
|
||||
|
||||
return <HeroBanner {...data} />;
|
||||
}
|
||||
```
|
||||
|
||||
**For Client Components:**
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
|
||||
export default function MyComponent() {
|
||||
// For page-specific content
|
||||
const t = useTranslation("pages.home.heroBanner");
|
||||
return <h1>{t("title")}</h1>;
|
||||
|
||||
// For component defaults
|
||||
const tDefault = useTranslation("heroBanner");
|
||||
return <img alt={tDefault("imageAlt")} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Translation Key Naming Conventions
|
||||
|
||||
1. **Use camelCase** for keys: `buttonText`, `ariaLabel`
|
||||
2. **Use descriptive names**: `createCommunityRule` not `btn1`
|
||||
3. **Group by component**: Each component has its own namespace
|
||||
4. **Use nested objects** for related strings: `buttons.createCommunityRule`
|
||||
5. **Include context in comments**: Use `_comment` fields for clarity
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"_comment": "HeroBanner component translations",
|
||||
"title": "Collaborate",
|
||||
"subtitle": "with clarity",
|
||||
"description": "Help your community make important decisions..."
|
||||
}
|
||||
```
|
||||
|
||||
## Extracting Strings from Components
|
||||
|
||||
When migrating a component to use translations:
|
||||
|
||||
1. **Identify hardcoded strings** in the component
|
||||
2. **Create translation keys** in the appropriate JSON file
|
||||
3. **Replace hardcoded strings** with `t("key.path")` calls
|
||||
4. **Test the component** to ensure translations load correctly
|
||||
|
||||
### Example Migration
|
||||
|
||||
**Before:**
|
||||
|
||||
```typescript
|
||||
export default function HeroBanner() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Collaborate</h1>
|
||||
<p>with clarity</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function HeroBanner() {
|
||||
const t = useTranslations("heroBanner");
|
||||
return (
|
||||
<div>
|
||||
<h1>{t("title")}</h1>
|
||||
<p>{t("subtitle")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Adding a New Page
|
||||
|
||||
When creating a new page that needs translations:
|
||||
|
||||
1. **Create a page translation file**: `messages/en/pages/about.json` (for example)
|
||||
2. **Add page-specific content**: All user-facing text for that page
|
||||
3. **Import in index.ts**: Add the import and export in `messages/en/index.ts`
|
||||
4. **Use in page component**: Use `t("pages.about.*")` pattern in your page
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// messages/en/pages/about.json
|
||||
{
|
||||
"_comment": "About page hero copy",
|
||||
"hero": {
|
||||
"title": "About Us",
|
||||
"subtitle": "Learn more about our mission"
|
||||
}
|
||||
}
|
||||
|
||||
// app/about/page.tsx
|
||||
const t = (key: string) => getTranslation(messages, key);
|
||||
const title = t("pages.about.hero.title");
|
||||
```
|
||||
|
||||
## Adding New Languages (Future)
|
||||
|
||||
When adding support for a new language:
|
||||
|
||||
1. **Create a new locale directory**: `messages/es/` (for Spanish, for example)
|
||||
2. **Copy the English files** as a starting point (including `pages/` structure)
|
||||
3. **Translate all strings** in the JSON files
|
||||
4. **Test thoroughly** to ensure all translations are present
|
||||
|
||||
## Testing Translations
|
||||
|
||||
1. **Check for missing keys**: Ensure all translation keys used in components exist in the JSON files
|
||||
2. **Verify type safety**: TypeScript will catch typos in translation keys at compile time
|
||||
3. **Test in browser**: Run the dev server and verify text displays correctly
|
||||
4. **Check for fallbacks**: Missing translations will show the key path (e.g., `heroBanner.title`)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Content Creators
|
||||
|
||||
- **Edit JSON files directly**: No need to understand React or TypeScript
|
||||
- **Use descriptive comments**: Add `_comment` fields to explain context
|
||||
- **Maintain consistency**: Use the same terminology across components
|
||||
- **Test changes**: Run the dev server to see your changes immediately
|
||||
|
||||
### For Developers
|
||||
|
||||
- **Use TypeScript**: Translation keys are type-safe
|
||||
- **Namespace when possible**: Use `useTranslations("namespace")` for better organization
|
||||
- **Server components first**: Prefer server-side translations for better performance
|
||||
- **Extract incrementally**: Migrate components one at a time
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Buttons and CTAs
|
||||
|
||||
```json
|
||||
{
|
||||
"buttons": {
|
||||
"createCommunityRule": "Create CommunityRule",
|
||||
"seeHowItWorks": "See how it works"
|
||||
"title": "About us",
|
||||
"subtitle": "Why CommunityRule exists"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Aria Labels
|
||||
## Style notes
|
||||
|
||||
```json
|
||||
{
|
||||
"ariaLabels": {
|
||||
"followBluesky": "Follow us on Bluesky",
|
||||
"followGitlab": "Follow us on GitLab"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **camelCase** for structural keys (`compactTitle`, `imageAlt`).
|
||||
- **kebab-case** for content ids that match a URL slug, card id, or step
|
||||
id (`"in-person-meetings"`, `"peer-mediation"`).
|
||||
- Use `_comment` to leave context for the next editor.
|
||||
- Keep terminology consistent — search the messages folder before coining a
|
||||
new label.
|
||||
|
||||
### Dynamic Content
|
||||
## Adding a new language (future)
|
||||
|
||||
For content that varies (like card text), use arrays or numbered keys:
|
||||
|
||||
```json
|
||||
{
|
||||
"cards": {
|
||||
"card1": { "text": "First step" },
|
||||
"card2": { "text": "Second step" },
|
||||
"card3": { "text": "Third step" }
|
||||
}
|
||||
}
|
||||
```
|
||||
1. Create `messages/<locale>/` mirroring `messages/en/`.
|
||||
2. Translate strings; keep keys identical.
|
||||
3. Update `messages/en/index.ts` (or split it per locale) — a developer
|
||||
will wire the locale switcher.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Translation Key Not Found
|
||||
|
||||
If you see a key path like `heroBanner.title` instead of the text:
|
||||
|
||||
1. Check the JSON file exists and has the key
|
||||
2. Verify the key path matches exactly (case-sensitive)
|
||||
3. Restart the dev server if you just added the key
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
If TypeScript complains about translation keys:
|
||||
|
||||
1. Ensure the key exists in the JSON file
|
||||
2. Check for typos in the key path
|
||||
3. Verify the namespace is correct if using `useTranslations("namespace")`
|
||||
|
||||
### Missing Translations
|
||||
|
||||
If text doesn't appear:
|
||||
|
||||
1. Check the browser console for errors
|
||||
2. Verify the component is wrapped in `MessagesProvider` (for client components)
|
||||
3. Ensure `getTranslation()` is called correctly in server components
|
||||
4. Check if you're using the correct namespace (`pages.*` vs component defaults)
|
||||
|
||||
## Architecture: Hybrid Approach
|
||||
|
||||
This implementation follows the recognized best practice of combining:
|
||||
|
||||
- **Globalized, shared UI elements**: Component defaults in `components/` (aria-labels, alt texts)
|
||||
- **Context-aware, localized content pages**: Page-specific content in `pages/` (titles, descriptions)
|
||||
|
||||
This allows:
|
||||
|
||||
- Components to remain flexible and reusable
|
||||
- Page content to be easily edited without code changes
|
||||
- Clear separation between shared defaults and page-specific content
|
||||
- Scalable structure for adding new pages
|
||||
|
||||
## Resources
|
||||
|
||||
- Component defaults in `messages/en/components/`
|
||||
- Page-specific content in `messages/en/pages/`
|
||||
- Shared UI strings in `messages/en/common.json`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: January 2025
|
||||
**Maintained by**: CommunityRule Development Team
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| Key path renders instead of text (e.g. `hero.title`) | Missing key or typo | Check spelling and bundle path |
|
||||
| Copy doesn't update | Dev server cache | Restart `npm run dev` |
|
||||
| TypeScript red squiggle | Bundle not registered | Add the import in `messages/en/index.ts` |
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
# Template Recommendation Matrix — Implementation Context (CR-88)
|
||||
|
||||
**Status:** Draft / context doc. Reference only — not yet implemented.
|
||||
**Linear:** [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)
|
||||
**Roadmap:** [`docs/backend-roadmap.md`](backend-roadmap.md) §4 (`RuleTemplate`) and §13.
|
||||
**Spec ticket:** [`docs/backend-linear-tickets.md`](backend-linear-tickets.md) Ticket 16.
|
||||
|
||||
This doc consolidates the **four product-authored matrix spreadsheets** with the
|
||||
**existing data model, create-flow facets, and section structure** so we have a
|
||||
single reference while implementing the importer + recommendation API.
|
||||
|
||||
> **Scope note:** No data, API, or UI surface for this feature is in production
|
||||
> yet. **Backwards compatibility is not a constraint** — we will replace the
|
||||
> hand-typed `prisma/seed.ts` `COMPOSITION_BY_SLUG` map, the existing
|
||||
> `GET /api/templates` response shape, and the static `messages/en/create/*.json`
|
||||
> card decks where it makes the design cleaner.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal (one paragraph)
|
||||
|
||||
Replace the hand-curated `prisma/seed.ts` `COMPOSITION_BY_SLUG` map with a
|
||||
**spreadsheet-authored matrix** for each rule section
|
||||
(Communication, Membership, Decision-making, Conflict management — and later
|
||||
Values), where each row is a **method/pattern card** and each column is either
|
||||
**long-form copy that populates the card UI** or a **facet flag** (✓/x or score)
|
||||
that the recommendation engine uses to filter and rank cards based on the
|
||||
user's create-flow answers (community size, organization type, location/scale,
|
||||
maturity).
|
||||
|
||||
The same authoring contract should make it trivial for product to ship updated
|
||||
spreadsheets and have the create-flow card decks (and the home/templates page
|
||||
recommendations) update without any code changes.
|
||||
|
||||
---
|
||||
|
||||
## 2. The four spreadsheets
|
||||
|
||||
All four xlsx files share **the same column shape**: leading **content
|
||||
columns** + trailing **facet columns** (✓ / x cells). Sheet name is `Current`
|
||||
in every workbook.
|
||||
|
||||
### 2.1 Shared facet columns (last 19, identical across the four sheets)
|
||||
|
||||
Order is preserved here because the columns are positional in the sheets:
|
||||
|
||||
| # | Column header (xlsx) | Maps to wizard step / state field | Wizard chip label (`messages/en/create/...`) |
|
||||
|---|---|---|---|
|
||||
| 1 | `1 member` | `community-size` → `selectedCommunitySizeIds` (id `"1"`) | `1 member` |
|
||||
| 2 | `2-5 members` | id `"2"` | `2-5 members` |
|
||||
| 3 | `6-12 members` | id `"3"` | `6-12 members` |
|
||||
| 4 | `13-100 members` | id `"4"` | `13-100 members` |
|
||||
| 5 | `100-100,000 members` | id `"5"` | `100-100,000 members` |
|
||||
| 6 | `Organization Type:DAO` (or `DAO` in conflict/comms/membership) | `community-structure` → `selectedOrganizationTypeIds` (id `"6"` in `organizationTypes`) | `DAO` |
|
||||
| 7 | `Organization Type:For profit business` (or `For profit business`) | id `"5"` in `organizationTypes` | `For profit business` |
|
||||
| 8 | `Organization Type:Nonprofit` (or `Nonprofit`) | id `"4"` in `organizationTypes` | `Nonprofit` |
|
||||
| 9 | `Organization Type:Open source project` (or `Open source project`) | id `"3"` | `Open source project` |
|
||||
| 10 | `Organization Type:Mutual aid` (or `Mutual aid`) | id `"2"` | `Mutual aid` |
|
||||
| 11 | `Organization Type: Worker’s coop` (or `Worker’s coop`) | id `"1"` | `Worker’s coop` |
|
||||
| 12 | `Location: Global` (or `Global`) | `community-structure` → `selectedScaleIds` (id `"4"` in `scaleOptions`) | `Global` |
|
||||
| 13 | `Location: National` (or `National`) | id `"3"` | `National` |
|
||||
| 14 | `Location: Regional` (or `Regional`) | id `"2"` | `Regional` |
|
||||
| 15 | `Location: Local` (or `Local`) | id `"1"` | `Local` |
|
||||
| 16 | `Organizational Maturity: Early stage` (or `Early stage`) | `community-structure` → `selectedMaturityIds` (id `"1"` in `maturityOptions`) | `Early stage` |
|
||||
| 17 | `Organizational Maturity: Growth stage` (or `Growth stage`) | id `"2"` | `Growth stage` |
|
||||
| 18 | `Organizational Maturity: Established` (or `Established`) | id `"3"` | `Established` |
|
||||
| 19 | `Organizational Maturity: Enterprise` (or `Enterprise`) | id `"4"` | `Enterprise` |
|
||||
|
||||
**Important normalization rules (importer must enforce):**
|
||||
|
||||
- Decision-making prefixes columns with `Organization Type:`, `Location:`,
|
||||
`Organizational Maturity:`. The other three sheets drop the prefix. Importer
|
||||
should normalize to a single canonical key (e.g.
|
||||
`orgType.workersCoop`, `scale.local`, `maturity.earlyStage`, `size.6_12`).
|
||||
- Cell value semantics: `✓` → match, `x` (lowercase) → no match, blank → no
|
||||
match, numbers → optional weighted score (only `Decision-making.xlsx` row 32
|
||||
contains a non-symbol cell — `"Military, Corporations"` in the size column —
|
||||
see §2.4 data-quality issues).
|
||||
- Wizard chip ids are **positional 1..N** within each `messages/en/create/*`
|
||||
array (see `chipRowsFromLabels` in
|
||||
`app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` lines 49–57).
|
||||
The importer should emit a stable lookup table mapping
|
||||
`(facetGroup, label) → wizardChipId` so the recommendation engine can match
|
||||
a user's `selectedXxxIds` against the matrix without depending on label
|
||||
spelling.
|
||||
- Curly apostrophes appear in `Worker’s coop`. Compare on a normalized key,
|
||||
not on raw label.
|
||||
|
||||
### 2.2 Communication Methods (`Communication Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps 1:1 to `messages/en/create/communication.json` and the
|
||||
`communication-methods` step
|
||||
(`app/(app)/create/screens/card/CommunicationMethodsScreen.tsx`).
|
||||
|
||||
**Content columns (positions 1–5):**
|
||||
|
||||
| Sheet column | Card field |
|
||||
|---|---|
|
||||
| `Label` | `cards[<id>].label` and `modals[<id>].title` |
|
||||
| `Description` | `cards[<id>].supportText` and `modals[<id>].description` |
|
||||
| `Core Principle & Scope` | `modals[<id>].sections.corePrinciple` |
|
||||
| `Logistics, Admin & Norms` | `modals[<id>].sections.logisticsAdmin` |
|
||||
| `Code of Conduct` | `modals[<id>].sections.codeOfConduct` |
|
||||
|
||||
`SECTION_FIELDS = ["corePrinciple", "logisticsAdmin", "codeOfConduct"]` is
|
||||
the source of truth (`CommunicationMethodsScreen.tsx`).
|
||||
|
||||
**Card rows (11):** In-Person Meetings · Signal · Video Meetings · Loomio ·
|
||||
Matrix / Element · GitHub / GitLab · Discord · Email Distribution List · Slack
|
||||
· WhatsApp · Discourse (Forum).
|
||||
|
||||
### 2.3 Membership / Group-Membership (`Group_Membership_Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `membership-methods` step
|
||||
(`app/(app)/create/screens/card/MembershipMethodsScreen.tsx`) and
|
||||
`messages/en/create/membership.json`.
|
||||
|
||||
**Content columns (positions 1–5):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Label` | `cards[<id>].label` / modal title |
|
||||
| `Description` | `cards[<id>].supportText` / modal description |
|
||||
| `Eligibility & Philosophy` | modal section A (`eligibilityPhilosophy`) |
|
||||
| `Joining Process` | modal section B (`joiningProcess`) |
|
||||
| `Expectations & Removal` | modal section C (`expectationsRemoval`) |
|
||||
|
||||
**Card rows (19):** Open Access · Orientation Required · Invitation Only ·
|
||||
Contribution Based · Mentorship · Peer Sponsorship · Consensus or Vote-Based
|
||||
Approval · Trial Period / Provisional Membership · Referral System with
|
||||
Screening · Membership Agreement or Pledge · Weighted or Tiered Membership ·
|
||||
Hybrid Approval Process · Skill-Based Contribution · Pay-to-Join · Application
|
||||
& Review · Identity Verification · Collective Interviews · Skill-Based
|
||||
Evaluation · Lottery / Sortition.
|
||||
|
||||
> The wizard's existing `membership.json` modal section keys do not yet match
|
||||
> these. Since backwards compatibility is not a constraint, **rename the
|
||||
> wizard's section keys to match the matrix** (`eligibilityPhilosophy` /
|
||||
> `joiningProcess` / `expectationsRemoval`) when wiring this up — the existing
|
||||
> copy is placeholder.
|
||||
|
||||
### 2.4 Decision-making (`Decision-making.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `decision-approaches` step
|
||||
(`app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
|
||||
`messages/en/create/rightRail.json`.
|
||||
|
||||
**Content columns (positions 1–7):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Label` | card title |
|
||||
| `Description` | card support text |
|
||||
| `Core Principle` | modal section A (`corePrinciple`) |
|
||||
| `Applicable Scope` | modal section B (`applicableScope`) — free-text examples, e.g. `"Daily Operations, Minor Expenditures"` |
|
||||
| `Consensus Level` | numeric 0.0–1.0 stored under `scalars.consensusLevel` (e.g. `0.51`, `0.67`, `1.0`) — drives the **Consensus axis** in any future visual sort/filter |
|
||||
| `Step-by-Step Instructions` | modal section C (`stepByStep`) |
|
||||
| `Objections & Deadlocks` | modal section D (`objectionsDeadlocks`) |
|
||||
|
||||
**Card rows (32):** Lazy Consensus · Do-ocracy · Consensus Decision-Making ·
|
||||
Rotational Leadership · Modified Consensus · Consensus Seeking with Delegates
|
||||
· Sociocracy · Supermajority Rule · Ranked Choice Voting · Range Voting ·
|
||||
Majority Rule · Approval Voting · Weighted Voting · Cumulative Voting ·
|
||||
Quadratic Voting · Continuous Voting · Holacracy · Collaborative Platforms ·
|
||||
Deliberative Polling · Investor-Filled Board Seats · Elected Board of
|
||||
Directors · Advisory Committees · Delegated Decision-Making · Executive
|
||||
Committees · First Past the Post · Lottery/Sortition · Proof of Work · Random
|
||||
Choice · Algorithm-Driven Decisions · Autocratic Decision-Making ·
|
||||
Hierarchical Decision-Making · Negotiated Decisions.
|
||||
|
||||
**Data-quality issues to handle in the importer (do not silently drop):**
|
||||
|
||||
- Row 32 (`Hierarchical Decision-Making`): the `Consensus Level` cell contains
|
||||
`"Military, Corporations"` (the value clearly belongs to `Applicable Scope`,
|
||||
which itself already contains `"Military, Corporations"`). Importer should
|
||||
flag this as a validation error and require a fix in the source workbook
|
||||
rather than try to repair it.
|
||||
- Row 11 (`Range Voting`): the **last facet column** (`Maturity: Enterprise`)
|
||||
is empty in the source — treat empty as `x` (no match) **only after** the
|
||||
importer logs a warning so the author knows it wasn't intentional ✓.
|
||||
|
||||
### 2.5 Conflict Management (`Conflict Management Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `conflict-management` step
|
||||
(`app/(app)/create/screens/card/ConflictManagementScreen.tsx`) and
|
||||
`messages/en/create/conflictManagement.json`.
|
||||
|
||||
**Content columns (positions 1–6):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Title` | card title (note: not `Label` like the other three) |
|
||||
| `Description` | card support text |
|
||||
| `Core Principle` | modal section A (`corePrinciple`) |
|
||||
| `Applicable Scope` | modal section B (`applicableScope`) |
|
||||
| `Process Protocol` | modal section C (`processProtocol`) |
|
||||
| `Restoration & Fallbacks` | modal section D (`restorationFallbacks`) |
|
||||
|
||||
**Card rows (19):** Peer Mediation · Conflict Resolution Council · Facilitated
|
||||
Negotiation · Ad Hoc Arbitration · Conflict Workshops · Supermajority Vote ·
|
||||
Interest-Based Bargaining · Restorative Practices · Mediation · Circle
|
||||
Processes · Judicial Committees · Managerial Decision · Internal Tribunal ·
|
||||
Consensus Building · Binding Arbitration · Non-Binding Arbitration · Binding
|
||||
Contracts · Lottery/Sortition · Rotational Judging.
|
||||
|
||||
> Conflict Management sheet uses `Title` instead of `Label` and omits the
|
||||
> `Organization Type:` / `Location:` / `Organizational Maturity:` prefixes —
|
||||
> normalize both at import time.
|
||||
|
||||
---
|
||||
|
||||
## 3. Existing data model & wizard surface area
|
||||
|
||||
### 3.1 `RuleTemplate` (today)
|
||||
|
||||
```64:73:prisma/schema.prisma
|
||||
model RuleTemplate {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
title String
|
||||
category String?
|
||||
description String?
|
||||
body Json
|
||||
sortOrder Int @default(0)
|
||||
featured Boolean @default(false)
|
||||
}
|
||||
```
|
||||
|
||||
`body` JSON is the rendered rule document
|
||||
(`{ sections: [{ categoryName, entries: [{ title, body }] }, ...] }`),
|
||||
authored today by the `bodyFromXlsxComposition()` helper in
|
||||
`prisma/seed.ts` from a hand-typed `COMPOSITION_BY_SLUG` map.
|
||||
|
||||
**Section ordering (canonical):** Values → Communication → Membership →
|
||||
Decision-making → Conflict management. Final-review and `governancePatternBody`
|
||||
both rely on this exact order and casing.
|
||||
|
||||
```16:60:prisma/seed.ts
|
||||
function governancePatternBody(coreValues: string): Prisma.InputJsonValue {
|
||||
return {
|
||||
sections: [
|
||||
{ categoryName: "Values", entries: [{ title: "Core stance", body: coreValues }] },
|
||||
{ categoryName: "Communication", entries: [...] },
|
||||
{ categoryName: "Membership", entries: [...] },
|
||||
{ categoryName: "Decision-making", entries: [...] },
|
||||
{ categoryName: "Conflict management", entries: [...] },
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Wizard facets captured today (`CreateFlowState`)
|
||||
|
||||
```83:95:app/(app)/create/types.ts
|
||||
selectedCommunitySizeIds?: string[];
|
||||
selectedOrganizationTypeIds?: string[];
|
||||
selectedScaleIds?: string[];
|
||||
selectedMaturityIds?: string[];
|
||||
selectedCoreValueIds?: string[];
|
||||
selectedCommunicationMethodIds?: string[];
|
||||
selectedMembershipMethodIds?: string[];
|
||||
selectedDecisionApproachIds?: string[];
|
||||
selectedConflictManagementIds?: string[];
|
||||
```
|
||||
|
||||
The first four are exactly the four **facet groups** in the matrix sheets. The
|
||||
last four are the user's chosen **cards per section**, which the recommendation
|
||||
flow can either pre-select (when picked from a template) or feed back into
|
||||
ranking.
|
||||
|
||||
These same fields are validated server-side by `createFlowStateSchema` in
|
||||
`lib/server/validation/createFlowSchemas.ts` (lines 47–106) — the recommend
|
||||
endpoint should reuse that schema (or a strict subset) instead of redefining
|
||||
the facet shape.
|
||||
|
||||
### 3.3 Wizard step order
|
||||
|
||||
Source of truth is `app/(app)/create/utils/flowSteps.ts` (`FLOW_STEP_ORDER`). The
|
||||
relevant slice is:
|
||||
|
||||
```
|
||||
review → core-values → communication-methods → membership-methods →
|
||||
decision-approaches → conflict-management → confirm-stakeholders → final-review
|
||||
```
|
||||
|
||||
`docs/create-flow.md`'s step table is **stale**; trust `flowSteps.ts`.
|
||||
|
||||
### 3.4 Where templates already surface in the UI
|
||||
|
||||
| Surface | File |
|
||||
|---|---|
|
||||
| Marketing home "Popular templates" | `app/(marketing)/_components/MarketingRuleStackSection.tsx` |
|
||||
| Templates index | `app/(marketing)/templates/page.tsx` |
|
||||
| Template preview (by slug) | `app/(app)/create/review-template/[slug]/page.tsx` |
|
||||
| "Use without changes" → publish | `app/(app)/create/CreateFlowLayoutClient.tsx` `handleUseTemplateWithoutChanges` |
|
||||
| API list | `app/api/templates/route.ts` (GET only, no params) |
|
||||
|
||||
There is currently **no** recommendation logic, no facet filtering, and the
|
||||
`/create/informational?template=<slug>` query param is a known no-op (see
|
||||
`CreateFlowLayoutClient.tsx` lines 479–482).
|
||||
|
||||
---
|
||||
|
||||
## 4. Repo conventions to follow (don't reinvent)
|
||||
|
||||
These are the patterns the implementation must match. References point at the
|
||||
canonical example for each.
|
||||
|
||||
### 4.1 API routes (`app/api/**/route.ts`)
|
||||
|
||||
`app/api/drafts/me/route.ts` is the reference — every new route in this
|
||||
feature must match this exact shape:
|
||||
|
||||
1. `if (!isDatabaseConfigured()) return dbUnavailable();` — always first.
|
||||
(`lib/server/env.ts`, `lib/server/responses.ts`).
|
||||
2. For auth'd routes: `const user = await getSessionUser();` then
|
||||
`return NextResponse.json({ error: "Unauthorized" }, { status: 401 });` if
|
||||
missing. (Recommendation read endpoints can stay unauthenticated.)
|
||||
3. For request bodies: `readLimitedJson(request)` →
|
||||
`<schema>.safeParse(parsed.value)` → `jsonFromZodError(validated.error)` on
|
||||
failure. (`lib/server/validation/requestBody.ts`,
|
||||
`lib/server/validation/zodHttp.ts`).
|
||||
4. Success: `NextResponse.json({ <key>: data })` — flat object with one or two
|
||||
named keys, no `success: true` envelope.
|
||||
5. Errors: structured `{ error: { code, message } }` (Zod path) or simple
|
||||
`{ error: "..." }` (auth path). Match what's already in the repo.
|
||||
6. Server-side query helpers swallow Prisma failures and return `[]`/`null`
|
||||
(see `listRuleTemplatesFromDb` in `lib/server/ruleTemplates.ts` lines 9–30).
|
||||
Routes do **not** wrap helper calls in `try/catch`.
|
||||
|
||||
### 4.2 Zod schemas live in `lib/server/validation/`
|
||||
|
||||
- One file per feature area (e.g. `createFlowSchemas.ts`, future
|
||||
`templateRecommendationSchemas.ts`).
|
||||
- Export the schema **and** the inferred type
|
||||
(`export type X = z.infer<typeof xSchema>`).
|
||||
- Wrap any free-form JSON blobs with `assertPlainJsonValue` /
|
||||
`DEFAULT_PLAIN_JSON_LIMITS` (`lib/server/validation/plainJson.ts`) so the
|
||||
size/depth bounds match the rest of the API.
|
||||
- Reuse `FLOW_STEP_ORDER` and existing array bounds where they overlap (see
|
||||
the `selectedXxxMethodIds` arrays in `createFlowStateSchema`).
|
||||
|
||||
### 4.3 Prisma access
|
||||
|
||||
- Singleton: `import { prisma } from "lib/server/db";` — never
|
||||
`new PrismaClient()` from app code. (Standalone scripts under `scripts/` /
|
||||
`prisma/` may instantiate their own, matching `prisma/seed.ts` lines
|
||||
363–403.)
|
||||
- Server-only "fetch/list" helpers live under `lib/server/<feature>.ts`,
|
||||
return DTOs (not raw Prisma rows), and degrade gracefully
|
||||
(`isDatabaseConfigured()` short-circuit, `try/catch` → empty result).
|
||||
- No `$transaction` patterns exist yet; **introduce one** for the importer
|
||||
(write `TemplateMethod` + `TemplateMethodFacet` rows atomically).
|
||||
|
||||
### 4.4 DTO style
|
||||
|
||||
- Hand-written `type` aliases that mirror a Prisma `select` clause, co-located
|
||||
with the consumer (see `RuleTemplateDto` in
|
||||
`lib/create/fetchTemplates.ts` lines 5–14).
|
||||
- For a feature with both client and server consumers, put the type in
|
||||
`lib/<feature>/types.ts` and import from both sides.
|
||||
|
||||
### 4.5 Standalone scripts
|
||||
|
||||
- Use `tsx` (already a dev dep; entry point `package.json` `prisma.seed`
|
||||
field).
|
||||
- Layout matches `prisma/seed.ts`: `async function main()`, log a one-line
|
||||
success summary, `console.error(e); process.exit(1)` on failure,
|
||||
`await prisma.$disconnect()` in `finally`.
|
||||
- Add an entry to `package.json` `scripts` (e.g.
|
||||
`"templates:import": "tsx scripts/import-templates-xlsx.ts"`).
|
||||
- No shared dotenv loader — rely on env from the shell / Next runtime.
|
||||
- Support a `--dry-run` flag that validates + diffs without writing.
|
||||
|
||||
### 4.6 Tests
|
||||
|
||||
- Vitest under `tests/unit/*.test.ts` for parsers / validators / pure
|
||||
functions (see `tests/unit/createFlowValidation.test.ts`).
|
||||
- API routes are not unit-tested today; cover route behavior indirectly with a
|
||||
`tests/unit/templateRecommendationSchemas.test.ts` (Zod) plus a fixture
|
||||
workbook + importer test under `tests/unit/importTemplatesXlsx.test.ts`.
|
||||
- E2E for the wizard (if needed) goes under `tests/e2e/*.spec.ts` — not
|
||||
required for CR-88 acceptance.
|
||||
- Test utilities: `tests/utils/test-utils.tsx` (`renderWithProviders`); MSW
|
||||
server in `tests/msw/server.ts`. No Prisma mock helper exists; importer test
|
||||
should use a fixture workbook and stub the `prisma` client at the import
|
||||
site.
|
||||
|
||||
### 4.7 Logging
|
||||
|
||||
- Use `logger` from `lib/logger.ts` for any server-side info/warn/error in
|
||||
scripts and route helpers (matches `app/api/auth/magic-link/request/route.ts`
|
||||
lines 14–15, 35–45). No `apiError` helper exists; do not introduce one.
|
||||
|
||||
### 4.8 New deps
|
||||
|
||||
- `xlsx` (SheetJS) is **not** currently in `package.json`. Add it as a
|
||||
**prod** dep only if the importer is invoked from app code; if the importer
|
||||
is script-only, `devDependency` is fine. CR-88's plan calls for a
|
||||
build/CLI-time importer, so `devDependencies` is the right home.
|
||||
|
||||
### 4.9 i18n / `messages/` constraint
|
||||
|
||||
- Card decks and modal copy are currently keyed in
|
||||
`messages/en/<feature>.json` and read via
|
||||
`useMessages().create.<feature>` (`app/contexts/MessagesContext.tsx`,
|
||||
`messages/en/index.ts`).
|
||||
- Only `en` is wired today, so we **don't** have a translation backlog
|
||||
blocking us. The wiring step (§7) replaces `messages/en/create/{communication,
|
||||
membership,rightRail,conflictManagement}.json` card/modal payloads with
|
||||
values served by `GET /api/template-methods` (still keyed by the same
|
||||
message namespace shape so future i18n can layer on if needed). Header
|
||||
strings, button labels, and other purely-static UI copy stay in
|
||||
`messages/en/*`.
|
||||
|
||||
### 4.10 `.cursorrules` scope
|
||||
|
||||
- The repo's `.cursorrules` PascalCase / lowercase normalization rule applies
|
||||
to **React component props only**. It does **not** apply to API query
|
||||
params, request bodies, or DB columns. The recommendation API uses lowercase
|
||||
facet keys throughout (`orgType`, `scale`, `maturity`, `size`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Authoring contract (informs §6 storage + §7 importer)
|
||||
|
||||
The four spreadsheets together imply this row schema (per matrix workbook):
|
||||
|
||||
```ts
|
||||
type MatrixRow = {
|
||||
/** Stable slug derived from `Label`/`Title` (kebab-case, lowercase, ascii).
|
||||
* Used as the card id everywhere downstream. */
|
||||
id: string;
|
||||
|
||||
/** Section this row belongs to. One of: communication, membership,
|
||||
* decisionMaking, conflictManagement. (values is not yet sheet-driven.) */
|
||||
section: "communication" | "membership" | "decisionMaking" | "conflictManagement";
|
||||
|
||||
/** Card-facing copy. Keys differ per section; importer normalizes. */
|
||||
card: {
|
||||
label: string;
|
||||
description: string;
|
||||
/** Section-specific long-form fields (3–4 per section). */
|
||||
modalSections: Record<string, string>;
|
||||
};
|
||||
|
||||
/** Optional numeric scalar fields (e.g. decisionMaking `Consensus Level`). */
|
||||
scalars?: Record<string, number>;
|
||||
|
||||
/** Facet matches (✓ → true, x/blank → false). Keys are canonical facet ids. */
|
||||
facets: {
|
||||
size: Record<"1" | "2_5" | "6_12" | "13_100" | "100_100k", boolean>;
|
||||
orgType: Record<"dao" | "forProfit" | "nonprofit" | "openSource" | "mutualAid" | "workersCoop", boolean>;
|
||||
scale: Record<"global" | "national" | "regional" | "local", boolean>;
|
||||
maturity: Record<"earlyStage" | "growthStage" | "established" | "enterprise", boolean>;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
A sibling **manifest** documents the per-section section-key mapping and
|
||||
column header → canonical facet/scalar key mapping, so the importer can be
|
||||
stable across header rewording.
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage (decided: normalized tables)
|
||||
|
||||
We are introducing two new Prisma models. Hand-typed `COMPOSITION_BY_SLUG` in
|
||||
`prisma/seed.ts` is replaced by template rows that **reference** method slugs.
|
||||
|
||||
```prisma
|
||||
model TemplateMethod {
|
||||
id String @id @default(cuid())
|
||||
section String // communication | membership | decisionMaking | conflictManagement
|
||||
slug String
|
||||
label String
|
||||
description String
|
||||
modalSections Json // { corePrinciple: "...", logisticsAdmin: "...", ... }
|
||||
scalars Json? // { consensusLevel: 0.51 }
|
||||
sortOrder Int @default(0)
|
||||
facets TemplateMethodFacet[]
|
||||
@@unique([section, slug])
|
||||
@@index([section])
|
||||
}
|
||||
|
||||
model TemplateMethodFacet {
|
||||
id String @id @default(cuid())
|
||||
methodId String
|
||||
group String // size | orgType | scale | maturity
|
||||
value String // e.g. "workersCoop"
|
||||
matches Boolean // ✓ → true, x/blank → false
|
||||
weight Float? // optional numeric override for future scoring
|
||||
method TemplateMethod @relation(fields: [methodId], references: [id], onDelete: Cascade)
|
||||
@@unique([methodId, group, value])
|
||||
@@index([group, value, matches])
|
||||
}
|
||||
```
|
||||
|
||||
`RuleTemplate.body` continues to express a **chosen composition** of methods
|
||||
(one or more per section). Curated templates in `prisma/seed.ts` become
|
||||
references to `TemplateMethod.slug` instead of literal copy strings — when
|
||||
copy changes in the spreadsheet, every template that references that slug
|
||||
inherits the new copy.
|
||||
|
||||
A follow-up (out of scope for CR-88) may add a `RuleTemplateMethodLink` join
|
||||
table if templates need ordering or per-template overrides; the current `body`
|
||||
JSON shape is sufficient for the first ship.
|
||||
|
||||
---
|
||||
|
||||
## 7. Importer (`scripts/import-templates-xlsx.ts`)
|
||||
|
||||
Phased plan that the implementation agent can follow top-to-bottom. Mirrors
|
||||
the structure of `prisma/seed.ts` (singleton client, `main()` +
|
||||
`finally { $disconnect }`, `process.exit(1)` on failure).
|
||||
|
||||
1. **Read `.xlsx`** with [`xlsx`](https://www.npmjs.com/package/xlsx) (SheetJS,
|
||||
add as devDependency) from a configurable input dir (default
|
||||
`data/template-matrix/`). The four workbooks live there as committed
|
||||
artifacts, not in `Downloads/`.
|
||||
2. **Schema-validate per section** with Zod schemas that live in
|
||||
`lib/server/validation/templateRecommendationSchemas.ts` so the API and
|
||||
importer share the row shape: required column headers, allowed cell
|
||||
symbols (`✓`, `x`, blank, decimal for `Consensus Level`).
|
||||
3. **Normalize**: kebab-case slug from label, strip
|
||||
`Organization Type:` / `Location:` / `Organizational Maturity:` prefixes,
|
||||
collapse whitespace, normalize curly quotes.
|
||||
4. **Cross-sheet validation**: facet columns must match the canonical 19-column
|
||||
set; unknown columns fail loudly via the importer (use `logger.error`).
|
||||
5. **Diff & upsert** inside `prisma.$transaction([...])`: upsert
|
||||
`TemplateMethod` rows by `(section, slug)`; delete + recreate
|
||||
`TemplateMethodFacet` rows for each method.
|
||||
6. **Emit a JSON snapshot** to `prisma/data/template-matrix.json` so
|
||||
`prisma/seed.ts` can replay imports when the source workbooks aren't
|
||||
available (e.g. CI seed without the spreadsheet checked in).
|
||||
7. **Flags**: `--dry-run` (validate + diff, no writes), `--allow-warnings`
|
||||
(don't fail on the row-32 / row-11 issues in §2.4 while authors are
|
||||
iterating).
|
||||
8. **Tests** in `tests/unit/importTemplatesXlsx.test.ts`: a fixture workbook
|
||||
with two rows per section asserts both validation errors (unknown column,
|
||||
bad symbol, miscategorized cell) and successful normalization. Reuse
|
||||
Vitest patterns from `tests/unit/createFlowValidation.test.ts`.
|
||||
|
||||
Per Ticket 16 and the roadmap, **prefer batch `.xlsx` import** over a live
|
||||
Google Sheets API in production. Authors export to `.xlsx` and a maintainer
|
||||
runs `npm run templates:import` (or CI does on a `data/template-matrix/` change).
|
||||
|
||||
---
|
||||
|
||||
## 8. APIs
|
||||
|
||||
Two read endpoints. Both follow §4.1 conventions exactly: `dbUnavailable()`
|
||||
guard → server helper from `lib/server/templateMethods.ts` →
|
||||
`NextResponse.json({ ... })`.
|
||||
|
||||
### 8.1 `GET /api/templates` (rewrite)
|
||||
|
||||
Query params (all optional):
|
||||
|
||||
- `facet.size=<chipId>` (repeatable)
|
||||
- `facet.orgType=<chipId>` (repeatable)
|
||||
- `facet.scale=<chipId>` (repeatable)
|
||||
- `facet.maturity=<chipId>` (repeatable)
|
||||
|
||||
Behavior:
|
||||
|
||||
- No params → existing curated ordering (`featured`, `sortOrder`, `title`),
|
||||
no scoring.
|
||||
- With facets → score each template by counting matching facets across the
|
||||
methods referenced in its `body`; return ranked `templates` plus an
|
||||
optional `scores` map.
|
||||
|
||||
Response:
|
||||
|
||||
```ts
|
||||
{
|
||||
templates: RuleTemplateDto[],
|
||||
scores?: Record<string, { score: number; matchedFacets: string[] }>
|
||||
}
|
||||
```
|
||||
|
||||
Param parsing helper lives next to `listRuleTemplatesFromDb` in
|
||||
`lib/server/ruleTemplates.ts` (e.g. `parseTemplateFacetsFromSearchParams`).
|
||||
|
||||
### 8.2 `GET /api/template-methods?section=<section>[&facet.*=...]`
|
||||
|
||||
Powers the four card-deck wizard steps and the section-level recommendation
|
||||
view. Response:
|
||||
|
||||
```ts
|
||||
{
|
||||
section: "communication" | "membership" | "decisionMaking" | "conflictManagement",
|
||||
methods: Array<{
|
||||
slug: string;
|
||||
label: string;
|
||||
description: string;
|
||||
modalSections: Record<string, string>;
|
||||
scalars?: Record<string, number>;
|
||||
/** Per-method facet match against the requested facets (omitted when no facets passed). */
|
||||
matches?: { score: number; matchedFacets: string[] };
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
Server helper: `listTemplateMethodsFromDb({ section, facets })` in
|
||||
`lib/server/templateMethods.ts`. Same swallow-and-return-`[]` failure mode as
|
||||
`listRuleTemplatesFromDb`.
|
||||
|
||||
### 8.3 `POST /api/templates/recommend` (follow-up, optional)
|
||||
|
||||
If product wants to send the full `CreateFlowState` (not just facet ids), the
|
||||
body schema **reuses** `createFlowStateSchema` from
|
||||
`lib/server/validation/createFlowSchemas.ts`. Same scoring engine, just a
|
||||
richer input. Skip until §8.1 + §8.2 ship.
|
||||
|
||||
**Empty / partial facets:** never error. Fall back to today's ordering and
|
||||
return all rows.
|
||||
|
||||
---
|
||||
|
||||
## 9. Wizard wiring (UI follow-on, not strictly part of CR-88)
|
||||
|
||||
Once the API exists:
|
||||
|
||||
- `communication-methods` / `membership-methods` / `decision-approaches` /
|
||||
`conflict-management` screens each call
|
||||
`GET /api/template-methods?section=...&facet.*=...`. The card label and
|
||||
modal copy come from the API response, not from
|
||||
`messages/en/create/<section>.json`. Static JSON in those four files is
|
||||
pruned to the page-level strings (header titles, button labels, modal
|
||||
chrome) only.
|
||||
- Selecting a template on the marketing home or `templates/` page can prefill
|
||||
the create flow's `selected*MethodIds` from the template's composition (this
|
||||
closes the `?template=` no-op gap noted in
|
||||
`CreateFlowLayoutClient.tsx`).
|
||||
- Recommendations should never **hide** options from the user — ranking only.
|
||||
Authors expect to see "all 32 decision-making patterns" with the ✓-matching
|
||||
ones surfaced first.
|
||||
|
||||
---
|
||||
|
||||
## 10. Open questions for product before coding
|
||||
|
||||
1. **Should `Values` also be sheet-driven?** Today it's free-text only and
|
||||
not in any of the four matrices. Roadmap implies eventual parity.
|
||||
2. **Scoring vs filtering**: do we want to **hide** non-✓ rows when a facet
|
||||
is set, or only **rank** them? Recommend ranking with a soft cutoff.
|
||||
3. **Per-template featured composition vs library-wide**: should
|
||||
`RuleTemplate` rows continue to exist as named compositions
|
||||
("Consensus", "Elected Board", etc.), or become derived from a
|
||||
"this is the best mix for nonprofit + 13–100 + early stage" scoring? Doc
|
||||
today assumes the former — templates remain curated.
|
||||
4. **Authoring source of truth**: are the `Downloads/*.xlsx` files committed
|
||||
to `data/template-matrix/` going forward, or do they live in a Drive folder
|
||||
pulled by the importer at build time? Recommend committing.
|
||||
5. **Data validation strictness**: the current Decision-making sheet has a
|
||||
miscategorized cell (row 32, see §2.4). Importer should fail by default,
|
||||
with a `--allow-warnings` flag for in-progress edits.
|
||||
|
||||
---
|
||||
|
||||
## 11. Test plan (acceptance for CR-88)
|
||||
|
||||
- [ ] `scripts/import-templates-xlsx.ts` runs end-to-end on the four committed
|
||||
workbooks with no errors and produces the expected DB diff (or JSON
|
||||
snapshot).
|
||||
- [ ] Editing a row in the source workbook and re-running the importer changes
|
||||
the rank order returned by `GET /api/templates?facet.orgType=4`
|
||||
(the `Nonprofit` chip id) without any manual Studio edit.
|
||||
- [ ] `tests/unit/importTemplatesXlsx.test.ts` rejects each documented
|
||||
validation failure (unknown column, bad symbol, miscategorized row).
|
||||
- [ ] `tests/unit/templateRecommendationSchemas.test.ts` exercises the Zod
|
||||
schemas the importer and API share.
|
||||
- [ ] Manual smoke on the four wizard card-deck steps: facet-narrowed
|
||||
ordering surfaces matching cards first; facetless GET returns the
|
||||
full curated list.
|
||||
- [ ] No regression in existing template surfaces (marketing home, templates
|
||||
index, review-template preview).
|
||||
|
||||
---
|
||||
|
||||
## 12. Source files referenced
|
||||
|
||||
- `prisma/schema.prisma` — `RuleTemplate` model (lines 64–73).
|
||||
- `prisma/seed.ts` — current curated composition + xlsx-shaped helpers
|
||||
(lines 1–404).
|
||||
- `app/api/templates/route.ts` — existing GET endpoint (to be rewritten).
|
||||
- `app/api/drafts/me/route.ts` — reference route shape (`dbUnavailable` →
|
||||
`getSessionUser` → `readLimitedJson` → `safeParse` → `jsonFromZodError`).
|
||||
- `lib/server/db.ts` — Prisma singleton (lines 1–18).
|
||||
- `lib/server/responses.ts` — `dbUnavailable()` (lines 1–8).
|
||||
- `lib/server/ruleTemplates.ts` — `listRuleTemplatesFromDb` (lines 9–30).
|
||||
- `lib/server/validation/createFlowSchemas.ts` — schema to reuse for
|
||||
`POST /api/templates/recommend` (lines 47–106).
|
||||
- `lib/server/validation/requestBody.ts` — `readLimitedJson` (lines 13–48).
|
||||
- `lib/server/validation/zodHttp.ts` — `jsonFromZodError` (lines 4–17).
|
||||
- `lib/server/validation/plainJson.ts` — `assertPlainJsonValue` /
|
||||
`DEFAULT_PLAIN_JSON_LIMITS`.
|
||||
- `lib/logger.ts` — server-side `logger`.
|
||||
- `app/(app)/create/types.ts` — `CreateFlowState` and facet fields.
|
||||
- `app/(app)/create/utils/flowSteps.ts` — canonical step order.
|
||||
- `app/(app)/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
|
||||
- `app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
|
||||
derivation pattern (positional `String(i+1)`).
|
||||
- `app/(app)/create/screens/card/CommunicationMethodsScreen.tsx` — section-field
|
||||
contract (`SECTION_FIELDS`).
|
||||
- `messages/en/create/{communitySize,communityStructure,communication,membership,rightRail,conflictManagement}.json` —
|
||||
current static card / chip copy that the matrix supersedes.
|
||||
- `lib/templates/governanceTemplateCatalog.ts`,
|
||||
`lib/templates/templateGridPresentation.ts`,
|
||||
`lib/create/fetchTemplates.ts` — current presentation/DTO layer.
|
||||
- `tests/unit/createFlowValidation.test.ts` — Vitest pattern for new
|
||||
schema/importer tests.
|
||||
- Roadmap: `docs/backend-roadmap.md` §4 (lines 83–85), §13.
|
||||
- Spec: `docs/backend-linear-tickets.md` Ticket 16 (lines 280–304).
|
||||
|
||||
## 13. Source workbooks
|
||||
|
||||
| File | Sheet | Rows | Cols | Section |
|
||||
|---|---|---|---|---|
|
||||
| `Communication Methods.xlsx` | `Current` | 11 cards | 24 | `communication` |
|
||||
| `Group_Membership_Methods.xlsx` | `Current` | 19 cards | 24 | `membership` |
|
||||
| `Decision-making.xlsx` | `Current` | 32 cards | 26 | `decisionMaking` |
|
||||
| `Conflict Management Methods.xlsx` | `Current` | 19 cards | 25 | `conflictManagement` |
|
||||
|
||||
Counts include the header row. Decision-making has 26 columns because of two
|
||||
extra content fields (`Consensus Level`, `Step-by-Step Instructions` vs the
|
||||
4-section pattern of the others).
|
||||
Reference in New Issue
Block a user