S6: i18n-paritet, provenans + eval, copy-skanning, fabricerade-minnes-eval

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 05:20:27 +07:00
parent f86a37dd33
commit 4116653b51
9 changed files with 1346 additions and 53 deletions
@@ -0,0 +1,188 @@
import { describe, expect, it } from "vitest";
import {
buildWhy,
containsForbiddenCopy,
FORBIDDEN_COPY_PATTERNS,
type ProvenanceEntry,
type RecommendationCandidate,
type RecommendationContext,
} from "../src/index.js";
const nutrition = {
kcal: 550,
proteinG: 45,
carbsG: 50,
fatG: 18,
saturatedFatG: 6,
fiberG: 6,
sugarG: 4,
saltG: 1.5,
};
function candidate(overrides: Partial<RecommendationCandidate> = {}): RecommendationCandidate {
return {
recipeId: "r1",
titleSv: "Kycklinggryta",
cuisine: "swedish",
tags: [],
totalTimeMinutes: 30,
nutritionPerPortion: nutrition,
estimatedCostMinorPerPortion: 2200,
ratingAverage: null,
ratingCount: 0,
peakSeasons: ["summer"],
holidayTags: [],
spiceLevel: 1,
coverage: { coverage: 0.75, matches: [], missing: [], expiringUsed: [] },
...overrides,
};
}
const baseCtx: RecommendationContext = {
mealType: "dinner",
persons: 4,
currentSeason: "summer",
activeHolidayTags: [],
isWeekday: true,
favoriteCuisines: ["swedish"],
remainingProteinG: 60,
remainingKcal: 800,
personalizationEnabled: true,
};
const provenanceSet: ProvenanceEntry[][] = [
[],
[{ key: "favoriteCuisine", args: { cuisine: "svensk" } }],
[{ key: "cookedOften", args: { recipe: "pannkakor", count: 3 } }],
[{ key: "expiringIngredient", args: { count: 2, ingredient: "gurkan", days: 1 } }],
[{ key: "fitsProteinGoal", args: {} }],
[{ key: "fitsVegetableGoal", args: {} }],
[{ key: "tastePreference", args: { axis: "spice" } }],
[{ key: "usesStapleYouFinish", args: { ingredient: "ris" } }],
];
describe("S6 förbjuden-copy-skanning av genererade förklaringar", () => {
it("100+ varierade förklaringar innehåller ingen förbjuden copy", () => {
const explanations: string[] = [];
const langs = ["sv-SE", "en-US", "de-DE", "es-ES", "fr-FR"];
const coverages = [0.25, 0.55, 0.75, 0.92, 1];
const proteins = [15, 30, 45, 58];
const ratings = [null, 4.5, 4.9];
const costs = [1200, 2200, 4500];
const daysSince = [null, 2, 14];
for (const lang of langs) {
for (const cov of coverages) {
for (const protein of proteins) {
for (const rating of ratings) {
for (const cost of costs) {
for (const days of daysSince) {
for (const prov of provenanceSet) {
const ctx: RecommendationContext = {
...baseCtx,
isTrainingDay: protein >= 35,
activeHolidayTags: lang === "sv-SE" ? ["midsummer"] : [],
};
const c = candidate({
coverage: {
coverage: cov,
matches: [],
missing: [],
expiringUsed:
cov < 1
? [
{
canonicalIngredientId: "chicken",
displayNameSv: "kycklingen",
required: 400,
unit: "GRAM",
availableInUnit: 500,
covered: true,
optional: false,
mostUrgentDaysLeft: 2,
usesExpiringItem: true,
},
]
: [],
},
nutritionPerPortion: { ...nutrition, proteinG: protein },
estimatedCostMinorPerPortion: cost,
householdRating: rating,
daysSinceLastCooked: days,
});
const parts = {
coverage: cov,
expiry: cov < 1 ? 0.8 : 0,
nutritionFit: protein >= 25 ? 0.85 : 0.4,
taste: 0.6,
rating: rating != null ? 0.9 : 0.5,
season: 1,
holiday: ctx.activeHolidayTags.length > 0 ? 1 : 0,
time: 1,
budget: cost <= 2500 ? 1 : 0.4,
variety: days == null ? 0.8 : days < 7 ? 0 : 1,
weather: 0.5,
craving: 0.5,
memoryFit: 1,
tasteFit: 0,
cookingAssumptionFit: 0,
};
explanations.push(buildWhy(c, ctx, parts, prov, lang));
}
}
}
}
}
}
}
expect(explanations.length).toBeGreaterThanOrEqual(100);
const hits = explanations.filter((e) => containsForbiddenCopy(e));
if (hits.length > 0) {
// eslint-disable-next-line no-console
console.error("Förbjuden copy hittades:", hits.slice(0, 5));
}
expect(hits).toHaveLength(0);
});
it("skam-exempel-korpus flaggas av förbjuden-copy-listan", () => {
const shameExamples = [
"Du har överskridit ditt kalorimål.",
"Bara 200 kcal kvar idag.",
"Begränsa dig nu.",
"Undvik kolhydrater.",
"Du borde inte äta så mycket kött.",
"Skäms över dina matvanor.",
"Din kost är dålig.",
"Du har ätit för mycket socker.",
"Måste sluta med snacks.",
"Överdriv inte.",
"Du får inte äta det där.",
];
const flagged = shameExamples.filter((s) => containsForbiddenCopy(s));
expect(flagged.length).toBeGreaterThanOrEqual(shameExamples.length - 1);
});
it("varje förbjudet mönster matchar minst ett skam-exempel", () => {
const shameExamples = [
"Du har överskridit ditt kalorimål.",
"Bara 200 kcal kvar idag.",
"Begränsa dig nu.",
"Undvik kolhydrater.",
"Du borde inte äta så mycket kött.",
"Skäms över dina matvanor.",
"Din kost är dålig.",
"Du har ätit för mycket socker.",
"Måste sluta med snacks.",
"Överdriv inte.",
"Du får inte äta det där.",
"För lite grönsaker idag.",
"För mycket fett.",
"Skärp dig.",
];
for (const pattern of FORBIDDEN_COPY_PATTERNS) {
const matches = shameExamples.filter((s) => pattern.test(s));
expect(matches.length, `mönster ${pattern.source} matchade inget exempel`).toBeGreaterThan(0);
}
});
});
@@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";
import {
provenanceTemplateLanguages,
whyTemplateLanguages,
buildWhy,
type RecommendationCandidate,
type RecommendationContext,
type ProvenanceEntry,
} from "../src/index.js";
const EXPECTED_LANGS = [
"sv",
"en",
"es",
"it",
"de",
"fr",
"da",
"nb",
"fi",
"nl",
"pl",
"pt",
];
const fullCoverage = { coverage: 1, matches: [], missing: [], expiringUsed: [] };
const nutrition = {
kcal: 550,
proteinG: 45,
carbsG: 50,
fatG: 18,
saturatedFatG: 6,
fiberG: 6,
sugarG: 4,
saltG: 1.5,
};
function candidate(overrides: Partial<RecommendationCandidate> = {}): RecommendationCandidate {
return {
recipeId: "r1",
titleSv: "Kycklinggryta",
cuisine: "swedish",
tags: [],
totalTimeMinutes: 30,
nutritionPerPortion: nutrition,
estimatedCostMinorPerPortion: 2200,
ratingAverage: null,
ratingCount: 0,
peakSeasons: [],
holidayTags: [],
spiceLevel: 1,
coverage: fullCoverage,
...overrides,
};
}
const ctx: RecommendationContext = {
mealType: "dinner",
persons: 4,
currentSeason: "summer",
activeHolidayTags: [],
isWeekday: true,
favoriteCuisines: ["swedish"],
remainingProteinG: 60,
remainingKcal: 800,
personalizationEnabled: false,
};
describe("S6 i18n-paritet", () => {
it("provenansmallarna täcker exakt 12 språk utan dubbletter", () => {
const langsByTemplate = provenanceTemplateLanguages();
expect(Object.keys(langsByTemplate).length).toBeGreaterThan(0);
for (const [key, langs] of Object.entries(langsByTemplate)) {
expect(langs.length, `mall ${key} har dubbletter eller saknar språk`).toBe(EXPECTED_LANGS.length);
expect(new Set(langs).size, `mall ${key} har dubbletter`).toBe(EXPECTED_LANGS.length);
for (const lang of EXPECTED_LANGS) {
expect(langs).toContain(lang);
}
}
});
it("why-mallarna täcker exakt 12 språk utan dubbletter", () => {
const langs = whyTemplateLanguages();
expect(langs.length).toBe(EXPECTED_LANGS.length);
expect(new Set(langs).size).toBe(EXPECTED_LANGS.length);
for (const lang of EXPECTED_LANGS) {
expect(langs).toContain(lang);
}
});
it("buildWhy renderar på rätt språk för varje supporterat språk", () => {
const provenance: ProvenanceEntry[] = [
{ key: "favoriteCuisine", args: { cuisine: "svensk" } },
];
for (const lang of EXPECTED_LANGS) {
const why = buildWhy(candidate(), { ...ctx, personalizationEnabled: true }, { coverage: 1, expiry: 0, nutritionFit: 0.9, taste: 0.5, rating: 0.5, season: 0, holiday: 0, time: 0, budget: 0, variety: 0.5, weather: 0.5, craving: 0.5, memoryFit: 1, tasteFit: 0, cookingAssumptionFit: 0 }, provenance, `${lang}-XX`);
expect(why.length, `tom why för ${lang}`).toBeGreaterThan(0);
// Ingen why får falla tillbaka på svenska fallback om språket finns.
if (lang !== "sv") {
expect(why).not.toBe("En balanserad rätt som passar er profil.");
}
}
});
});