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:
@@ -328,6 +328,7 @@ export async function recommendationRoutes(app: FastifyInstance) {
|
|||||||
activeHolidayTags,
|
activeHolidayTags,
|
||||||
isWeekday: today.getUTCDay() >= 1 && today.getUTCDay() <= 4,
|
isWeekday: today.getUTCDay() >= 1 && today.getUTCDay() <= 4,
|
||||||
favoriteCuisines: myPrefs?.favoriteCuisines ?? [],
|
favoriteCuisines: myPrefs?.favoriteCuisines ?? [],
|
||||||
|
dietPattern: myPrefs?.dietPattern ?? undefined,
|
||||||
cravingTags: craving?.tags,
|
cravingTags: craving?.tags,
|
||||||
cravingCuisine: craving?.cuisine,
|
cravingCuisine: craving?.cuisine,
|
||||||
cravingMaxKcal: craving?.maxKcal,
|
cravingMaxKcal: craving?.maxKcal,
|
||||||
|
|||||||
@@ -40,7 +40,17 @@ const GOALS = [
|
|||||||
"cook_more",
|
"cook_more",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const DIETS = ["omnivore", "flexitarian", "pescatarian", "vegetarian", "vegan"] as const;
|
const DIETS = [
|
||||||
|
"omnivore",
|
||||||
|
"flexitarian",
|
||||||
|
"pescatarian",
|
||||||
|
"vegetarian",
|
||||||
|
"vegan",
|
||||||
|
"nordic",
|
||||||
|
"paleo",
|
||||||
|
"low_carb",
|
||||||
|
"carnivore",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const RELIGIOUS = ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"] as const;
|
const RELIGIOUS = ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,17 @@ const CONSENT_KINDS = [
|
|||||||
"push_notifications",
|
"push_notifications",
|
||||||
"product_analytics",
|
"product_analytics",
|
||||||
] as const;
|
] as const;
|
||||||
const DIETS = ["omnivore", "flexitarian", "pescatarian", "vegetarian", "vegan"] as const;
|
const DIETS = [
|
||||||
|
"omnivore",
|
||||||
|
"flexitarian",
|
||||||
|
"pescatarian",
|
||||||
|
"vegetarian",
|
||||||
|
"vegan",
|
||||||
|
"nordic",
|
||||||
|
"paleo",
|
||||||
|
"low_carb",
|
||||||
|
"carnivore",
|
||||||
|
] as const;
|
||||||
const ALLERGENS = ["gluten", "milk", "eggs", "tree_nuts", "peanuts", "fish", "crustaceans", "soy", "sesame"] as const;
|
const ALLERGENS = ["gluten", "milk", "eggs", "tree_nuts", "peanuts", "fish", "crustaceans", "soy", "sesame"] as const;
|
||||||
const RELIGIOUS = ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"] as const;
|
const RELIGIOUS = ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetar",
|
"onboarding.diet.pescatarian": "Pescetar",
|
||||||
"onboarding.diet.vegan": "Veganer",
|
"onboarding.diet.vegan": "Veganer",
|
||||||
"onboarding.diet.vegetarian": "Vegetar",
|
"onboarding.diet.vegetarian": "Vegetar",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Hvad er dit vigtigste mål?",
|
"onboarding.goal": "Hvad er dit vigtigste mål?",
|
||||||
"onboarding.goal.build_muscle": "Opbygge muskler",
|
"onboarding.goal.build_muscle": "Opbygge muskler",
|
||||||
"onboarding.goal.cook_more": "Lave mere mad hjemme",
|
"onboarding.goal.cook_more": "Lave mere mad hjemme",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetarier",
|
"onboarding.diet.pescatarian": "Pescetarier",
|
||||||
"onboarding.diet.vegan": "Veganer",
|
"onboarding.diet.vegan": "Veganer",
|
||||||
"onboarding.diet.vegetarian": "Vegetarier",
|
"onboarding.diet.vegetarian": "Vegetarier",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Was ist dein wichtigstes Ziel?",
|
"onboarding.goal": "Was ist dein wichtigstes Ziel?",
|
||||||
"onboarding.goal.build_muscle": "Muskeln aufbauen",
|
"onboarding.goal.build_muscle": "Muskeln aufbauen",
|
||||||
"onboarding.goal.cook_more": "Öfter selbst kochen",
|
"onboarding.goal.cook_more": "Öfter selbst kochen",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescatarian",
|
"onboarding.diet.pescatarian": "Pescatarian",
|
||||||
"onboarding.diet.vegan": "Vegan",
|
"onboarding.diet.vegan": "Vegan",
|
||||||
"onboarding.diet.vegetarian": "Vegetarian",
|
"onboarding.diet.vegetarian": "Vegetarian",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "What is your main goal?",
|
"onboarding.goal": "What is your main goal?",
|
||||||
"onboarding.goal.build_muscle": "Build muscle",
|
"onboarding.goal.build_muscle": "Build muscle",
|
||||||
"onboarding.goal.cook_more": "Cook more at home",
|
"onboarding.goal.cook_more": "Cook more at home",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetariano",
|
"onboarding.diet.pescatarian": "Pescetariano",
|
||||||
"onboarding.diet.vegan": "Vegano",
|
"onboarding.diet.vegan": "Vegano",
|
||||||
"onboarding.diet.vegetarian": "Vegetariano",
|
"onboarding.diet.vegetarian": "Vegetariano",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "¿Cuál es tu objetivo principal?",
|
"onboarding.goal": "¿Cuál es tu objetivo principal?",
|
||||||
"onboarding.goal.build_muscle": "Ganar músculo",
|
"onboarding.goal.build_muscle": "Ganar músculo",
|
||||||
"onboarding.goal.cook_more": "Cocinar más en casa",
|
"onboarding.goal.cook_more": "Cocinar más en casa",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetaristi",
|
"onboarding.diet.pescatarian": "Pescetaristi",
|
||||||
"onboarding.diet.vegan": "Vegaani",
|
"onboarding.diet.vegan": "Vegaani",
|
||||||
"onboarding.diet.vegetarian": "Kasvissyöjä",
|
"onboarding.diet.vegetarian": "Kasvissyöjä",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Mikä on tärkein tavoitteesi?",
|
"onboarding.goal": "Mikä on tärkein tavoitteesi?",
|
||||||
"onboarding.goal.build_muscle": "Kasvattaa lihasta",
|
"onboarding.goal.build_muscle": "Kasvattaa lihasta",
|
||||||
"onboarding.goal.cook_more": "Kokata enemmän kotona",
|
"onboarding.goal.cook_more": "Kokata enemmän kotona",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescétarien",
|
"onboarding.diet.pescatarian": "Pescétarien",
|
||||||
"onboarding.diet.vegan": "Végane",
|
"onboarding.diet.vegan": "Végane",
|
||||||
"onboarding.diet.vegetarian": "Végétarien",
|
"onboarding.diet.vegetarian": "Végétarien",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Quel est votre objectif principal ?",
|
"onboarding.goal": "Quel est votre objectif principal ?",
|
||||||
"onboarding.goal.build_muscle": "Prendre du muscle",
|
"onboarding.goal.build_muscle": "Prendre du muscle",
|
||||||
"onboarding.goal.cook_more": "Cuisiner plus à la maison",
|
"onboarding.goal.cook_more": "Cuisiner plus à la maison",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetariano",
|
"onboarding.diet.pescatarian": "Pescetariano",
|
||||||
"onboarding.diet.vegan": "Vegano",
|
"onboarding.diet.vegan": "Vegano",
|
||||||
"onboarding.diet.vegetarian": "Vegetariano",
|
"onboarding.diet.vegetarian": "Vegetariano",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Qual è il tuo obiettivo principale?",
|
"onboarding.goal": "Qual è il tuo obiettivo principale?",
|
||||||
"onboarding.goal.build_muscle": "Aumentare la massa muscolare",
|
"onboarding.goal.build_muscle": "Aumentare la massa muscolare",
|
||||||
"onboarding.goal.cook_more": "Cucinare di più a casa",
|
"onboarding.goal.cook_more": "Cucinare di più a casa",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetarianer",
|
"onboarding.diet.pescatarian": "Pescetarianer",
|
||||||
"onboarding.diet.vegan": "Veganer",
|
"onboarding.diet.vegan": "Veganer",
|
||||||
"onboarding.diet.vegetarian": "Vegetarianer",
|
"onboarding.diet.vegetarian": "Vegetarianer",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Hva er ditt viktigste mål?",
|
"onboarding.goal": "Hva er ditt viktigste mål?",
|
||||||
"onboarding.goal.build_muscle": "Bygge muskler",
|
"onboarding.goal.build_muscle": "Bygge muskler",
|
||||||
"onboarding.goal.cook_more": "Lage mer mat hjemme",
|
"onboarding.goal.cook_more": "Lage mer mat hjemme",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescotariër",
|
"onboarding.diet.pescatarian": "Pescotariër",
|
||||||
"onboarding.diet.vegan": "Veganist",
|
"onboarding.diet.vegan": "Veganist",
|
||||||
"onboarding.diet.vegetarian": "Vegetariër",
|
"onboarding.diet.vegetarian": "Vegetariër",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Wat is je belangrijkste doel?",
|
"onboarding.goal": "Wat is je belangrijkste doel?",
|
||||||
"onboarding.goal.build_muscle": "Spieren opbouwen",
|
"onboarding.goal.build_muscle": "Spieren opbouwen",
|
||||||
"onboarding.goal.cook_more": "Vaker thuis koken",
|
"onboarding.goal.cook_more": "Vaker thuis koken",
|
||||||
|
|||||||
@@ -183,6 +183,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Peskatarianin",
|
"onboarding.diet.pescatarian": "Peskatarianin",
|
||||||
"onboarding.diet.vegan": "Weganin",
|
"onboarding.diet.vegan": "Weganin",
|
||||||
"onboarding.diet.vegetarian": "Wegetarianin",
|
"onboarding.diet.vegetarian": "Wegetarianin",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Jaki jest Twój główny cel?",
|
"onboarding.goal": "Jaki jest Twój główny cel?",
|
||||||
"onboarding.goal.build_muscle": "Zbudować mięśnie",
|
"onboarding.goal.build_muscle": "Zbudować mięśnie",
|
||||||
"onboarding.goal.cook_more": "Częściej gotować w domu",
|
"onboarding.goal.cook_more": "Częściej gotować w domu",
|
||||||
|
|||||||
@@ -177,6 +177,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetariano",
|
"onboarding.diet.pescatarian": "Pescetariano",
|
||||||
"onboarding.diet.vegan": "Vegano",
|
"onboarding.diet.vegan": "Vegano",
|
||||||
"onboarding.diet.vegetarian": "Vegetariano",
|
"onboarding.diet.vegetarian": "Vegetariano",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordic",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Qual é o seu objetivo principal?",
|
"onboarding.goal": "Qual é o seu objetivo principal?",
|
||||||
"onboarding.goal.build_muscle": "Ganhar músculo",
|
"onboarding.goal.build_muscle": "Ganhar músculo",
|
||||||
"onboarding.goal.cook_more": "Cozinhar mais em casa",
|
"onboarding.goal.cook_more": "Cozinhar mais em casa",
|
||||||
|
|||||||
@@ -185,6 +185,10 @@
|
|||||||
"onboarding.diet.pescatarian": "Pescetarian",
|
"onboarding.diet.pescatarian": "Pescetarian",
|
||||||
"onboarding.diet.vegan": "Vegan",
|
"onboarding.diet.vegan": "Vegan",
|
||||||
"onboarding.diet.vegetarian": "Vegetarian",
|
"onboarding.diet.vegetarian": "Vegetarian",
|
||||||
|
"onboarding.diet.carnivore": "Carnivore",
|
||||||
|
"onboarding.diet.low_carb": "LCHF",
|
||||||
|
"onboarding.diet.nordic": "Nordisk",
|
||||||
|
"onboarding.diet.paleo": "Paleo",
|
||||||
"onboarding.goal": "Vad är ditt främsta mål?",
|
"onboarding.goal": "Vad är ditt främsta mål?",
|
||||||
"onboarding.goal.build_muscle": "Bygga muskler",
|
"onboarding.goal.build_muscle": "Bygga muskler",
|
||||||
"onboarding.goal.cook_more": "Laga mer hemma",
|
"onboarding.goal.cook_more": "Laga mer hemma",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TYPE "public"."diet_pattern" ADD VALUE 'nordic';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."diet_pattern" ADD VALUE 'carnivore';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."diet_pattern" ADD VALUE 'paleo';
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -204,6 +204,13 @@
|
|||||||
"when": 1786797105109,
|
"when": 1786797105109,
|
||||||
"tag": "0028_unknown_wrecking_crew",
|
"tag": "0028_unknown_wrecking_crew",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787093092721,
|
||||||
|
"tag": "0029_lumpy_donald_blake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -114,6 +114,10 @@ export function scoreCandidate(
|
|||||||
(candidate.mealTypes ?? []).includes("breakfast") || (candidate.mealTypes ?? []).includes("dessert");
|
(candidate.mealTypes ?? []).includes("breakfast") || (candidate.mealTypes ?? []).includes("dessert");
|
||||||
parts.mealFit = mainMeal && crossover ? 0.15 : 1;
|
parts.mealFit = mainMeal && crossover ? 0.15 : 1;
|
||||||
|
|
||||||
|
// 17. Diet-affinitet (spec §diet): mjuk rankning mot matvanan. Aldrig ett hårt
|
||||||
|
// filter – uteslutande matvanor (vegan m.fl.) hanteras i säkerhetsfiltret.
|
||||||
|
parts.dietAffinity = dietAffinityFit(candidate, ctx);
|
||||||
|
|
||||||
const score = weightedSum(parts, weights);
|
const score = weightedSum(parts, weights);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -216,6 +220,64 @@ function cravingFit(candidate: RecommendationCandidate, ctx: RecommendationConte
|
|||||||
return checks === 0 ? 0.5 : hits / checks;
|
return checks === 0 ? 0.5 : hits / checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mjuk diet-affinitet (spec §diet). Rankar recept som passar användarens matvana
|
||||||
|
* högre – men aldrig ett hårt filter: uteslutande matvanor (vegan/vegetarian/
|
||||||
|
* pescetarian), religion och allergener hanteras redan deterministiskt i
|
||||||
|
* säkerhetsfiltret före poängsättning. Returnerar 0–1 med 0.5 som neutralt
|
||||||
|
* utgångsläge när vi saknar signal (belägger aldrig ett straff vi inte kan visa –
|
||||||
|
* matchar den stödjande, icke-restriktiva hälsoprofilen, R7).
|
||||||
|
*/
|
||||||
|
function dietAffinityFit(candidate: RecommendationCandidate, ctx: RecommendationContext): number {
|
||||||
|
const diet = ctx.dietPattern;
|
||||||
|
if (!diet) return 0.5;
|
||||||
|
const { proteinG, carbsG } = candidate.nutritionPerPortion;
|
||||||
|
const tags = new Set<string>(candidate.tags);
|
||||||
|
|
||||||
|
switch (diet) {
|
||||||
|
case "low_carb":
|
||||||
|
case "keto": {
|
||||||
|
// LCHF/keto: låg kolhydrat premieras. Explicit low_carb-tagg = säkrast.
|
||||||
|
if (tags.has("low_carb")) return 1;
|
||||||
|
if (carbsG <= 15) return 1;
|
||||||
|
if (carbsG <= 30) return 0.75;
|
||||||
|
if (carbsG <= 50) return 0.45;
|
||||||
|
return 0.15;
|
||||||
|
}
|
||||||
|
case "carnivore": {
|
||||||
|
// Djurbaserat: hög proteinandel + mycket låg kolhydrat (macro-proxy). Vi
|
||||||
|
// saknar ingrediens-kategoriflaggor, så ingen hård uteslutning – salt och
|
||||||
|
// kryddor ska aldrig straffas.
|
||||||
|
const lowCarb = carbsG <= 10 ? 1 : carbsG <= 25 ? 0.5 : 0.15;
|
||||||
|
const highProtein = proteinG >= 35 ? 1 : proteinG >= 20 ? 0.6 : 0.3;
|
||||||
|
return clamp01(lowCarb * 0.6 + highProtein * 0.4);
|
||||||
|
}
|
||||||
|
case "paleo": {
|
||||||
|
// Paleo utesluter spannmål/baljväxter/mejeri/raffinerat socker. Exakta
|
||||||
|
// kategoriflaggor saknas -> tillförlitliga proxyer: glutenfri- och
|
||||||
|
// laktosfri-taggar är positiva signaler. Utan signal: neutral (0.5).
|
||||||
|
let score = 0.5;
|
||||||
|
if (tags.has("gluten_free")) score += 0.2;
|
||||||
|
if (tags.has("lactose_free")) score += 0.15;
|
||||||
|
if (tags.has("low_carb")) score += 0.1;
|
||||||
|
return clamp01(score);
|
||||||
|
}
|
||||||
|
case "nordic": {
|
||||||
|
// Nordisk: svenskt/nordiskt kök + säsong premieras.
|
||||||
|
const regional = candidate.cuisine === "swedish" || candidate.cuisine === "nordic";
|
||||||
|
const inSeason = candidate.peakSeasons.includes(ctx.currentSeason);
|
||||||
|
if (regional && inSeason) return 1;
|
||||||
|
if (regional) return 0.85;
|
||||||
|
if (inSeason) return 0.55;
|
||||||
|
return 0.4;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// omnivore/flexitarian/mediterranean/high_protein + veg-mönster: neutral.
|
||||||
|
// (Veg-mönster hårdfiltreras redan; ingen rankningsskillnad behövs här.)
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface FitResult {
|
interface FitResult {
|
||||||
score: number;
|
score: number;
|
||||||
provenance: ProvenanceEntry[];
|
provenance: ProvenanceEntry[];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
Cuisine,
|
Cuisine,
|
||||||
|
DietPattern,
|
||||||
MealType,
|
MealType,
|
||||||
MemoryItem,
|
MemoryItem,
|
||||||
NutritionValues,
|
NutritionValues,
|
||||||
@@ -63,6 +64,12 @@ export interface RecommendationContext {
|
|||||||
isWeekday: boolean;
|
isWeekday: boolean;
|
||||||
isTrainingDay?: boolean | undefined;
|
isTrainingDay?: boolean | undefined;
|
||||||
favoriteCuisines: Cuisine[];
|
favoriteCuisines: Cuisine[];
|
||||||
|
/**
|
||||||
|
* Användarens matvana (spec §diet). Styr enbart MJUK rankning (dietAffinity) –
|
||||||
|
* hårda uteslutningar (vegan/vegetarian/pescetarian/religion/allergener) sker
|
||||||
|
* redan i säkerhetsfiltret före poängsättning. undefined = ingen preferens.
|
||||||
|
*/
|
||||||
|
dietPattern?: DietPattern | undefined;
|
||||||
/** Tolkade "jag är sugen på"-taggar (spec §19), redan normaliserade. */
|
/** Tolkade "jag är sugen på"-taggar (spec §19), redan normaliserade. */
|
||||||
cravingTags?: string[] | undefined;
|
cravingTags?: string[] | undefined;
|
||||||
cravingCuisine?: Cuisine | undefined;
|
cravingCuisine?: Cuisine | undefined;
|
||||||
@@ -106,6 +113,8 @@ export interface ScoringWeights {
|
|||||||
weather: number;
|
weather: number;
|
||||||
craving: number;
|
craving: number;
|
||||||
mealFit: number;
|
mealFit: number;
|
||||||
|
/** Mjuk diet-affinitet (spec §diet) – rankar recept som passar matvanan högre. */
|
||||||
|
dietAffinity: number;
|
||||||
/** Nya S1-vikter; 0 om personalisering är av. */
|
/** Nya S1-vikter; 0 om personalisering är av. */
|
||||||
memoryFit: number;
|
memoryFit: number;
|
||||||
tasteFit: number;
|
tasteFit: number;
|
||||||
@@ -126,6 +135,7 @@ export const DEFAULT_WEIGHTS: ScoringWeights = {
|
|||||||
weather: 3,
|
weather: 3,
|
||||||
craving: 15,
|
craving: 15,
|
||||||
mealFit: 12,
|
mealFit: 12,
|
||||||
|
dietAffinity: 10,
|
||||||
memoryFit: 6,
|
memoryFit: 6,
|
||||||
tasteFit: 6,
|
tasteFit: 6,
|
||||||
cookingAssumptionFit: 4,
|
cookingAssumptionFit: 4,
|
||||||
@@ -159,6 +169,7 @@ export const TASTE_VIEW_WEIGHTS: ScoringWeights = {
|
|||||||
weather: 1,
|
weather: 1,
|
||||||
craving: 25,
|
craving: 25,
|
||||||
mealFit: 12,
|
mealFit: 12,
|
||||||
|
dietAffinity: 8,
|
||||||
memoryFit: 12,
|
memoryFit: 12,
|
||||||
tasteFit: 12,
|
tasteFit: 12,
|
||||||
cookingAssumptionFit: 6,
|
cookingAssumptionFit: 6,
|
||||||
@@ -179,6 +190,7 @@ export const HEALTH_VIEW_WEIGHTS: ScoringWeights = {
|
|||||||
weather: 2,
|
weather: 2,
|
||||||
craving: 5,
|
craving: 5,
|
||||||
mealFit: 12,
|
mealFit: 12,
|
||||||
|
dietAffinity: 14,
|
||||||
memoryFit: 3,
|
memoryFit: 3,
|
||||||
tasteFit: 3,
|
tasteFit: 3,
|
||||||
cookingAssumptionFit: 4,
|
cookingAssumptionFit: 4,
|
||||||
@@ -199,6 +211,7 @@ export const PANTRY_VIEW_WEIGHTS: ScoringWeights = {
|
|||||||
weather: 1,
|
weather: 1,
|
||||||
craving: 5,
|
craving: 5,
|
||||||
mealFit: 12,
|
mealFit: 12,
|
||||||
|
dietAffinity: 8,
|
||||||
memoryFit: 4,
|
memoryFit: 4,
|
||||||
tasteFit: 4,
|
tasteFit: 4,
|
||||||
cookingAssumptionFit: 8,
|
cookingAssumptionFit: 8,
|
||||||
|
|||||||
@@ -486,3 +486,72 @@ describe("S4 rekommendationsvyer", () => {
|
|||||||
expect(tasteWeights.taste).toBe(TASTE_VIEW_WEIGHTS.taste);
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ export const DIET_PATTERNS = [
|
|||||||
"keto",
|
"keto",
|
||||||
"high_protein",
|
"high_protein",
|
||||||
"mediterranean",
|
"mediterranean",
|
||||||
|
// Tillagda (spec §diet): nordiskt, carnivore, paleo. Additivt – gamla värden
|
||||||
|
// orörda, ingen dataförlust. Rankas/AI-styrs snarare än hårdfiltreras.
|
||||||
|
// OBS: ingen separat "lchf" – LCHF täcks av befintliga "low_carb" (undviker
|
||||||
|
// dubblett med keto/low_carb). UI visar "low_carb" med etiketten "LCHF".
|
||||||
|
"nordic",
|
||||||
|
"carnivore",
|
||||||
|
"paleo",
|
||||||
] as const;
|
] as const;
|
||||||
export type DietPattern = (typeof DIET_PATTERNS)[number];
|
export type DietPattern = (typeof DIET_PATTERNS)[number];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user