33 KiB
Template Recommendation Matrix — Implementation Context (CR-88)
Status: Draft / context doc. Reference only — not yet implemented.
Linear: CR-88
Roadmap: docs/backend-roadmap.md §4 (RuleTemplate) and §13.
Spec ticket: docs/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.tsCOMPOSITION_BY_SLUGmap, the existingGET /api/templatesresponse shape, and the staticmessages/en/create/*.jsoncard 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 (onlyDecision-making.xlsxrow 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 (seechipRowsFromLabelsinapp/(app)/create/screens/select/CommunityStructureSelectScreen.tsxlines 49–57). The importer should emit a stable lookup table mapping(facetGroup, label) → wizardChipIdso the recommendation engine can match a user'sselectedXxxIdsagainst 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.jsonmodal 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): theConsensus Levelcell contains"Military, Corporations"(the value clearly belongs toApplicable 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 asx(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
Titleinstead ofLabeland omits theOrganization Type:/Location:/Organizational Maturity:prefixes — normalize both at import time.
3. Existing data model & wizard surface area
3.1 RuleTemplate (today)
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.
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)
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:
if (!isDatabaseConfigured()) return dbUnavailable();— always first. (lib/server/env.ts,lib/server/responses.ts).- For auth'd routes:
const user = await getSessionUser();thenreturn NextResponse.json({ error: "Unauthorized" }, { status: 401 });if missing. (Recommendation read endpoints can stay unauthenticated.) - For request bodies:
readLimitedJson(request)→<schema>.safeParse(parsed.value)→jsonFromZodError(validated.error)on failure. (lib/server/validation/requestBody.ts,lib/server/validation/zodHttp.ts). - Success:
NextResponse.json({ <key>: data })— flat object with one or two named keys, nosuccess: trueenvelope. - Errors: structured
{ error: { code, message } }(Zod path) or simple{ error: "..." }(auth path). Match what's already in the repo. - Server-side query helpers swallow Prisma failures and return
[]/null(seelistRuleTemplatesFromDbinlib/server/ruleTemplates.tslines 9–30). Routes do not wrap helper calls intry/catch.
4.2 Zod schemas live in lib/server/validation/
- One file per feature area (e.g.
createFlowSchemas.ts, futuretemplateRecommendationSchemas.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_ORDERand existing array bounds where they overlap (see theselectedXxxMethodIdsarrays increateFlowStateSchema).
4.3 Prisma access
- Singleton:
import { prisma } from "lib/server/db";— nevernew PrismaClient()from app code. (Standalone scripts underscripts//prisma/may instantiate their own, matchingprisma/seed.tslines 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
$transactionpatterns exist yet; introduce one for the importer (writeTemplateMethod+TemplateMethodFacetrows atomically).
4.4 DTO style
- Hand-written
typealiases that mirror a Prismaselectclause, co-located with the consumer (seeRuleTemplateDtoinlib/create/fetchTemplates.tslines 5–14). - For a feature with both client and server consumers, put the type in
lib/<feature>/types.tsand import from both sides.
4.5 Standalone scripts
- Use
tsx(already a dev dep; entry pointpackage.jsonprisma.seedfield). - 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()infinally. - Add an entry to
package.jsonscripts(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-runflag that validates + diffs without writing.
4.6 Tests
- Vitest under
tests/unit/*.test.tsfor parsers / validators / pure functions (seetests/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 undertests/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 intests/msw/server.ts. No Prisma mock helper exists; importer test should use a fixture workbook and stub theprismaclient at the import site.
4.7 Logging
- Use
loggerfromlib/logger.tsfor any server-side info/warn/error in scripts and route helpers (matchesapp/api/auth/magic-link/request/route.tslines 14–15, 35–45). NoapiErrorhelper exists; do not introduce one.
4.8 New deps
xlsx(SheetJS) is not currently inpackage.json. Add it as a prod dep only if the importer is invoked from app code; if the importer is script-only,devDependencyis fine. CR-88's plan calls for a build/CLI-time importer, sodevDependenciesis the right home.
4.9 i18n / messages/ constraint
- Card decks and modal copy are currently keyed in
messages/en/<feature>.jsonand read viauseMessages().create.<feature>(app/contexts/MessagesContext.tsx,messages/en/index.ts). - Only
enis wired today, so we don't have a translation backlog blocking us. The wiring step (§7) replacesmessages/en/create/{communication, membership,rightRail,conflictManagement}.jsoncard/modal payloads with values served byGET /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 inmessages/en/*.
4.10 .cursorrules scope
- The repo's
.cursorrulesPascalCase / 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):
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.
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).
- Read
.xlsxwithxlsx(SheetJS, add as devDependency) from a configurable input dir (defaultdata/template-matrix/). The four workbooks live there as committed artifacts, not inDownloads/. - Schema-validate per section with Zod schemas that live in
lib/server/validation/templateRecommendationSchemas.tsso the API and importer share the row shape: required column headers, allowed cell symbols (✓,x, blank, decimal forConsensus Level). - Normalize: kebab-case slug from label, strip
Organization Type:/Location:/Organizational Maturity:prefixes, collapse whitespace, normalize curly quotes. - Cross-sheet validation: facet columns must match the canonical 19-column
set; unknown columns fail loudly via the importer (use
logger.error). - Diff & upsert inside
prisma.$transaction([...]): upsertTemplateMethodrows by(section, slug); delete + recreateTemplateMethodFacetrows for each method. - Emit a JSON snapshot to
prisma/data/template-matrix.jsonsoprisma/seed.tscan replay imports when the source workbooks aren't available (e.g. CI seed without the spreadsheet checked in). - 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). - 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 fromtests/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 rankedtemplatesplus an optionalscoresmap.
Response:
{
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:
{
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-managementscreens each callGET /api/template-methods?section=...&facet.*=.... The card label and modal copy come from the API response, not frommessages/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'sselected*MethodIdsfrom the template's composition (this closes the?template=no-op gap noted inCreateFlowLayoutClient.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
- Should
Valuesalso be sheet-driven? Today it's free-text only and not in any of the four matrices. Roadmap implies eventual parity. - 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.
- Per-template featured composition vs library-wide: should
RuleTemplaterows 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. - Authoring source of truth: are the
Downloads/*.xlsxfiles committed todata/template-matrix/going forward, or do they live in a Drive folder pulled by the importer at build time? Recommend committing. - 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-warningsflag for in-progress edits.
11. Test plan (acceptance for CR-88)
scripts/import-templates-xlsx.tsruns 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(theNonprofitchip id) without any manual Studio edit. tests/unit/importTemplatesXlsx.test.tsrejects each documented validation failure (unknown column, bad symbol, miscategorized row).tests/unit/templateRecommendationSchemas.test.tsexercises 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—RuleTemplatemodel (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 forPOST /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-sidelogger.app/(app)/create/types.ts—CreateFlowStateand 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 (positionalString(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.mdTicket 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).