feat(recommendation-engine): S1 personalisering för 'Vad ska vi äta?'
- Mallbaserad proveniens i 12 språk (inga fria AI-texter i rekommendationer). - memoryFit, tasteFit, cookingAssumptionFit endast vid personalization-samtycke. - Hård grind i API:et: läser memory_items/taste_signals/cooking_assumption_profiles endast när userConsents.personalization = granted. - NON_PERSONALIZED_WEIGHTS bevarar existerande beteende vid avsaknad av samtycke. - Positiv, icke-restriktiv näringscopy (R7). - Deterministisk scoring + enhetstester för S1. - Integrationstest som verifierar provenans-gate med/utan samtycke.
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
import type { RecommendationCandidate, RecommendationContext } from "./types.js";
|
||||
import type {
|
||||
ProvenanceEntry,
|
||||
RecommendationCandidate,
|
||||
RecommendationContext,
|
||||
} from "./types.js";
|
||||
import { renderProvenance } from "./provenance-templates.js";
|
||||
|
||||
/**
|
||||
* "Varför rekommenderar vi detta?" (spec §18).
|
||||
* Bygger en ärlig, konkret svensk förklaring ur de deterministiska delpoängen.
|
||||
* Exempel ur specen: "Ni har 92 % av ingredienserna. Kycklingen bör användas
|
||||
* senast i morgon. Rätten ger 58 gram protein per portion …"
|
||||
* "Varför rekommenderar vi detta?" (spec §18, S1).
|
||||
* Bygger en ärlig, konkret svensk förklaring ur:
|
||||
* 1. Grundade delpoäng (täckning, utgångsdatum, etc.).
|
||||
* 2. Personaliserings-mallar från provenans (endast om samtycke granted).
|
||||
*
|
||||
* Alla användarvisningstexter kommer från mallar, inte fri AI-text.
|
||||
* R7: näringsrelaterad copy är positiv/stödjande, aldrig restriktiv.
|
||||
*/
|
||||
export function buildWhySv(
|
||||
candidate: RecommendationCandidate,
|
||||
ctx: RecommendationContext,
|
||||
parts: Record<string, number>,
|
||||
provenance: ProvenanceEntry[] = [],
|
||||
): string {
|
||||
const sentences: string[] = [];
|
||||
|
||||
@@ -31,6 +40,7 @@ export function buildWhySv(
|
||||
sentences.push(`${capitalize(urgent.displayNameSv)} bör användas ${when}.`);
|
||||
}
|
||||
|
||||
// R7: positiv näringsframing
|
||||
const protein = Math.round(candidate.nutritionPerPortion.proteinG);
|
||||
if ((parts.nutritionFit ?? 0) >= 0.7 && protein >= 25) {
|
||||
sentences.push(`Rätten ger ${protein} gram protein per portion.`);
|
||||
@@ -63,6 +73,15 @@ export function buildWhySv(
|
||||
sentences.push("Matchar det du är sugen på.");
|
||||
}
|
||||
|
||||
// S1: personaliserings-mallar från provenans
|
||||
if (ctx.personalizationEnabled && provenance.length > 0) {
|
||||
const lang = "sv-SE"; // explain.ts används för närvarande bara för svenska whySv
|
||||
const rendered = renderProvenance(provenance.slice(0, 2), lang); // max 2 personliga satser
|
||||
if (rendered) {
|
||||
sentences.push(rendered);
|
||||
}
|
||||
}
|
||||
|
||||
if (sentences.length === 0) {
|
||||
sentences.push("En balanserad rätt som passar er profil.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user