App reorganization
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
# Template Recommendation Matrix — Implementation Context (CR-88)
|
||||
|
||||
**Status:** Draft / context doc. Reference only — not yet implemented.
|
||||
**Linear:** [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)
|
||||
**Roadmap:** [`docs/backend-roadmap.md`](backend-roadmap.md) §4 (`RuleTemplate`) and §13.
|
||||
**Spec ticket:** [`docs/backend-linear-tickets.md`](backend-linear-tickets.md) Ticket 16.
|
||||
|
||||
This doc consolidates the **four product-authored matrix spreadsheets** with the
|
||||
**existing data model, create-flow facets, and section structure** so we have a
|
||||
single reference while implementing the importer + recommendation API.
|
||||
|
||||
> **Scope note:** No data, API, or UI surface for this feature is in production
|
||||
> yet. **Backwards compatibility is not a constraint** — we will replace the
|
||||
> hand-typed `prisma/seed.ts` `COMPOSITION_BY_SLUG` map, the existing
|
||||
> `GET /api/templates` response shape, and the static `messages/en/create/*.json`
|
||||
> card decks where it makes the design cleaner.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal (one paragraph)
|
||||
|
||||
Replace the hand-curated `prisma/seed.ts` `COMPOSITION_BY_SLUG` map with a
|
||||
**spreadsheet-authored matrix** for each rule section
|
||||
(Communication, Membership, Decision-making, Conflict management — and later
|
||||
Values), where each row is a **method/pattern card** and each column is either
|
||||
**long-form copy that populates the card UI** or a **facet flag** (✓/x or score)
|
||||
that the recommendation engine uses to filter and rank cards based on the
|
||||
user's create-flow answers (community size, organization type, location/scale,
|
||||
maturity).
|
||||
|
||||
The same authoring contract should make it trivial for product to ship updated
|
||||
spreadsheets and have the create-flow card decks (and the home/templates page
|
||||
recommendations) update without any code changes.
|
||||
|
||||
---
|
||||
|
||||
## 2. The four spreadsheets
|
||||
|
||||
All four xlsx files share **the same column shape**: leading **content
|
||||
columns** + trailing **facet columns** (✓ / x cells). Sheet name is `Current`
|
||||
in every workbook.
|
||||
|
||||
### 2.1 Shared facet columns (last 19, identical across the four sheets)
|
||||
|
||||
Order is preserved here because the columns are positional in the sheets:
|
||||
|
||||
| # | Column header (xlsx) | Maps to wizard step / state field | Wizard chip label (`messages/en/create/...`) |
|
||||
|---|---|---|---|
|
||||
| 1 | `1 member` | `community-size` → `selectedCommunitySizeIds` (id `"1"`) | `1 member` |
|
||||
| 2 | `2-5 members` | id `"2"` | `2-5 members` |
|
||||
| 3 | `6-12 members` | id `"3"` | `6-12 members` |
|
||||
| 4 | `13-100 members` | id `"4"` | `13-100 members` |
|
||||
| 5 | `100-100,000 members` | id `"5"` | `100-100,000 members` |
|
||||
| 6 | `Organization Type:DAO` (or `DAO` in conflict/comms/membership) | `community-structure` → `selectedOrganizationTypeIds` (id `"6"` in `organizationTypes`) | `DAO` |
|
||||
| 7 | `Organization Type:For profit business` (or `For profit business`) | id `"5"` in `organizationTypes` | `For profit business` |
|
||||
| 8 | `Organization Type:Nonprofit` (or `Nonprofit`) | id `"4"` in `organizationTypes` | `Nonprofit` |
|
||||
| 9 | `Organization Type:Open source project` (or `Open source project`) | id `"3"` | `Open source project` |
|
||||
| 10 | `Organization Type:Mutual aid` (or `Mutual aid`) | id `"2"` | `Mutual aid` |
|
||||
| 11 | `Organization Type: Worker’s coop` (or `Worker’s coop`) | id `"1"` | `Worker’s coop` |
|
||||
| 12 | `Location: Global` (or `Global`) | `community-structure` → `selectedScaleIds` (id `"4"` in `scaleOptions`) | `Global` |
|
||||
| 13 | `Location: National` (or `National`) | id `"3"` | `National` |
|
||||
| 14 | `Location: Regional` (or `Regional`) | id `"2"` | `Regional` |
|
||||
| 15 | `Location: Local` (or `Local`) | id `"1"` | `Local` |
|
||||
| 16 | `Organizational Maturity: Early stage` (or `Early stage`) | `community-structure` → `selectedMaturityIds` (id `"1"` in `maturityOptions`) | `Early stage` |
|
||||
| 17 | `Organizational Maturity: Growth stage` (or `Growth stage`) | id `"2"` | `Growth stage` |
|
||||
| 18 | `Organizational Maturity: Established` (or `Established`) | id `"3"` | `Established` |
|
||||
| 19 | `Organizational Maturity: Enterprise` (or `Enterprise`) | id `"4"` | `Enterprise` |
|
||||
|
||||
**Important normalization rules (importer must enforce):**
|
||||
|
||||
- Decision-making prefixes columns with `Organization Type:`, `Location:`,
|
||||
`Organizational Maturity:`. The other three sheets drop the prefix. Importer
|
||||
should normalize to a single canonical key (e.g.
|
||||
`orgType.workersCoop`, `scale.local`, `maturity.earlyStage`, `size.6_12`).
|
||||
- Cell value semantics: `✓` → match, `x` (lowercase) → no match, blank → no
|
||||
match, numbers → optional weighted score (only `Decision-making.xlsx` row 32
|
||||
contains a non-symbol cell — `"Military, Corporations"` in the size column —
|
||||
see §2.4 data-quality issues).
|
||||
- Wizard chip ids are **positional 1..N** within each `messages/en/create/*`
|
||||
array (see `chipRowsFromLabels` in
|
||||
`app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` lines 49–57).
|
||||
The importer should emit a stable lookup table mapping
|
||||
`(facetGroup, label) → wizardChipId` so the recommendation engine can match
|
||||
a user's `selectedXxxIds` against the matrix without depending on label
|
||||
spelling.
|
||||
- Curly apostrophes appear in `Worker’s coop`. Compare on a normalized key,
|
||||
not on raw label.
|
||||
|
||||
### 2.2 Communication Methods (`Communication Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps 1:1 to `messages/en/create/communication.json` and the
|
||||
`communication-methods` step
|
||||
(`app/(app)/create/screens/card/CommunicationMethodsScreen.tsx`).
|
||||
|
||||
**Content columns (positions 1–5):**
|
||||
|
||||
| Sheet column | Card field |
|
||||
|---|---|
|
||||
| `Label` | `cards[<id>].label` and `modals[<id>].title` |
|
||||
| `Description` | `cards[<id>].supportText` and `modals[<id>].description` |
|
||||
| `Core Principle & Scope` | `modals[<id>].sections.corePrinciple` |
|
||||
| `Logistics, Admin & Norms` | `modals[<id>].sections.logisticsAdmin` |
|
||||
| `Code of Conduct` | `modals[<id>].sections.codeOfConduct` |
|
||||
|
||||
`SECTION_FIELDS = ["corePrinciple", "logisticsAdmin", "codeOfConduct"]` is
|
||||
the source of truth (`CommunicationMethodsScreen.tsx`).
|
||||
|
||||
**Card rows (11):** In-Person Meetings · Signal · Video Meetings · Loomio ·
|
||||
Matrix / Element · GitHub / GitLab · Discord · Email Distribution List · Slack
|
||||
· WhatsApp · Discourse (Forum).
|
||||
|
||||
### 2.3 Membership / Group-Membership (`Group_Membership_Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `membership-methods` step
|
||||
(`app/(app)/create/screens/card/MembershipMethodsScreen.tsx`) and
|
||||
`messages/en/create/membership.json`.
|
||||
|
||||
**Content columns (positions 1–5):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Label` | `cards[<id>].label` / modal title |
|
||||
| `Description` | `cards[<id>].supportText` / modal description |
|
||||
| `Eligibility & Philosophy` | modal section A (`eligibilityPhilosophy`) |
|
||||
| `Joining Process` | modal section B (`joiningProcess`) |
|
||||
| `Expectations & Removal` | modal section C (`expectationsRemoval`) |
|
||||
|
||||
**Card rows (19):** Open Access · Orientation Required · Invitation Only ·
|
||||
Contribution Based · Mentorship · Peer Sponsorship · Consensus or Vote-Based
|
||||
Approval · Trial Period / Provisional Membership · Referral System with
|
||||
Screening · Membership Agreement or Pledge · Weighted or Tiered Membership ·
|
||||
Hybrid Approval Process · Skill-Based Contribution · Pay-to-Join · Application
|
||||
& Review · Identity Verification · Collective Interviews · Skill-Based
|
||||
Evaluation · Lottery / Sortition.
|
||||
|
||||
> The wizard's existing `membership.json` modal section keys do not yet match
|
||||
> these. Since backwards compatibility is not a constraint, **rename the
|
||||
> wizard's section keys to match the matrix** (`eligibilityPhilosophy` /
|
||||
> `joiningProcess` / `expectationsRemoval`) when wiring this up — the existing
|
||||
> copy is placeholder.
|
||||
|
||||
### 2.4 Decision-making (`Decision-making.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `decision-approaches` step
|
||||
(`app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
|
||||
`messages/en/create/rightRail.json`.
|
||||
|
||||
**Content columns (positions 1–7):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Label` | card title |
|
||||
| `Description` | card support text |
|
||||
| `Core Principle` | modal section A (`corePrinciple`) |
|
||||
| `Applicable Scope` | modal section B (`applicableScope`) — free-text examples, e.g. `"Daily Operations, Minor Expenditures"` |
|
||||
| `Consensus Level` | numeric 0.0–1.0 stored under `scalars.consensusLevel` (e.g. `0.51`, `0.67`, `1.0`) — drives the **Consensus axis** in any future visual sort/filter |
|
||||
| `Step-by-Step Instructions` | modal section C (`stepByStep`) |
|
||||
| `Objections & Deadlocks` | modal section D (`objectionsDeadlocks`) |
|
||||
|
||||
**Card rows (32):** Lazy Consensus · Do-ocracy · Consensus Decision-Making ·
|
||||
Rotational Leadership · Modified Consensus · Consensus Seeking with Delegates
|
||||
· Sociocracy · Supermajority Rule · Ranked Choice Voting · Range Voting ·
|
||||
Majority Rule · Approval Voting · Weighted Voting · Cumulative Voting ·
|
||||
Quadratic Voting · Continuous Voting · Holacracy · Collaborative Platforms ·
|
||||
Deliberative Polling · Investor-Filled Board Seats · Elected Board of
|
||||
Directors · Advisory Committees · Delegated Decision-Making · Executive
|
||||
Committees · First Past the Post · Lottery/Sortition · Proof of Work · Random
|
||||
Choice · Algorithm-Driven Decisions · Autocratic Decision-Making ·
|
||||
Hierarchical Decision-Making · Negotiated Decisions.
|
||||
|
||||
**Data-quality issues to handle in the importer (do not silently drop):**
|
||||
|
||||
- Row 32 (`Hierarchical Decision-Making`): the `Consensus Level` cell contains
|
||||
`"Military, Corporations"` (the value clearly belongs to `Applicable Scope`,
|
||||
which itself already contains `"Military, Corporations"`). Importer should
|
||||
flag this as a validation error and require a fix in the source workbook
|
||||
rather than try to repair it.
|
||||
- Row 11 (`Range Voting`): the **last facet column** (`Maturity: Enterprise`)
|
||||
is empty in the source — treat empty as `x` (no match) **only after** the
|
||||
importer logs a warning so the author knows it wasn't intentional ✓.
|
||||
|
||||
### 2.5 Conflict Management (`Conflict Management Methods.xlsx`, sheet `Current`)
|
||||
|
||||
Maps to the `conflict-management` step
|
||||
(`app/(app)/create/screens/card/ConflictManagementScreen.tsx`) and
|
||||
`messages/en/create/conflictManagement.json`.
|
||||
|
||||
**Content columns (positions 1–6):**
|
||||
|
||||
| Sheet column | Card field (proposed naming) |
|
||||
|---|---|
|
||||
| `Title` | card title (note: not `Label` like the other three) |
|
||||
| `Description` | card support text |
|
||||
| `Core Principle` | modal section A (`corePrinciple`) |
|
||||
| `Applicable Scope` | modal section B (`applicableScope`) |
|
||||
| `Process Protocol` | modal section C (`processProtocol`) |
|
||||
| `Restoration & Fallbacks` | modal section D (`restorationFallbacks`) |
|
||||
|
||||
**Card rows (19):** Peer Mediation · Conflict Resolution Council · Facilitated
|
||||
Negotiation · Ad Hoc Arbitration · Conflict Workshops · Supermajority Vote ·
|
||||
Interest-Based Bargaining · Restorative Practices · Mediation · Circle
|
||||
Processes · Judicial Committees · Managerial Decision · Internal Tribunal ·
|
||||
Consensus Building · Binding Arbitration · Non-Binding Arbitration · Binding
|
||||
Contracts · Lottery/Sortition · Rotational Judging.
|
||||
|
||||
> Conflict Management sheet uses `Title` instead of `Label` and omits the
|
||||
> `Organization Type:` / `Location:` / `Organizational Maturity:` prefixes —
|
||||
> normalize both at import time.
|
||||
|
||||
---
|
||||
|
||||
## 3. Existing data model & wizard surface area
|
||||
|
||||
### 3.1 `RuleTemplate` (today)
|
||||
|
||||
```64:73:prisma/schema.prisma
|
||||
model RuleTemplate {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
title String
|
||||
category String?
|
||||
description String?
|
||||
body Json
|
||||
sortOrder Int @default(0)
|
||||
featured Boolean @default(false)
|
||||
}
|
||||
```
|
||||
|
||||
`body` JSON is the rendered rule document
|
||||
(`{ sections: [{ categoryName, entries: [{ title, body }] }, ...] }`),
|
||||
authored today by the `bodyFromXlsxComposition()` helper in
|
||||
`prisma/seed.ts` from a hand-typed `COMPOSITION_BY_SLUG` map.
|
||||
|
||||
**Section ordering (canonical):** Values → Communication → Membership →
|
||||
Decision-making → Conflict management. Final-review and `governancePatternBody`
|
||||
both rely on this exact order and casing.
|
||||
|
||||
```16:60:prisma/seed.ts
|
||||
function governancePatternBody(coreValues: string): Prisma.InputJsonValue {
|
||||
return {
|
||||
sections: [
|
||||
{ categoryName: "Values", entries: [{ title: "Core stance", body: coreValues }] },
|
||||
{ categoryName: "Communication", entries: [...] },
|
||||
{ categoryName: "Membership", entries: [...] },
|
||||
{ categoryName: "Decision-making", entries: [...] },
|
||||
{ categoryName: "Conflict management", entries: [...] },
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Wizard facets captured today (`CreateFlowState`)
|
||||
|
||||
```83:95:app/(app)/create/types.ts
|
||||
selectedCommunitySizeIds?: string[];
|
||||
selectedOrganizationTypeIds?: string[];
|
||||
selectedScaleIds?: string[];
|
||||
selectedMaturityIds?: string[];
|
||||
selectedCoreValueIds?: string[];
|
||||
selectedCommunicationMethodIds?: string[];
|
||||
selectedMembershipMethodIds?: string[];
|
||||
selectedDecisionApproachIds?: string[];
|
||||
selectedConflictManagementIds?: string[];
|
||||
```
|
||||
|
||||
The first four are exactly the four **facet groups** in the matrix sheets. The
|
||||
last four are the user's chosen **cards per section**, which the recommendation
|
||||
flow can either pre-select (when picked from a template) or feed back into
|
||||
ranking.
|
||||
|
||||
These same fields are validated server-side by `createFlowStateSchema` in
|
||||
`lib/server/validation/createFlowSchemas.ts` (lines 47–106) — the recommend
|
||||
endpoint should reuse that schema (or a strict subset) instead of redefining
|
||||
the facet shape.
|
||||
|
||||
### 3.3 Wizard step order
|
||||
|
||||
Source of truth is `app/(app)/create/utils/flowSteps.ts` (`FLOW_STEP_ORDER`). The
|
||||
relevant slice is:
|
||||
|
||||
```
|
||||
review → core-values → communication-methods → membership-methods →
|
||||
decision-approaches → conflict-management → confirm-stakeholders → final-review
|
||||
```
|
||||
|
||||
`docs/create-flow.md`'s step table is **stale**; trust `flowSteps.ts`.
|
||||
|
||||
### 3.4 Where templates already surface in the UI
|
||||
|
||||
| Surface | File |
|
||||
|---|---|
|
||||
| Marketing home "Popular templates" | `app/(marketing)/_components/MarketingRuleStackSection.tsx` |
|
||||
| Templates index | `app/(marketing)/templates/page.tsx` |
|
||||
| Template preview (by slug) | `app/(app)/create/review-template/[slug]/page.tsx` |
|
||||
| "Use without changes" → publish | `app/(app)/create/CreateFlowLayoutClient.tsx` `handleUseTemplateWithoutChanges` |
|
||||
| API list | `app/api/templates/route.ts` (GET only, no params) |
|
||||
|
||||
There is currently **no** recommendation logic, no facet filtering, and the
|
||||
`/create/informational?template=<slug>` query param is a known no-op (see
|
||||
`CreateFlowLayoutClient.tsx` lines 479–482).
|
||||
|
||||
---
|
||||
|
||||
## 4. Repo conventions to follow (don't reinvent)
|
||||
|
||||
These are the patterns the implementation must match. References point at the
|
||||
canonical example for each.
|
||||
|
||||
### 4.1 API routes (`app/api/**/route.ts`)
|
||||
|
||||
`app/api/drafts/me/route.ts` is the reference — every new route in this
|
||||
feature must match this exact shape:
|
||||
|
||||
1. `if (!isDatabaseConfigured()) return dbUnavailable();` — always first.
|
||||
(`lib/server/env.ts`, `lib/server/responses.ts`).
|
||||
2. For auth'd routes: `const user = await getSessionUser();` then
|
||||
`return NextResponse.json({ error: "Unauthorized" }, { status: 401 });` if
|
||||
missing. (Recommendation read endpoints can stay unauthenticated.)
|
||||
3. For request bodies: `readLimitedJson(request)` →
|
||||
`<schema>.safeParse(parsed.value)` → `jsonFromZodError(validated.error)` on
|
||||
failure. (`lib/server/validation/requestBody.ts`,
|
||||
`lib/server/validation/zodHttp.ts`).
|
||||
4. Success: `NextResponse.json({ <key>: data })` — flat object with one or two
|
||||
named keys, no `success: true` envelope.
|
||||
5. Errors: structured `{ error: { code, message } }` (Zod path) or simple
|
||||
`{ error: "..." }` (auth path). Match what's already in the repo.
|
||||
6. Server-side query helpers swallow Prisma failures and return `[]`/`null`
|
||||
(see `listRuleTemplatesFromDb` in `lib/server/ruleTemplates.ts` lines 9–30).
|
||||
Routes do **not** wrap helper calls in `try/catch`.
|
||||
|
||||
### 4.2 Zod schemas live in `lib/server/validation/`
|
||||
|
||||
- One file per feature area (e.g. `createFlowSchemas.ts`, future
|
||||
`templateRecommendationSchemas.ts`).
|
||||
- Export the schema **and** the inferred type
|
||||
(`export type X = z.infer<typeof xSchema>`).
|
||||
- Wrap any free-form JSON blobs with `assertPlainJsonValue` /
|
||||
`DEFAULT_PLAIN_JSON_LIMITS` (`lib/server/validation/plainJson.ts`) so the
|
||||
size/depth bounds match the rest of the API.
|
||||
- Reuse `FLOW_STEP_ORDER` and existing array bounds where they overlap (see
|
||||
the `selectedXxxMethodIds` arrays in `createFlowStateSchema`).
|
||||
|
||||
### 4.3 Prisma access
|
||||
|
||||
- Singleton: `import { prisma } from "lib/server/db";` — never
|
||||
`new PrismaClient()` from app code. (Standalone scripts under `scripts/` /
|
||||
`prisma/` may instantiate their own, matching `prisma/seed.ts` lines
|
||||
363–403.)
|
||||
- Server-only "fetch/list" helpers live under `lib/server/<feature>.ts`,
|
||||
return DTOs (not raw Prisma rows), and degrade gracefully
|
||||
(`isDatabaseConfigured()` short-circuit, `try/catch` → empty result).
|
||||
- No `$transaction` patterns exist yet; **introduce one** for the importer
|
||||
(write `TemplateMethod` + `TemplateMethodFacet` rows atomically).
|
||||
|
||||
### 4.4 DTO style
|
||||
|
||||
- Hand-written `type` aliases that mirror a Prisma `select` clause, co-located
|
||||
with the consumer (see `RuleTemplateDto` in
|
||||
`lib/create/fetchTemplates.ts` lines 5–14).
|
||||
- For a feature with both client and server consumers, put the type in
|
||||
`lib/<feature>/types.ts` and import from both sides.
|
||||
|
||||
### 4.5 Standalone scripts
|
||||
|
||||
- Use `tsx` (already a dev dep; entry point `package.json` `prisma.seed`
|
||||
field).
|
||||
- Layout matches `prisma/seed.ts`: `async function main()`, log a one-line
|
||||
success summary, `console.error(e); process.exit(1)` on failure,
|
||||
`await prisma.$disconnect()` in `finally`.
|
||||
- Add an entry to `package.json` `scripts` (e.g.
|
||||
`"templates:import": "tsx scripts/import-templates-xlsx.ts"`).
|
||||
- No shared dotenv loader — rely on env from the shell / Next runtime.
|
||||
- Support a `--dry-run` flag that validates + diffs without writing.
|
||||
|
||||
### 4.6 Tests
|
||||
|
||||
- Vitest under `tests/unit/*.test.ts` for parsers / validators / pure
|
||||
functions (see `tests/unit/createFlowValidation.test.ts`).
|
||||
- API routes are not unit-tested today; cover route behavior indirectly with a
|
||||
`tests/unit/templateRecommendationSchemas.test.ts` (Zod) plus a fixture
|
||||
workbook + importer test under `tests/unit/importTemplatesXlsx.test.ts`.
|
||||
- E2E for the wizard (if needed) goes under `tests/e2e/*.spec.ts` — not
|
||||
required for CR-88 acceptance.
|
||||
- Test utilities: `tests/utils/test-utils.tsx` (`renderWithProviders`); MSW
|
||||
server in `tests/msw/server.ts`. No Prisma mock helper exists; importer test
|
||||
should use a fixture workbook and stub the `prisma` client at the import
|
||||
site.
|
||||
|
||||
### 4.7 Logging
|
||||
|
||||
- Use `logger` from `lib/logger.ts` for any server-side info/warn/error in
|
||||
scripts and route helpers (matches `app/api/auth/magic-link/request/route.ts`
|
||||
lines 14–15, 35–45). No `apiError` helper exists; do not introduce one.
|
||||
|
||||
### 4.8 New deps
|
||||
|
||||
- `xlsx` (SheetJS) is **not** currently in `package.json`. Add it as a
|
||||
**prod** dep only if the importer is invoked from app code; if the importer
|
||||
is script-only, `devDependency` is fine. CR-88's plan calls for a
|
||||
build/CLI-time importer, so `devDependencies` is the right home.
|
||||
|
||||
### 4.9 i18n / `messages/` constraint
|
||||
|
||||
- Card decks and modal copy are currently keyed in
|
||||
`messages/en/<feature>.json` and read via
|
||||
`useMessages().create.<feature>` (`app/contexts/MessagesContext.tsx`,
|
||||
`messages/en/index.ts`).
|
||||
- Only `en` is wired today, so we **don't** have a translation backlog
|
||||
blocking us. The wiring step (§7) replaces `messages/en/create/{communication,
|
||||
membership,rightRail,conflictManagement}.json` card/modal payloads with
|
||||
values served by `GET /api/template-methods` (still keyed by the same
|
||||
message namespace shape so future i18n can layer on if needed). Header
|
||||
strings, button labels, and other purely-static UI copy stay in
|
||||
`messages/en/*`.
|
||||
|
||||
### 4.10 `.cursorrules` scope
|
||||
|
||||
- The repo's `.cursorrules` PascalCase / lowercase normalization rule applies
|
||||
to **React component props only**. It does **not** apply to API query
|
||||
params, request bodies, or DB columns. The recommendation API uses lowercase
|
||||
facet keys throughout (`orgType`, `scale`, `maturity`, `size`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Authoring contract (informs §6 storage + §7 importer)
|
||||
|
||||
The four spreadsheets together imply this row schema (per matrix workbook):
|
||||
|
||||
```ts
|
||||
type MatrixRow = {
|
||||
/** Stable slug derived from `Label`/`Title` (kebab-case, lowercase, ascii).
|
||||
* Used as the card id everywhere downstream. */
|
||||
id: string;
|
||||
|
||||
/** Section this row belongs to. One of: communication, membership,
|
||||
* decisionMaking, conflictManagement. (values is not yet sheet-driven.) */
|
||||
section: "communication" | "membership" | "decisionMaking" | "conflictManagement";
|
||||
|
||||
/** Card-facing copy. Keys differ per section; importer normalizes. */
|
||||
card: {
|
||||
label: string;
|
||||
description: string;
|
||||
/** Section-specific long-form fields (3–4 per section). */
|
||||
modalSections: Record<string, string>;
|
||||
};
|
||||
|
||||
/** Optional numeric scalar fields (e.g. decisionMaking `Consensus Level`). */
|
||||
scalars?: Record<string, number>;
|
||||
|
||||
/** Facet matches (✓ → true, x/blank → false). Keys are canonical facet ids. */
|
||||
facets: {
|
||||
size: Record<"1" | "2_5" | "6_12" | "13_100" | "100_100k", boolean>;
|
||||
orgType: Record<"dao" | "forProfit" | "nonprofit" | "openSource" | "mutualAid" | "workersCoop", boolean>;
|
||||
scale: Record<"global" | "national" | "regional" | "local", boolean>;
|
||||
maturity: Record<"earlyStage" | "growthStage" | "established" | "enterprise", boolean>;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
A sibling **manifest** documents the per-section section-key mapping and
|
||||
column header → canonical facet/scalar key mapping, so the importer can be
|
||||
stable across header rewording.
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage (decided: normalized tables)
|
||||
|
||||
We are introducing two new Prisma models. Hand-typed `COMPOSITION_BY_SLUG` in
|
||||
`prisma/seed.ts` is replaced by template rows that **reference** method slugs.
|
||||
|
||||
```prisma
|
||||
model TemplateMethod {
|
||||
id String @id @default(cuid())
|
||||
section String // communication | membership | decisionMaking | conflictManagement
|
||||
slug String
|
||||
label String
|
||||
description String
|
||||
modalSections Json // { corePrinciple: "...", logisticsAdmin: "...", ... }
|
||||
scalars Json? // { consensusLevel: 0.51 }
|
||||
sortOrder Int @default(0)
|
||||
facets TemplateMethodFacet[]
|
||||
@@unique([section, slug])
|
||||
@@index([section])
|
||||
}
|
||||
|
||||
model TemplateMethodFacet {
|
||||
id String @id @default(cuid())
|
||||
methodId String
|
||||
group String // size | orgType | scale | maturity
|
||||
value String // e.g. "workersCoop"
|
||||
matches Boolean // ✓ → true, x/blank → false
|
||||
weight Float? // optional numeric override for future scoring
|
||||
method TemplateMethod @relation(fields: [methodId], references: [id], onDelete: Cascade)
|
||||
@@unique([methodId, group, value])
|
||||
@@index([group, value, matches])
|
||||
}
|
||||
```
|
||||
|
||||
`RuleTemplate.body` continues to express a **chosen composition** of methods
|
||||
(one or more per section). Curated templates in `prisma/seed.ts` become
|
||||
references to `TemplateMethod.slug` instead of literal copy strings — when
|
||||
copy changes in the spreadsheet, every template that references that slug
|
||||
inherits the new copy.
|
||||
|
||||
A follow-up (out of scope for CR-88) may add a `RuleTemplateMethodLink` join
|
||||
table if templates need ordering or per-template overrides; the current `body`
|
||||
JSON shape is sufficient for the first ship.
|
||||
|
||||
---
|
||||
|
||||
## 7. Importer (`scripts/import-templates-xlsx.ts`)
|
||||
|
||||
Phased plan that the implementation agent can follow top-to-bottom. Mirrors
|
||||
the structure of `prisma/seed.ts` (singleton client, `main()` +
|
||||
`finally { $disconnect }`, `process.exit(1)` on failure).
|
||||
|
||||
1. **Read `.xlsx`** with [`xlsx`](https://www.npmjs.com/package/xlsx) (SheetJS,
|
||||
add as devDependency) from a configurable input dir (default
|
||||
`data/template-matrix/`). The four workbooks live there as committed
|
||||
artifacts, not in `Downloads/`.
|
||||
2. **Schema-validate per section** with Zod schemas that live in
|
||||
`lib/server/validation/templateRecommendationSchemas.ts` so the API and
|
||||
importer share the row shape: required column headers, allowed cell
|
||||
symbols (`✓`, `x`, blank, decimal for `Consensus Level`).
|
||||
3. **Normalize**: kebab-case slug from label, strip
|
||||
`Organization Type:` / `Location:` / `Organizational Maturity:` prefixes,
|
||||
collapse whitespace, normalize curly quotes.
|
||||
4. **Cross-sheet validation**: facet columns must match the canonical 19-column
|
||||
set; unknown columns fail loudly via the importer (use `logger.error`).
|
||||
5. **Diff & upsert** inside `prisma.$transaction([...])`: upsert
|
||||
`TemplateMethod` rows by `(section, slug)`; delete + recreate
|
||||
`TemplateMethodFacet` rows for each method.
|
||||
6. **Emit a JSON snapshot** to `prisma/data/template-matrix.json` so
|
||||
`prisma/seed.ts` can replay imports when the source workbooks aren't
|
||||
available (e.g. CI seed without the spreadsheet checked in).
|
||||
7. **Flags**: `--dry-run` (validate + diff, no writes), `--allow-warnings`
|
||||
(don't fail on the row-32 / row-11 issues in §2.4 while authors are
|
||||
iterating).
|
||||
8. **Tests** in `tests/unit/importTemplatesXlsx.test.ts`: a fixture workbook
|
||||
with two rows per section asserts both validation errors (unknown column,
|
||||
bad symbol, miscategorized cell) and successful normalization. Reuse
|
||||
Vitest patterns from `tests/unit/createFlowValidation.test.ts`.
|
||||
|
||||
Per Ticket 16 and the roadmap, **prefer batch `.xlsx` import** over a live
|
||||
Google Sheets API in production. Authors export to `.xlsx` and a maintainer
|
||||
runs `npm run templates:import` (or CI does on a `data/template-matrix/` change).
|
||||
|
||||
---
|
||||
|
||||
## 8. APIs
|
||||
|
||||
Two read endpoints. Both follow §4.1 conventions exactly: `dbUnavailable()`
|
||||
guard → server helper from `lib/server/templateMethods.ts` →
|
||||
`NextResponse.json({ ... })`.
|
||||
|
||||
### 8.1 `GET /api/templates` (rewrite)
|
||||
|
||||
Query params (all optional):
|
||||
|
||||
- `facet.size=<chipId>` (repeatable)
|
||||
- `facet.orgType=<chipId>` (repeatable)
|
||||
- `facet.scale=<chipId>` (repeatable)
|
||||
- `facet.maturity=<chipId>` (repeatable)
|
||||
|
||||
Behavior:
|
||||
|
||||
- No params → existing curated ordering (`featured`, `sortOrder`, `title`),
|
||||
no scoring.
|
||||
- With facets → score each template by counting matching facets across the
|
||||
methods referenced in its `body`; return ranked `templates` plus an
|
||||
optional `scores` map.
|
||||
|
||||
Response:
|
||||
|
||||
```ts
|
||||
{
|
||||
templates: RuleTemplateDto[],
|
||||
scores?: Record<string, { score: number; matchedFacets: string[] }>
|
||||
}
|
||||
```
|
||||
|
||||
Param parsing helper lives next to `listRuleTemplatesFromDb` in
|
||||
`lib/server/ruleTemplates.ts` (e.g. `parseTemplateFacetsFromSearchParams`).
|
||||
|
||||
### 8.2 `GET /api/template-methods?section=<section>[&facet.*=...]`
|
||||
|
||||
Powers the four card-deck wizard steps and the section-level recommendation
|
||||
view. Response:
|
||||
|
||||
```ts
|
||||
{
|
||||
section: "communication" | "membership" | "decisionMaking" | "conflictManagement",
|
||||
methods: Array<{
|
||||
slug: string;
|
||||
label: string;
|
||||
description: string;
|
||||
modalSections: Record<string, string>;
|
||||
scalars?: Record<string, number>;
|
||||
/** Per-method facet match against the requested facets (omitted when no facets passed). */
|
||||
matches?: { score: number; matchedFacets: string[] };
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
Server helper: `listTemplateMethodsFromDb({ section, facets })` in
|
||||
`lib/server/templateMethods.ts`. Same swallow-and-return-`[]` failure mode as
|
||||
`listRuleTemplatesFromDb`.
|
||||
|
||||
### 8.3 `POST /api/templates/recommend` (follow-up, optional)
|
||||
|
||||
If product wants to send the full `CreateFlowState` (not just facet ids), the
|
||||
body schema **reuses** `createFlowStateSchema` from
|
||||
`lib/server/validation/createFlowSchemas.ts`. Same scoring engine, just a
|
||||
richer input. Skip until §8.1 + §8.2 ship.
|
||||
|
||||
**Empty / partial facets:** never error. Fall back to today's ordering and
|
||||
return all rows.
|
||||
|
||||
---
|
||||
|
||||
## 9. Wizard wiring (UI follow-on, not strictly part of CR-88)
|
||||
|
||||
Once the API exists:
|
||||
|
||||
- `communication-methods` / `membership-methods` / `decision-approaches` /
|
||||
`conflict-management` screens each call
|
||||
`GET /api/template-methods?section=...&facet.*=...`. The card label and
|
||||
modal copy come from the API response, not from
|
||||
`messages/en/create/<section>.json`. Static JSON in those four files is
|
||||
pruned to the page-level strings (header titles, button labels, modal
|
||||
chrome) only.
|
||||
- Selecting a template on the marketing home or `templates/` page can prefill
|
||||
the create flow's `selected*MethodIds` from the template's composition (this
|
||||
closes the `?template=` no-op gap noted in
|
||||
`CreateFlowLayoutClient.tsx`).
|
||||
- Recommendations should never **hide** options from the user — ranking only.
|
||||
Authors expect to see "all 32 decision-making patterns" with the ✓-matching
|
||||
ones surfaced first.
|
||||
|
||||
---
|
||||
|
||||
## 10. Open questions for product before coding
|
||||
|
||||
1. **Should `Values` also be sheet-driven?** Today it's free-text only and
|
||||
not in any of the four matrices. Roadmap implies eventual parity.
|
||||
2. **Scoring vs filtering**: do we want to **hide** non-✓ rows when a facet
|
||||
is set, or only **rank** them? Recommend ranking with a soft cutoff.
|
||||
3. **Per-template featured composition vs library-wide**: should
|
||||
`RuleTemplate` rows continue to exist as named compositions
|
||||
("Consensus", "Elected Board", etc.), or become derived from a
|
||||
"this is the best mix for nonprofit + 13–100 + early stage" scoring? Doc
|
||||
today assumes the former — templates remain curated.
|
||||
4. **Authoring source of truth**: are the `Downloads/*.xlsx` files committed
|
||||
to `data/template-matrix/` going forward, or do they live in a Drive folder
|
||||
pulled by the importer at build time? Recommend committing.
|
||||
5. **Data validation strictness**: the current Decision-making sheet has a
|
||||
miscategorized cell (row 32, see §2.4). Importer should fail by default,
|
||||
with a `--allow-warnings` flag for in-progress edits.
|
||||
|
||||
---
|
||||
|
||||
## 11. Test plan (acceptance for CR-88)
|
||||
|
||||
- [ ] `scripts/import-templates-xlsx.ts` runs end-to-end on the four committed
|
||||
workbooks with no errors and produces the expected DB diff (or JSON
|
||||
snapshot).
|
||||
- [ ] Editing a row in the source workbook and re-running the importer changes
|
||||
the rank order returned by `GET /api/templates?facet.orgType=4`
|
||||
(the `Nonprofit` chip id) without any manual Studio edit.
|
||||
- [ ] `tests/unit/importTemplatesXlsx.test.ts` rejects each documented
|
||||
validation failure (unknown column, bad symbol, miscategorized row).
|
||||
- [ ] `tests/unit/templateRecommendationSchemas.test.ts` exercises the Zod
|
||||
schemas the importer and API share.
|
||||
- [ ] Manual smoke on the four wizard card-deck steps: facet-narrowed
|
||||
ordering surfaces matching cards first; facetless GET returns the
|
||||
full curated list.
|
||||
- [ ] No regression in existing template surfaces (marketing home, templates
|
||||
index, review-template preview).
|
||||
|
||||
---
|
||||
|
||||
## 12. Source files referenced
|
||||
|
||||
- `prisma/schema.prisma` — `RuleTemplate` model (lines 64–73).
|
||||
- `prisma/seed.ts` — current curated composition + xlsx-shaped helpers
|
||||
(lines 1–404).
|
||||
- `app/api/templates/route.ts` — existing GET endpoint (to be rewritten).
|
||||
- `app/api/drafts/me/route.ts` — reference route shape (`dbUnavailable` →
|
||||
`getSessionUser` → `readLimitedJson` → `safeParse` → `jsonFromZodError`).
|
||||
- `lib/server/db.ts` — Prisma singleton (lines 1–18).
|
||||
- `lib/server/responses.ts` — `dbUnavailable()` (lines 1–8).
|
||||
- `lib/server/ruleTemplates.ts` — `listRuleTemplatesFromDb` (lines 9–30).
|
||||
- `lib/server/validation/createFlowSchemas.ts` — schema to reuse for
|
||||
`POST /api/templates/recommend` (lines 47–106).
|
||||
- `lib/server/validation/requestBody.ts` — `readLimitedJson` (lines 13–48).
|
||||
- `lib/server/validation/zodHttp.ts` — `jsonFromZodError` (lines 4–17).
|
||||
- `lib/server/validation/plainJson.ts` — `assertPlainJsonValue` /
|
||||
`DEFAULT_PLAIN_JSON_LIMITS`.
|
||||
- `lib/logger.ts` — server-side `logger`.
|
||||
- `app/(app)/create/types.ts` — `CreateFlowState` and facet fields.
|
||||
- `app/(app)/create/utils/flowSteps.ts` — canonical step order.
|
||||
- `app/(app)/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
|
||||
- `app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
|
||||
derivation pattern (positional `String(i+1)`).
|
||||
- `app/(app)/create/screens/card/CommunicationMethodsScreen.tsx` — section-field
|
||||
contract (`SECTION_FIELDS`).
|
||||
- `messages/en/create/{communitySize,communityStructure,communication,membership,rightRail,conflictManagement}.json` —
|
||||
current static card / chip copy that the matrix supersedes.
|
||||
- `lib/templates/governanceTemplateCatalog.ts`,
|
||||
`lib/templates/templateGridPresentation.ts`,
|
||||
`lib/create/fetchTemplates.ts` — current presentation/DTO layer.
|
||||
- `tests/unit/createFlowValidation.test.ts` — Vitest pattern for new
|
||||
schema/importer tests.
|
||||
- Roadmap: `docs/backend-roadmap.md` §4 (lines 83–85), §13.
|
||||
- Spec: `docs/backend-linear-tickets.md` Ticket 16 (lines 280–304).
|
||||
|
||||
## 13. Source workbooks
|
||||
|
||||
| File | Sheet | Rows | Cols | Section |
|
||||
|---|---|---|---|---|
|
||||
| `Communication Methods.xlsx` | `Current` | 11 cards | 24 | `communication` |
|
||||
| `Group_Membership_Methods.xlsx` | `Current` | 19 cards | 24 | `membership` |
|
||||
| `Decision-making.xlsx` | `Current` | 32 cards | 26 | `decisionMaking` |
|
||||
| `Conflict Management Methods.xlsx` | `Current` | 19 cards | 25 | `conflictManagement` |
|
||||
|
||||
Counts include the header row. Decision-making has 26 columns because of two
|
||||
extra content fields (`Consensus Level`, `Step-by-Step Instructions` vs the
|
||||
4-section pattern of the others).
|
||||
Reference in New Issue
Block a user