fix(recommendation-engine): S4-FIX — opersonliga vyer fungerar utan samtycke via depersonalize(weights)

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 04:02:46 +07:00
parent 05bba93b90
commit 1e18146665
4 changed files with 160 additions and 16 deletions
@@ -211,3 +211,8 @@ export function viewWeights(view: RecommendationView): ScoringWeights {
return DEFAULT_WEIGHTS;
}
}
/** Nollställ de personliga axlarna i en viktmängd — används när samtycke saknas. */
export function depersonalize(weights: ScoringWeights): ScoringWeights {
return { ...weights, memoryFit: 0, tasteFit: 0, cookingAssumptionFit: 0 };
}
@@ -4,6 +4,7 @@ import type { MemoryItem, TasteSignal } from "@app/shared-types";
import {
containsForbiddenCopy,
DEFAULT_WEIGHTS,
depersonalize,
easterSunday,
HEALTH_VIEW_WEIGHTS,
isEventActive,
@@ -403,4 +404,69 @@ describe("S4 rekommendationsvyer", () => {
expect(viewWeights("health").nutritionFit).toBe(HEALTH_VIEW_WEIGHTS.nutritionFit);
expect(viewWeights("pantry").coverage).toBe(PANTRY_VIEW_WEIGHTS.coverage);
});
it("opersonliga vyerna fungerar utan samtycke genom depersonalize", () => {
const nonPersonalCtx: RecommendationContext = {
...viewCtx,
personalizationEnabled: false,
memoryItems: undefined,
tasteSignals: undefined,
cookingAssumptions: undefined,
};
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 },
});
// default utan samtycke = depersonalize(DEFAULT_WEIGHTS) == NON_PERSONALIZED_WEIGHTS
const defaultRanked = rankAll([pantry, healthy, tasty], nonPersonalCtx, depersonalize(viewWeights("default")));
const nonPersonalDefaultRanked = rankAll([pantry, healthy, tasty], nonPersonalCtx, NON_PERSONALIZED_WEIGHTS);
expect(defaultRanked.map((r) => r.recipeId)).toEqual(nonPersonalDefaultRanked.map((r) => r.recipeId));
// vyerna är fortfarande skilda från default även utan samtycke
expect(rankAll([tasty, healthy, pantry], nonPersonalCtx, depersonalize(viewWeights("pantry")))[0]?.recipeId).toBe("pantry");
expect(rankAll([tasty, pantry, healthy], nonPersonalCtx, depersonalize(viewWeights("health")))[0]?.recipeId).toBe("healthy");
// taste-vyn honoreras men personliga axlar är nollade
const tasteWeights = depersonalize(viewWeights("taste"));
expect(tasteWeights.memoryFit).toBe(0);
expect(tasteWeights.tasteFit).toBe(0);
expect(tasteWeights.cookingAssumptionFit).toBe(0);
expect(tasteWeights.taste).toBe(TASTE_VIEW_WEIGHTS.taste);
});
});