# 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[].label` and `modals[].title` | | `Description` | `cards[].supportText` and `modals[].description` | | `Core Principle & Scope` | `modals[].sections.corePrinciple` | | `Logistics, Admin & Norms` | `modals[].sections.logisticsAdmin` | | `Code of Conduct` | `modals[].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[].label` / modal title | | `Description` | `cards[].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=` 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)` → `.safeParse(parsed.value)` → `jsonFromZodError(validated.error)` on failure. (`lib/server/validation/requestBody.ts`, `lib/server/validation/zodHttp.ts`). 4. Success: `NextResponse.json({ : 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`). - 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/.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//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/.json` and read via `useMessages().create.` (`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; }; /** Optional numeric scalar fields (e.g. decisionMaking `Consensus Level`). */ scalars?: Record; /** 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=` (repeatable) - `facet.orgType=` (repeatable) - `facet.scale=` (repeatable) - `facet.maturity=` (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 } ``` Param parsing helper lives next to `listRuleTemplatesFromDb` in `lib/server/ruleTemplates.ts` (e.g. `parseTemplateFacetsFromSearchParams`). ### 8.2 `GET /api/template-methods?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; scalars?: Record; /** 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/
.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).