feat(diet): lagg till nordisk/carnivore/paleo + LCHF som matvanor med mjuk rankning
- DIET_PATTERNS: +nordic, +carnivore, +paleo (migration 0029). Ingen separat "lchf" - LCHF tacks av befintliga "low_carb" for att undvika dubblett med keto/low_carb. UI visar low_carb med etiketten "LCHF". - Mobil onboarding + profil: 9 rena val (allatare..carnivore), 12 sprak. - recommendation-engine: ny mjuk dietAffinity-poangterm (0-1, neutral 0.5). Rankar recept mot matvanan - ALDRIG hart filter (uteslutande matvanor, religion, allergener hanteras redan deterministiskt i sakerhetsfiltret). low_carb/keto: lag kolhydrat. carnivore: hog protein + lag kolhydrat. paleo: glutenfri/laktosfri-proxyer. nordic: svenskt/nordiskt kok + sasong. - 6 nya tester (35 grona). Full typecheck 20/20 + mobil gron.
This commit is contained in:
@@ -486,3 +486,72 @@ describe("S4 rekommendationsvyer", () => {
|
||||
expect(tasteWeights.taste).toBe(TASTE_VIEW_WEIGHTS.taste);
|
||||
});
|
||||
});
|
||||
|
||||
describe("diet-affinitet (spec §diet, mjuk rankning)", () => {
|
||||
it("utan matvana är affiniteten neutral (0.5)", () => {
|
||||
const scored = scoreCandidate(candidate(), ctx);
|
||||
expect(scored.parts.dietAffinity).toBe(0.5);
|
||||
});
|
||||
|
||||
it("low_carb/LCHF premierar låg kolhydrat", () => {
|
||||
const low = scoreCandidate(candidate({ nutritionPerPortion: { ...nutrition, carbsG: 10 } }), {
|
||||
...ctx,
|
||||
dietPattern: "low_carb",
|
||||
});
|
||||
const high = scoreCandidate(candidate({ nutritionPerPortion: { ...nutrition, carbsG: 60 } }), {
|
||||
...ctx,
|
||||
dietPattern: "low_carb",
|
||||
});
|
||||
expect(low.parts.dietAffinity).toBe(1);
|
||||
expect(high.parts.dietAffinity).toBe(0.15);
|
||||
});
|
||||
|
||||
it("explicit low_carb-tagg räcker oavsett makro", () => {
|
||||
const tagged = scoreCandidate(
|
||||
candidate({ tags: ["low_carb"], nutritionPerPortion: { ...nutrition, carbsG: 55 } }),
|
||||
{ ...ctx, dietPattern: "keto" },
|
||||
);
|
||||
expect(tagged.parts.dietAffinity).toBe(1);
|
||||
});
|
||||
|
||||
it("carnivore premierar högt protein + låg kolhydrat", () => {
|
||||
const meaty = scoreCandidate(
|
||||
candidate({ nutritionPerPortion: { ...nutrition, proteinG: 45, carbsG: 5 } }),
|
||||
{ ...ctx, dietPattern: "carnivore" },
|
||||
);
|
||||
const carby = scoreCandidate(
|
||||
candidate({ nutritionPerPortion: { ...nutrition, proteinG: 12, carbsG: 70 } }),
|
||||
{ ...ctx, dietPattern: "carnivore" },
|
||||
);
|
||||
expect(meaty.parts.dietAffinity).toBe(1);
|
||||
expect(meaty.parts.dietAffinity).toBeGreaterThan(carby.parts.dietAffinity ?? 0);
|
||||
});
|
||||
|
||||
it("nordisk premierar svenskt/nordiskt kök", () => {
|
||||
const swedish = scoreCandidate(candidate({ cuisine: "swedish" }), {
|
||||
...ctx,
|
||||
dietPattern: "nordic",
|
||||
});
|
||||
const italian = scoreCandidate(candidate({ cuisine: "italian" }), {
|
||||
...ctx,
|
||||
dietPattern: "nordic",
|
||||
});
|
||||
expect(swedish.parts.dietAffinity).toBe(0.85);
|
||||
expect(swedish.parts.dietAffinity).toBeGreaterThan(italian.parts.dietAffinity ?? 0);
|
||||
});
|
||||
|
||||
it("är mjuk rankning, inte hårt filter – ej matchande recept finns kvar", () => {
|
||||
const lowCarbRecipe = candidate({
|
||||
recipeId: "lowcarb",
|
||||
nutritionPerPortion: { ...nutrition, carbsG: 8 },
|
||||
});
|
||||
const pastaRecipe = candidate({
|
||||
recipeId: "pasta",
|
||||
nutritionPerPortion: { ...nutrition, carbsG: 80 },
|
||||
});
|
||||
const ranked = rankAll([pastaRecipe, lowCarbRecipe], { ...ctx, dietPattern: "low_carb" });
|
||||
// Båda recepten finns kvar (inget filtreras bort), men lågkolhydrat rankas högre.
|
||||
expect(ranked).toHaveLength(2);
|
||||
expect(ranked[0]?.recipeId).toBe("lowcarb");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user