Implement modals across create flow

This commit is contained in:
adilallo
2026-04-17 23:45:29 -06:00
parent 36dcb79870
commit 4854c49c4a
21 changed files with 2089 additions and 318 deletions
+728
View File
@@ -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: Workers coop` (or `Workers coop`) | id `"1"` | `Workers 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/create/screens/select/CommunityStructureSelectScreen.tsx` lines 4957).
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 `Workers 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/create/screens/card/CommunicationMethodsScreen.tsx`).
**Content columns (positions 15):**
| 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/create/screens/card/MembershipMethodsScreen.tsx`) and
`messages/en/create/membership.json`.
**Content columns (positions 15):**
| 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/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
`messages/en/create/rightRail.json`.
**Content columns (positions 17):**
| 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.01.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/create/screens/card/ConflictManagementScreen.tsx`) and
`messages/en/create/conflictManagement.json`.
**Content columns (positions 16):**
| 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/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 47106) — 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/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)/MarketingRuleStackSection.tsx` |
| Templates index | `app/(marketing)/templates/page.tsx` |
| Template preview (by slug) | `app/create/review-template/[slug]/page.tsx` |
| "Use without changes" → publish | `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 479482).
---
## 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 930).
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
363403.)
- 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 514).
- 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 1415, 3545). 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 (34 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 + 13100 + 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 6473).
- `prisma/seed.ts` — current curated composition + xlsx-shaped helpers
(lines 1404).
- `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 118).
- `lib/server/responses.ts` — `dbUnavailable()` (lines 18).
- `lib/server/ruleTemplates.ts` — `listRuleTemplatesFromDb` (lines 930).
- `lib/server/validation/createFlowSchemas.ts` — schema to reuse for
`POST /api/templates/recommend` (lines 47106).
- `lib/server/validation/requestBody.ts` — `readLimitedJson` (lines 1348).
- `lib/server/validation/zodHttp.ts` — `jsonFromZodError` (lines 417).
- `lib/server/validation/plainJson.ts` — `assertPlainJsonValue` /
`DEFAULT_PLAIN_JSON_LIMITS`.
- `lib/logger.ts` — server-side `logger`.
- `app/create/types.ts` — `CreateFlowState` and facet fields.
- `app/create/utils/flowSteps.ts` — canonical step order.
- `app/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
- `app/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
derivation pattern (positional `String(i+1)`).
- `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 8385), §13.
- Spec: `docs/backend-linear-tickets.md` Ticket 16 (lines 280304).
## 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).