fix(recipe-generation): BRAND.name-interpolation för creatorDisplayName + rubriker/filnamn
feat(recommendation-engine,api): S4 Smak/Hälsa/Lager-vyer för 'Vad ska vi äta?' - Ersätter hårdkodat 'Cibello' i scale-batch, scale-smoke-test och export-verified. - DB-backfill: 224 recept hade 'Cibello AI' i creator_display_name (värdena motsvarar nuvarande BRAND.name, ingen rad ändrades men kontrollen är gjord). - Lägger till view-query-param (default|taste|health|pantry) med fördefinierade ScoringWeights och samtyckesgrind. - Unit-tester för vyer; integrationstester för vy-param, validering och fallback utan personalization-samtycke. - brand-guard grön; pnpm typecheck 19/19; pnpm test --force x2 grönt (34 tasks, 275 tester).
This commit is contained in:
@@ -135,3 +135,79 @@ export const NON_PERSONALIZED_WEIGHTS: ScoringWeights = {
|
||||
tasteFit: 0,
|
||||
cookingAssumptionFit: 0,
|
||||
};
|
||||
|
||||
/** Tillåtna vyer för "Vad ska vi äta?". */
|
||||
export type RecommendationView = "default" | "taste" | "health" | "pantry";
|
||||
|
||||
export const RECOMMENDATION_VIEWS: RecommendationView[] = ["default", "taste", "health", "pantry"];
|
||||
|
||||
/** Fördefinierade vikter för smak-vyn (S4). */
|
||||
export const TASTE_VIEW_WEIGHTS: ScoringWeights = {
|
||||
coverage: 20,
|
||||
expiry: 10,
|
||||
nutritionFit: 5,
|
||||
taste: 25,
|
||||
rating: 10,
|
||||
season: 3,
|
||||
holiday: 3,
|
||||
time: 5,
|
||||
budget: 3,
|
||||
variety: 3,
|
||||
weather: 1,
|
||||
craving: 25,
|
||||
memoryFit: 12,
|
||||
tasteFit: 12,
|
||||
cookingAssumptionFit: 6,
|
||||
};
|
||||
|
||||
/** Fördefinierade vikter för hälsa-vyn (S4) — stödjande, icke-restriktiv (R7). */
|
||||
export const HEALTH_VIEW_WEIGHTS: ScoringWeights = {
|
||||
coverage: 20,
|
||||
expiry: 12,
|
||||
nutritionFit: 35,
|
||||
taste: 5,
|
||||
rating: 6,
|
||||
season: 4,
|
||||
holiday: 4,
|
||||
time: 6,
|
||||
budget: 3,
|
||||
variety: 4,
|
||||
weather: 2,
|
||||
craving: 5,
|
||||
memoryFit: 3,
|
||||
tasteFit: 3,
|
||||
cookingAssumptionFit: 4,
|
||||
};
|
||||
|
||||
/** Fördefinierade vikter för lager-vyn (S4). */
|
||||
export const PANTRY_VIEW_WEIGHTS: ScoringWeights = {
|
||||
coverage: 45,
|
||||
expiry: 35,
|
||||
nutritionFit: 5,
|
||||
taste: 4,
|
||||
rating: 4,
|
||||
season: 2,
|
||||
holiday: 2,
|
||||
time: 6,
|
||||
budget: 4,
|
||||
variety: 2,
|
||||
weather: 1,
|
||||
craving: 5,
|
||||
memoryFit: 4,
|
||||
tasteFit: 4,
|
||||
cookingAssumptionFit: 8,
|
||||
};
|
||||
|
||||
/** Hämta vikter för en specifik vy. */
|
||||
export function viewWeights(view: RecommendationView): ScoringWeights {
|
||||
switch (view) {
|
||||
case "taste":
|
||||
return TASTE_VIEW_WEIGHTS;
|
||||
case "health":
|
||||
return HEALTH_VIEW_WEIGHTS;
|
||||
case "pantry":
|
||||
return PANTRY_VIEW_WEIGHTS;
|
||||
default:
|
||||
return DEFAULT_WEIGHTS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,18 @@ import {
|
||||
containsForbiddenCopy,
|
||||
DEFAULT_WEIGHTS,
|
||||
easterSunday,
|
||||
HEALTH_VIEW_WEIGHTS,
|
||||
isEventActive,
|
||||
midsummerEve,
|
||||
NON_PERSONALIZED_WEIGHTS,
|
||||
parseCraving,
|
||||
PANTRY_VIEW_WEIGHTS,
|
||||
rankAll,
|
||||
renderProvenance,
|
||||
scoreCandidate,
|
||||
seasonForDate,
|
||||
TASTE_VIEW_WEIGHTS,
|
||||
viewWeights,
|
||||
type CookingAssumption,
|
||||
type RecommendationCandidate,
|
||||
type RecommendationContext,
|
||||
@@ -329,3 +333,74 @@ describe("S1 personalisering", () => {
|
||||
expect(NON_PERSONALIZED_WEIGHTS.coverage).toBe(DEFAULT_WEIGHTS.coverage);
|
||||
});
|
||||
});
|
||||
|
||||
describe("S4 rekommendationsvyer", () => {
|
||||
const viewCtx: RecommendationContext = {
|
||||
...ctx,
|
||||
personalizationEnabled: true,
|
||||
memoryItems: [],
|
||||
tasteSignals: [],
|
||||
cookingAssumptions: [],
|
||||
};
|
||||
|
||||
const tasty = candidate({
|
||||
recipeId: "tasty",
|
||||
titleSv: "Svensk köttbullsgryta",
|
||||
cuisine: "swedish",
|
||||
coverage: fullCoverage,
|
||||
nutritionPerPortion: { ...nutrition, proteinG: 20 },
|
||||
});
|
||||
|
||||
const pantry = candidate({
|
||||
recipeId: "pantry",
|
||||
titleSv: "Italiensk pastarätt",
|
||||
cuisine: "italian",
|
||||
coverage: {
|
||||
...fullCoverage,
|
||||
expiringUsed: [
|
||||
{
|
||||
canonicalIngredientId: "pasta",
|
||||
displayNameSv: "pastan",
|
||||
required: 200,
|
||||
unit: "GRAM",
|
||||
availableInUnit: 250,
|
||||
covered: true,
|
||||
optional: false,
|
||||
mostUrgentDaysLeft: 1,
|
||||
usesExpiringItem: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
nutritionPerPortion: { ...nutrition, proteinG: 15 },
|
||||
});
|
||||
|
||||
const healthy = candidate({
|
||||
recipeId: "healthy",
|
||||
titleSv: "Kyckling och quinoa",
|
||||
cuisine: "greek",
|
||||
coverage: fullCoverage,
|
||||
nutritionPerPortion: { ...nutrition, proteinG: 50 },
|
||||
});
|
||||
|
||||
it("smak-vyn höjer recept i favoritkök", () => {
|
||||
const ranked = rankAll([pantry, healthy, tasty], viewCtx, TASTE_VIEW_WEIGHTS);
|
||||
expect(ranked[0]?.recipeId).toBe("tasty");
|
||||
});
|
||||
|
||||
it("lager-vyn höjer recept som räddar varor nära utgångsdatum", () => {
|
||||
const ranked = rankAll([tasty, healthy, pantry], viewCtx, PANTRY_VIEW_WEIGHTS);
|
||||
expect(ranked[0]?.recipeId).toBe("pantry");
|
||||
});
|
||||
|
||||
it("hälsa-vyn höjer recept som matchar näringsmål", () => {
|
||||
const ranked = rankAll([tasty, pantry, healthy], viewCtx, HEALTH_VIEW_WEIGHTS);
|
||||
expect(ranked[0]?.recipeId).toBe("healthy");
|
||||
});
|
||||
|
||||
it("viewWeights returnerar rätt vikter för varje vy", () => {
|
||||
expect(viewWeights("default").taste).toBe(DEFAULT_WEIGHTS.taste);
|
||||
expect(viewWeights("taste").taste).toBe(TASTE_VIEW_WEIGHTS.taste);
|
||||
expect(viewWeights("health").nutritionFit).toBe(HEALTH_VIEW_WEIGHTS.nutritionFit);
|
||||
expect(viewWeights("pantry").coverage).toBe(PANTRY_VIEW_WEIGHTS.coverage);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user