import { readFileSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { DECK_IDS } from "../../lib/create/deckTypes"; import type { DeckId, RecommendedCard } from "../../lib/create/deckTypes"; import { deckData, recommendDeck } from "../../lib/create/deckRecommend"; import { variantsOf } from "../../lib/create/reviewProfiles"; import type { ReviewProfile } from "../../lib/create/reviewProfiles"; import { methodSlugFromTitle } from "../../lib/create/methodSlugFromTitle"; import { resolveMethodId } from "../../lib/create/idAliases"; import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json"; const ROOT = path.resolve(__dirname, "..", ".."); function readJson(rel: string): T { return JSON.parse(readFileSync(path.join(ROOT, rel), "utf8")) as T; } const profiles = readJson("data/create/profiles/real-group-profiles.json"); const founders = readJson("data/create/profiles/one-member-founders.json"); function compact(card: RecommendedCard) { const reason = card.reason.kind === "added" ? `added:${card.reason.answer}` : card.reason.kind === "opened" ? `opened:${card.reason.answer}` : card.reason.answer ? `fit+${card.reason.answer}` : "fit"; return { id: card.id, tag: card.tag, tier: card.tier, reason, ...(card.decidedBy ? { decidedBy: card.decidedBy } : {}), ...(card.alternative ? { alternative: card.alternative } : {}), }; } function picksFor(set: ReviewProfile[]) { return set.flatMap(variantsOf).map((variant) => ({ profile: variant.profile, variant: variant.variant, decks: Object.fromEntries( DECK_IDS.map((deck) => { const result = recommendDeck(deckData(deck), variant.answers); return [ deck, { shown: result.shown.map(compact), waiting: result.waiting.map((card) => card.id), }, ]; }), ), })); } function rowOrderSentence(set: ReviewProfile[]): string { const base = set.flatMap(variantsOf).filter((variant) => variant.variant === "base"); const totals = { only: 0, score: 0, otherKinds: 0, rowOrder: 0 }; for (const deck of DECK_IDS) { const jobs = deckData(deck).engine.jobs.filter((job) => job.displayTier !== "none"); for (const job of jobs) { for (const variant of base) { const winner = recommendDeck(deckData(deck), variant.answers).recommended.find( (card) => card.job === job.id && card.tier !== "answers", ); if (winner?.decidedBy) totals[winner.decidedBy] += 1; } } } const picks = totals.only + totals.score + totals.otherKinds + totals.rowOrder; return `Of ${picks} picks for the 92 groups, row order settled ${totals.rowOrder}, other kinds ${totals.otherKinds} and fit ${totals.score}, and ${totals.only} had one eligible card.`; } const EXAMPLE_DECK_BY_CATEGORY: Record = { Membership: "membership", "Decision-Making": "decisionApproaches", "Decision-making": "decisionApproaches", "Conflict Management": "conflictManagement", Communication: "communication", }; function chosenCards(documentKey: string): { total: number; ids: string[] } { const doc = ( useCasesCompletedRules as Record< string, { document: { sections: { categoryName: string; entries: { title: string }[] }[] } } > )[documentKey]; const live = new Map( DECK_IDS.map((deck) => [deck, new Set(Object.keys(deckData(deck).engine.cards))]), ); let total = 0; const ids: string[] = []; for (const section of doc.document.sections) { const deck = EXAMPLE_DECK_BY_CATEGORY[section.categoryName]; if (!deck) continue; for (const entry of section.entries) { total += 1; const slug = methodSlugFromTitle(entry.title); const resolved = resolveMethodId(deck, slug); if (resolved && live.get(resolved.deck)?.has(resolved.id)) ids.push(resolved.id); } } return { total, ids }; } describe("real-group profiles", () => { it("matches the reviewed 2 Nov package run", () => { const expected = readJson<{ picks: unknown[] }>("tests/fixtures/profiles-picks.json"); expect(picksFor([...profiles, ...founders])).toEqual(expected.picks); }); it("reports how often row order settled a pick", () => { const summary = readFileSync( path.join(ROOT, "tests/fixtures/profiles-summary.md"), "utf8", ); expect(summary).toContain(rowOrderSentence(profiles)); expect(profiles).toHaveLength(92); expect(founders.length).toBeGreaterThan(0); expect(JSON.stringify(profiles)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/); expect(JSON.stringify(founders)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/); }); it("counts how many example-rule cards the engine recommends", () => { const base = profiles.flatMap(variantsOf).filter((variant) => variant.variant === "base"); const report: Record = {}; for (const key of ["mutualAidColorado", "foodNotBombs", "boulderCountyStreetMedics"] as const) { const chosen = chosenCards(key); let best = 0; for (const variant of base) { const recommended = new Set(); for (const deck of DECK_IDS) { for (const card of recommendDeck(deckData(deck), variant.answers).recommended) { recommended.add(card.id); } } let hit = 0; for (const id of chosen.ids) if (recommended.has(id)) hit += 1; if (hit > best) best = hit; } report[key] = { chosen: chosen.total, resolved: chosen.ids.length, recommendedOnMost: best, }; } // The team's own Rule is not in the repo, so this locks the three example // Rules. A change that moves a recommendation changes recommendedOnMost. expect(report).toEqual({ mutualAidColorado: { chosen: 7, resolved: 6, recommendedOnMost: 4 }, foodNotBombs: { chosen: 8, resolved: 8, recommendedOnMost: 5 }, boulderCountyStreetMedics: { chosen: 9, resolved: 8, recommendedOnMost: 5 }, }); }); });