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:
Sven (AAMOS AI)
2026-08-10 05:17:43 +07:00
parent 881c5bb1e3
commit f4a603e977
8 changed files with 605 additions and 103 deletions
@@ -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);
});
});