Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.

The real-group profiles stay as a test, so a change that moves a pick fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilalloandCursor committed 2026-11-04 14:22:54 -07:00
1 parent 96ca15d8e3
commit bcbc6f30d9
89 files changed
+36555 -2828

No files matched your search

+5 -11
View File
@@ -21,7 +21,7 @@ export type CatalogMethodDto = {
};
export type CatalogCoreValueDto = {
/** 1-based position in `coreValues.json` (`"1"`, `"2"`, …). */
/** Stable id from `coreValues.json` `values[].id`. */
id: string;
label: string;
meaning: string;
@@ -107,23 +107,17 @@ export function getCatalogMethod(
return row ? rowToCatalogMethod(row) : null;
}
/** All preset core values, in `coreValues.json` order (ids `"1"` … `"n"`). */
/** All preset core values, in `coreValues.json` order, keyed by stable id. */
export function listCatalogCoreValues(): CatalogCoreValueDto[] {
const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return [];
const out: CatalogCoreValueDto[] = [];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const id = String(i + 1);
if (typeof row === "string") {
out.push({ id, label: row, meaning: "", signals: "" });
continue;
}
for (const row of values) {
if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>;
if (typeof o.label !== "string") continue;
if (typeof o.id !== "string" || typeof o.label !== "string") continue;
out.push({
id,
id: o.id,
label: o.label,
meaning: typeof o.meaning === "string" ? o.meaning : "",
signals: typeof o.signals === "string" ? o.signals : "",
+26 -41
View File
@@ -1,3 +1,5 @@
import { groupAnswersFromRequestedFacets } from "../create/groupAnswers";
import { deckData, recommendDeck } from "../create/deckRecommend";
import { prisma } from "./db";
import { isDatabaseConfigured } from "./env";
import {
@@ -27,57 +29,40 @@ export type ListMethodRecommendationsResult = {
};
/**
* Returns the per-method match scores for `section`, given `facets`.
* Returns `null` so callers can fall back to messages-file order when DB
* is unavailable or the query fails.
*
* Notes:
* - Empty facets ⇒ `rankedSlugs: []`, `matchesBySlug: {}` (caller falls back
* to authoring order).
* - Sort is `score` desc only — re-stabilising into authoring order is the
* caller's job (the wizard already iterates the on-disk `methods[]` array).
* Ranks one deck with the job rule. Empty facets return no scores so callers
* keep deck order. The first maturity value is the only one that scores.
* Scores decrease along the engine's display order so a score sort matches it.
*/
export async function listMethodRecommendations(args: {
section: SectionId;
facets: RequestedFacets;
}): Promise<ListMethodRecommendationsResult | null> {
if (!isDatabaseConfigured()) return null;
const requested = flattenRequestedFacets(args.facets);
if (requested.length === 0) {
return { rankedSlugs: [], matchesBySlug: {} };
}
try {
const rows = await prisma.methodFacet.findMany({
where: {
section: args.section,
matches: true,
OR: requested.map(({ group, value }) => ({ group, value })),
},
select: { slug: true, group: true, value: true },
});
const matchesBySlug: Record<string, MethodRanking["matches"]> = {};
for (const row of rows) {
const key = `${row.group}:${row.value}`;
const entry =
matchesBySlug[row.slug] ??
(matchesBySlug[row.slug] = { score: 0, matchedFacets: [] });
if (!entry.matchedFacets.includes(key)) {
entry.matchedFacets.push(key);
entry.score += 1;
}
}
const rankedSlugs = Object.entries(matchesBySlug)
.sort(([, a], [, b]) => b.score - a.score)
.map(([slug]) => slug);
return { rankedSlugs, matchesBySlug };
} catch {
return null;
}
const answers = groupAnswersFromRequestedFacets(args.facets);
const recommendation = recommendDeck(deckData(args.section), answers);
const orderedIds = recommendation.fallback
? recommendation.seeAll
: [
...recommendation.shown.map((card) => card.id),
...recommendation.seeAll,
];
const matchesBySlug: Record<string, MethodRanking["matches"]> = {};
orderedIds.forEach((id, index) => {
const card = recommendation.recommended.find((row) => row.id === id);
const matched =
card?.reason.kind === "fit" || card?.reason.kind === "opened"
? card.reason.matched
: [];
matchesBySlug[id] = {
score: orderedIds.length - index,
matchedFacets: matched,
};
});
return { rankedSlugs: orderedIds, matchesBySlug };
}
/**
@@ -115,6 +115,16 @@ export const createFlowStateSchema = z
selectedOrganizationTypeIds: z.array(z.string()).optional(),
selectedScaleIds: z.array(z.string()).optional(),
selectedMaturityIds: z.array(z.string()).optional(),
setupTraits: z
.object({
network: z.boolean().optional(),
nonvoters: z.boolean().optional(),
employer: z.boolean().optional(),
sponsor: z.boolean().optional(),
records: z.boolean().optional(),
risky: z.boolean().optional(),
})
.optional(),
communityStructureChipSnapshots:
communityStructureChipSnapshotsSchema.optional(),
selectedCoreValueIds: z.array(z.string()).max(200).optional(),