Template recommendation implemented

This commit is contained in:
adilallo
2026-04-29 19:24:50 -06:00
parent c4c74ecdb4
commit a4f0c4bf27
20 changed files with 899 additions and 82 deletions
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "TemplateFacet" (
"id" TEXT NOT NULL,
"templateSlug" TEXT NOT NULL,
"group" TEXT NOT NULL,
"value" TEXT NOT NULL,
"matches" BOOLEAN NOT NULL,
CONSTRAINT "TemplateFacet_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "TemplateFacet_templateSlug_group_value_key" ON "TemplateFacet"("templateSlug", "group", "value");
-- CreateIndex
CREATE INDEX "TemplateFacet_templateSlug_idx" ON "TemplateFacet"("templateSlug");
-- CreateIndex
CREATE INDEX "TemplateFacet_group_value_matches_idx" ON "TemplateFacet"("group", "value", "matches");
+21
View File
@@ -111,3 +111,24 @@ model MethodFacet {
@@index([section])
@@index([group, value, matches])
}
/// Template-level recommendation matrix (Template Composition, cols GY). Canonical
/// JSON in `data/templates/templateFacet.json`; rebuilt at seed like
/// `MethodFacet`. One row per `(templateSlug, group, value)` where the matrix
/// marks a fit (✓). `GET /api/templates?facet.*` joins these rows to user facets.
/// See `docs/guides/template-recommendation-matrix.md` (parallel to `MethodFacet` §7).
model TemplateFacet {
id String @id @default(cuid())
/// `RuleTemplate.slug` (e.g. `consensus`, `do-ocracy`).
templateSlug String
/// `size` | `orgType` | `scale` | `maturity` — same as `MethodFacet.group`.
group String
/// Canonical facet value id, e.g. `workersCoop`, `local`.
value String
/// `true` iff the JSON marks a fit; seed only writes `true` rows.
matches Boolean
@@unique([templateSlug, group, value])
@@index([templateSlug])
@@index([group, value, matches])
}
+7
View File
@@ -1,5 +1,6 @@
import { PrismaClient, type Prisma } from "@prisma/client";
import { seedMethodFacets } from "./seed/methodFacets";
import { seedTemplateFacets } from "./seed/templateFacets";
/**
* Curated rule templates for GET /api/templates.
@@ -393,6 +394,12 @@ async function main() {
.map(([section, count]) => `${section}=${count}`)
.join(", ")}`,
);
const templateFacetSeed = await seedTemplateFacets(prisma);
// eslint-disable-next-line no-console -- seed CLI feedback
console.log(
`Seeded TemplateFacet rows: ${templateFacetSeed.rowCount}`,
);
}
main()
+71
View File
@@ -0,0 +1,71 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import type { PrismaClient } from "@prisma/client";
import { FACET_GROUP_IDS } from "../../lib/server/validation/methodFacetsSchemas";
import { templateFacetFileSchema } from "../../lib/server/validation/templateFacetSchema";
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const TEMPLATE_FACET_FILE = path.join(
REPO_ROOT,
"data",
"templates",
"templateFacet.json",
);
type TemplateFacetRow = {
templateSlug: string;
group: string;
value: string;
matches: boolean;
};
async function loadTemplateFacets() {
const raw = await readFile(TEMPLATE_FACET_FILE, "utf8");
const parsed = JSON.parse(raw) as unknown;
const result = templateFacetFileSchema.safeParse(parsed);
if (!result.success) {
throw new Error(
`Invalid template facet file ${TEMPLATE_FACET_FILE}: ${JSON.stringify(
result.error.flatten(),
null,
2,
)}`,
);
}
return result.data;
}
/**
* One row per `(templateSlug, group, value)` where the matrix lists a fit (✓).
* Sparse: omitted cells are not stored (unlike `MethodFacet`, which materializes
* all cells for constant table density).
*/
function flattenTemplateFacets(
data: Awaited<ReturnType<typeof loadTemplateFacets>>,
): TemplateFacetRow[] {
const rows: TemplateFacetRow[] = [];
for (const [templateSlug, row] of Object.entries(data)) {
for (const group of FACET_GROUP_IDS) {
for (const value of row[group]) {
rows.push({ templateSlug, group, value, matches: true });
}
}
}
return rows;
}
/**
* Validates and re-seeds the `TemplateFacet` table from
* `data/templates/templateFacet.json` (Template Composition-2, cols GY).
*/
export async function seedTemplateFacets(
prisma: PrismaClient,
): Promise<{ rowCount: number }> {
const data = await loadTemplateFacets();
const rows = flattenTemplateFacets(data);
await prisma.$transaction([
prisma.templateFacet.deleteMany(),
prisma.templateFacet.createMany({ data: rows }),
]);
return { rowCount: rows.length };
}