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

+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 };
}
/**