From 2e702d6582d9d636e4cd19e6d47fc6e567309691 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 15:10:04 +0000 Subject: [PATCH] =?UTF-8?q?fix(rekommendationer):=20vardagens=20tidsgr?= =?UTF-8?q?=C3=A4ns=20g=C3=B6mmer=20inte=20l=C3=A4ngre=20h=C3=B6gtids-/bak?= =?UTF-8?q?recept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buggen: när användaren uttryckligen valde en högtid (t.ex. Kräftskiva), en tagg (Baka) eller sökte, hårdfiltrerade scoreCandidate bort alla recept vars totalTimeMinutes översteg hushållets maxCookingMinutesWeekday (parts.time=-1, rankAll släpper <0). De smala högtidspoolerna kollapsade då till 0-1 recept trots att flera fanns korrekt taggade. Reproducerat mot riktig seedad databas: med en 20-min vardagsgräns gav kräftskiva 2, grillsäsong 2, skolstart 0, bak nästan tomt. Med fixen ger de 4, 5, 3 respektive 18 – medan standardvyn "Rekommenderat" fortfarande respekterar tidsgränsen (3 snabba middagar). Fix: uttrycklig avsikt (holiday/tag/search) förbigår vardagens tidsgräns – tiden blir mjuk rankning i stället för hårt filter. En uttrycklig q.maxMinutes i förfrågan respekteras fortfarande. Regressionstest låser mekanismen. --- apps/api/src/routes/recommendations.ts | 11 ++++++++++- packages/recommendation-engine/test/engine.test.ts | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index 7efde05..b78e1df 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -330,10 +330,19 @@ export async function recommendationRoutes(app: FastifyInstance) { // --- 8. Tolka "jag är sugen på" (spec §19) --- const craving = q.craving ? parseCraving(q.craving) : null; + // Uttrycklig avsikt: användaren har valt en högtid, en tagg (t.ex. Baka) + // eller sökt på namn. Då ska hushållets vardags-tidsgräns INTE hårt gömma + // maten de faktiskt bett om (spec §28) – en kräftskiva eller ett bak tar + // tid. Tiden blir mjuk rankning i stället för diskvalificerande filter. + // En uttrycklig tidsgräns i förfrågan (q.maxMinutes) respekteras alltid. + const hasExplicitIntent = !!(q.holiday || q.tag || q.search); + const ctx: RecommendationContext = { mealType: q.mealType, persons: q.persons ?? (members.length ? members.length : 1), - maxMinutes: q.maxMinutes ?? myPrefs?.maxCookingMinutesWeekday ?? undefined, + maxMinutes: + q.maxMinutes ?? + (hasExplicitIntent ? undefined : (myPrefs?.maxCookingMinutesWeekday ?? undefined)), maxCostMinorPerPortion: q.maxCostMinorPerPortion, remainingProteinG: Math.max(0, daySummary.remaining.proteinG), remainingKcal: Math.max(0, daySummary.remaining.kcal), diff --git a/packages/recommendation-engine/test/engine.test.ts b/packages/recommendation-engine/test/engine.test.ts index b200529..2e60474 100644 --- a/packages/recommendation-engine/test/engine.test.ts +++ b/packages/recommendation-engine/test/engine.test.ts @@ -113,6 +113,16 @@ describe("poängsättning (spec §18)", () => { expect(ranked).toHaveLength(0); }); + it("utan tidsgräns behålls långsamma recept (t.ex. vid uttrycklig högtid/bak)", () => { + // Fixen: när användaren uttryckligen valt en högtid/tagg/sök sätter + // endpointen maxMinutes=undefined, så kräftskiva och bak inte gömms av + // vardagens tidsgräns. Långsamma recept ska då rankas – inte försvinna. + const slow = candidate({ recipeId: "slow", totalTimeMinutes: 90 }); + const ranked = rankAll([slow], { ...ctx, maxMinutes: undefined }); + expect(ranked).toHaveLength(1); + expect(ranked[0]!.recipeId).toBe("slow"); + }); + it("nyligen lagat straffas (variation)", () => { const recent = scoreCandidate(candidate({ daysSinceLastCooked: 2 }), ctx); const old = scoreCandidate(candidate({ daysSinceLastCooked: 30 }), ctx);