Files
Cibello-app/docs/34-personalisering-implementation.md
T
Sven (AAMOS AI) 16a7849e71 docs: persona-charter + personaliseringsplan godkänd med justeringar
- Checkar in docs/31-persona-personalisering.md (nordstjärna).
- Uppdaterar docs/34-personalisering-implementation.md:
  - R7: välmående / icke-restriktiv hälsa-framing.
  - Personaliseringstexter = mallar med grundade fakta, inte fri AI-text.
  - ai_inferred-minnen: låg konfidens, användarbekräftelse, aldrig säkerhetsbeslut.
  - UPDATE_USER_MEMORY: budgettak + mock-mönster som skanning.
  - S0 får starta; S1 inväntar godkännande.
- Staging helig, prod orörd.
2026-08-10 02:43:13 +07:00

296 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Del 34 Personalisering: implementationsplan (Spår 1)
> Status: **plan för granskning**. Ingen funktionskod får skrivas förrän planen är godkänd.
> Omfattning: Staging endast. Prod/AAMOS förblir orörd.
> Mål: mappa persona-chartern (`docs/31-persona-personalisering.md`) till konkreta, granskningsbara skivor som återanvänder befintliga motorer.
---
## 1. YT-INVENTERING — vilka AI-nära ytor ska personaliseras
| Yta | Nuvarande läge | Persona-effekt efter Spår 1 | Varför denna yta? |
| --- | --- | --- | --- |
| **A. "Vad ska vi äta?" / `GET /v1/recommendations/what-to-eat`** | ✅ Byggd. Rankar recept efter täckning, utgångsdatum, näringsfit, smak, betyg, säsong, högtid, tid, budget, variation, väder, craving. | Recept rangordnas med tydlig proveniens som inkluderar *användarens egna minnen* ("Du lagade kycklinggryta tre tisdagar i rad — här är en annan variant"), *hushållsförbrukningsmönster* och *utgångs-varor*. | Appens viktigaste surface; här märks personliga förslag mest. |
| **B. Grupperade sök-svar / recept-sök** | 🔧 Delvis byggd (`/v1/recipes/search` finns troligen; söksvar är grundade i katalogen). | Sökresultat tonas av användarens smakprofil, hushållsbegränsningar och lager. Proaktivt utesluter allergener/undvikanden. **Bygger PÅ befintligt grundat svar (lager + plats + trust-hedge); no-fabrication-grundningen bevaras.** | Sök är en aktiv användarintent; personlig rangordning måste vara förklarlig. |
| **C. Proaktiva puffar** | 🔧 `SEND_EXPIRY_NOTIFICATION` körs dagligen kl 07. Matlådepåminnelser finns. | Nya puffar: "Gurkan börjar se trött ut — tre recept du brukar gilla med gurka", "Ni har ätit risotto ofta på söndagar; vill du planera en?" | Hög användarnytta, låg frekvens, kräver explicit opt-in. |
| **D. Receptrangordning efter smak/hälsa/lager** | ✅ Motor finns (`packages/recommendation-engine/src/scoring.ts`). | Skapa tre fördefinierade vyer: **Smak** (favoritkök + taste_signals), **Hälsa** (näring mot dagsmål + health_profile), **Lager** (täckning + utgår-snart). Användaren växlar; ingen hemlig viktning. | Ger användaren kontroll och transparens. |
| **E. Onboarding-relationen** | ✅ Onboarding med mål, allergier, favoritkök. | Onboarding-klassifikationer skrivs som `user_stated`-minnen och `taste_signals`; de synkroniseras tillbaka till "Vad plattformen vet om mig". | Tidigt förtroende: användaren ser att svaren används. |
**Out of scope för Spår 1 (sparas till senare spår):**
- Generering av helt nya recept utifrån minne.
- Community/creator-personalisering.
- Extern hälsointegration (HealthKit/Health Connect).
- Butikserbjudanden/partnerships.
- Bildtränings-personalisering.
---
## 2. MOTOR-MAPPNING — anti-dubblett: återbruk, inga parallella motorer
För varje yta anger tabellen **vilken befintlig data/motor** som är primär källa. Ingen ny AI-motor ska byggas för Spår 1; personalisering = att låta befintliga motorer läsa mer kontext.
| Yta | Primär befintlig data/motor | Ny data vi läser in (ingen ny motor) | Vad vi INTE bygger |
| --- | --- | --- | --- |
| A. Vad ska vi äta? | `recommendation-engine` (`scoreCandidate`/`rankAll`) | `memory_items` (smak/faktum/mönster), `taste_signals` (axelriktning), `cooking_assumption_profiles` (förbrukningsmönster), `recipe_cooks` (senast lagat), `recipe_ratings` (egna betyg). | Ingen ny rankningsmotor. |
| B. Sök-svar | Recept-katalog + `recipe-engine` (säkerhet/täckning) | Samma som A plus `userPreferences` (favoriteCuisines, avoidIngredientIds, spiceLevelMax). | Ingen separat sök-AI. |
| C. Proaktiva puffar | `inventory-engine/forecast.ts`, `reconciliation.ts`, `worker`-schemaläggare | `memory_items.recipe_memory`, `recipe_cooks`, `taste_signals`, `userConsents` (opt-in). | Ingen ny notismotor. |
| D. Smak/Hälsa/Lager-vyer | `recommendation-engine` + `nutrition-engine` | `userPreferences`, `userHealthProfiles`, dagens `meals`, lager. | Ingen ny vy-motor; bara fördefinierade vikter. |
| E. Onboarding | Onboarding-flödet i `apps/mobile` | Skriver till `memory_items` + `taste_signals` via befintliga API-routes. | Ingen ny onboarding-backend. |
**Centrala datatabeller som används av flera ytor:**
- `users` + `userPreferences` + `userHealthProfiles`: mål, allergier, diet, kök, undvikanden, spice max, utrustning, portioner.
- `userConsents`: `personalization`-flaggan är hård port för alla personliga ytor.
- `households` + `household_members`: hushållsgemensamma begränsningar (strängaste gäller) och portionsfaktorer.
- `inventory_items` + `inventory_transactions`: lager, utgångsdatum, förbrukningstakt.
- `canonical_ingredients` + `recipe_ingredients`: säkerhetsfiltrering, täckning, allergener.
- `recipes` + `recipe_ratings` + `recipe_cooks` + `recipe_favorites`: betyg, historik, variation.
- `memory_items`: användarägda, transparenta minnen (alla `origin`, `paused`, `verifiedByUser`).
- `taste_signals`: strukturerade smakpreferenser per axel (ursprung `user_stated`/`observed`/`ai_inferred`).
- `cooking_assumption_profiles`: hushållsspecifika förbrukningsantaganden per ingrediens.
- `meals` + `meal_boxes`: dagens näringsintag och rester.
- `domain_events`: händelseflöde för `UPDATE_USER_MEMORY`.
- `season_events` + `weather` (stub): säsong/högtid/väder.
**Anti-dubblett-princip:** Om en funktion kan uppnås genom att lägga till ett fält i en befintlig motor eller läsa en befintlig tabell, ska vi göra det. Endast om en yta kräver helt ny algoritmisk logik får en ny fil skapas — och då i `packages/recommendation-engine` med egna enhetstester.
---
## 3. CHARTER-REGLER inbyggda
Persona-chartern översätts till följande tekniska regler. Varje regel ska gå att granska i kod och test.
### R1. Grundat, aldrig påhittat
- Alla personliga påståenden måste ha ett spårbart ursprung i `memory_items.origin` (`user_stated`, `observed`, `ai_inferred`).
- `ai_inferred`-poster:
- Har **låg startkonfidens** (t.ex. 0.5).
- Formuleras som "ett mönster vi sett", aldrig "du gillar".
- Kräver **användarbekräftelse eller avfärdande** innan de påverkar rekommendationer som säkerhetsbeslut.
- Driver **aldrig** säkerhetsbeslut (allergen/diet/exkludering).
- Ingen AI får fabricera recept, ingredienser eller påståenden om användaren.
### R2. Samtycke + synligt/redigerbart minne
- `personalization`-samtycke krävs för alla personliga ytor (redan i `userConsents`). **Enkeltflaggat samtycke är OK för Spår 1**; granulär uppdelning skjuts till senare spår om behov uppstår.
- "Vad plattformen vet om mig" (`GET /v1/me/memory`) visar alla minnen, inklusive ursprung och konfidens.
- Användaren kan rätta (`PATCH /v1/me/memory/:id`), pausa (`POST /v1/me/memory/pause-all`) och radera (`DELETE /v1/me/memory/:id` samt `DELETE /v1/me/memory`).
- Användarkorrigering ändrar `origin` till `user_stated` och `confidence` till `1`.
### R3. Ingen skam-ton
- Förklaringar (`whySv`) får aldrig innehålla skam, moralisering eller värdeomdömen om matvanor.
- Exempel på förbjuden copy: "Du borde äta mindre kött", "Din kost är obalanserad".
- Tillåten copy: "Denna rätt ger 35 g protein, vilket matchar ditt mål", "Du har lagat risotto ofta på söndagar — här är en variation".
- Enhetstester ska validera copy mot en förbudslista.
### R4. Allergener/hälsa användarsatt
- Allergener kommer från `userPreferences.allergens` + hushållsaggregering (strängaste gäller), aldrig från AI.
- Hälsodata (`userHealthProfiles`) är frivillig och användarsatt; appen visar icke-medicinsk disclaimer.
- Recept-allergener fortsätter att härledas deterministiskt från `canonical_ingredients.allergens` (se Del 33 / allergen-invariant-testet).
### R5. i18n × 12
- Alla nya användarvisningstexter ska gå via `localeContext` och `memory-client/renderMemorySummary`.
- Proveniessatser genereras med språkspecifika mallar (sv/en/es/it/de/fr/da/nb/fi/nl/pl/pt).
- Förklaringar (`whySv`) ska ha motsvarande `whyEn`, `whyEs`, … eller genereras dynamiskt från mallar.
### R6. Proveniens
- Varje personaliserat förslag ska kunna förklara *varför* det valdes.
- **Personaliseringstexter (proveniens, why, puffar) genereras från mallar med grundade fakta**, inte fri AI-text. Mallarna är översatta, skam- och fabriceringsfria per konstruktion.
- Exempel på mallar (svenska):
- `"För att du har berättat att du gillar {{cuisine}}."`
- `"För att ni har {{count}} {{ingredient}} som bör användas inom {{days}} dagar."`
- `"För att du lagat {{recipe}} {{count}} gånger den senaste månaden."`
- `"För att det passar ditt återstående proteinbehov i dag."`
- Förbudslistan (R3) behålls som skyddsnät för mall-renderade strängar.
- **Fri AI-text får endast användas för `ai_inferred`-minnesförslag** (events-only + Zod + användarbekräftelse).
- Proveniens lagras/transporteras i `ScoredRecommendation.parts` + en ny valfri `provenance` array.
### R7. Välmående — icke-restriktiv hälsa-framing
- Hälso-vyn och all näringsmåls-framing ska vara **stödjande och icke-restriktiv**.
- Tillåten copy: `"Passar ditt proteinmål"`, `"Bidrar till dina grönsaker i dag"`.
- **Förbjuden copy:** `"Du har överskridit ditt kalorimål"`, `"Bara X kcal kvar"`, `"Begränsa dig"`, `"Undvik …"` eller annan negativ/restriktiv framing.
- Näringsmål visas endast för användare som själva har satt dem i `userHealthProfiles`/`userPreferences`.
- Copy-granskning + eval-fall ska täcka välmående-framing.
---
## 4. SKIVINDELNING — små reviewbara skivor med testplan
Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva levererar: kod + enhetstester + integrationstest mot staging-DB + dokumentationsuppdatering.
### Skiva S0 — förberedelse (inga användarfunktioner)
**Innehåll:**
- Checka in `docs/31-persona-nordstjärnan.md` och förankra R1R7 explicit mot varje avsnitt i chartern.
- Säkerställ att `userConsents` för `personalization` läses korrekt överallt. **Enkeltflaggat `personalization`-samtycke är OK för Spår 1** (ingen granulär uppdelning av personalisering ännu).
- Uppdatera `packages/recommendation-engine/src/types.ts` med `provenance` och `memorySignalIds``ScoredRecommendation`.
- Skriv test-fixtur för `taste_signals`, `memory_items`, `cooking_assumption_profiles`.
- Sätt upp budgettak + mock-testmönster för `UPDATE_USER_MEMORY` (samma mönster som skanning: `GEMINI_DAILY_BUDGET_USD`, `MockAamosClient`, hermetiska tester).
**Testplan:**
- Typecheck och enhetstester gröna.
- Fixturen kan skapa en användare med samtycken, minnen, smaksignaler och matlagningshistorik.
- `UPDATE_USER_MEMORY`-anrop kan mockas och kostnadsräknas utan att träffa AAMOS.
**Estimat:** 1 dag.
---
### Skiva S1 — "Vad ska vi äta?" tonad mot lager + utgår-snart + smak + proveniens
**Innehåll:**
- Utöka `recommendation-engine/src/scoring.ts` med tre nya delpoäng som läser befintliga tabeller:
- `memoryFit`: positiv boost om recept matchar `memory_items` (t.ex. favoritkök, gillade rätter).
- `tasteFit`: boost/penalty från `taste_signals` per axel.
- `cookingAssumptionFit`: boost om recept använder ingredienser där `cooking_assumption_profiles` visar att hushållet brukar äta upp.
- Lägg till `provenance` array i resultatet med motiveringar på svenska + andra språk.
- Uppdatera `explain.ts` så proveniens visas i `whySv`.
- Uppdatera `apps/api/src/routes/recommendations.ts` att läsa in `memory_items`, `taste_signals` och `cooking_assumption_profiles` för aktuell användare/hushåll.
- Hård krav: `personalization`-samtycke måste vara `granted`; annars används nuvarande beteende.
**Testplan:**
- Enhetstest: en kandidat med `taste_signals` (positiv/negativ) får rätt `tasteFit`.
- Enhetstest: `memoryFit` boostas av `verifiedByUser`-minne mer än `ai_inferred`.
- Enhetstest: `cookingAssumptionFit` boostar recept som använder ingredienser med hög `averageEatenPortions`.
- Integrationstest: `GET /v1/recommendations/what-to-eat` med/utan samtycke ger olika resultat.
- UI/Copy-test: inga skam-fraser i 100 genererade `whySv`.
**Estimat:** 34 dagar.
---
### Skiva S2 — Proaktiva puffar (opt-in)
**Innehåll:**
- Ny jobbtyp `SEND_PERSONALIZED_NUDGE` i worker (BullMQ-schemaläggare, t.ex. kl 11:00 på helger och kl 16:00 på vardagar).
- Logik:
1. Hämta hushåll med `personalization`-samtycke.
2. Hämta utgående varor (`inventory-engine/expiry.ts`).
3. Hämta recept som använder dessa varor och som matchar `taste_signals`/`memory_items`.
4. Generera max 1 puff per hushåll per dag; undvik upprepning inom 7 dagar.
5. Skriv notis i `notifications` (inte push direkt; push kräver separat opt-in).
- Proveniens: "Gurkan håller på att bli slapp — här är ett recept du brukar gilla."
**Testplan:**
- Enhetstest: puff genereras endast om det finns utgående vara + matchande recept + samtycke.
- Enhetstest: max 1 puff per hushåll per dag.
- Integrationstest: jobbet kör mot staging-DB och skapar rätt notis.
- GDPR-test: avbryt vid `personalization`-revoke.
**Estimat:** 3 dagar.
---
### Skiva S3 — Minnes-ytan + personliga grundade svar
**Innehåll:**
- Förbättra `GET /v1/me/memory` så minnen visas med tydligare ursprung och användbara kategorier.
- Koppla minnen till reella ytor: t.ex. "Du gillar italienskt kök" → visar vilka recept som påverkas.
- Lägg till `GET /v1/me/memory/impact` som returnerar vilka rekommendationer som påverkas av ett specifikt minne (utan att exponera andras data).
- Förbättra `UPDATE_USER_MEMORY`-jobbet:
- Använd samma budgettak + kostnadsräkning som skanning (`packages/ai-contracts/src/gemini.ts`, `GEMINI_DAILY_BUDGET_USD`).
- Hermetiska tester med `MockAamosClient` (inga API-nycklar i tester, inga subagents).
- Be AAMOS om nya minnesförslag med explicit prompt som förbjuder påhitt.
- Validera output mot Zod-kontraktet.
- Avvisa förslag som saknar stöd i events.
**Testplan:**
- Enhetstest: `buildMemoryOverview` grupperar rätt och visar pausade poster.
- Integrationstest: `PATCH /v1/me/memory/:id` ändrar `origin` till `user_stated`.
- Mock-test: `UPDATE_USER_MEMORY` avvisar fabricerat minne (t.ex. påhittad favoriträtt).
- GDPR-test: `DELETE /v1/me/memory` raderar allt; `DELETE /v1/me` raderar även `taste_signals`.
**Estimat:** 3 dagar.
---
### Skiva S4 — Smak / Hälsa / Lager-vyer i "Vad ska vi äta?"
**Innehåll:**
- Lägg till query-param `view=default|taste|health|pantry``GET /v1/recommendations/what-to-eat`.
- Varje vy använder samma `rankAll` men med fördefinierade `ScoringWeights`:
- **Taste**: högre `taste`, `craving`, `memoryFit`, `rating`; lägre `nutritionFit`.
- **Health**: högre `nutritionFit`; boosta protein-/fiber-/kcal-mål; lägre `budget`.
- **Pantry**: högre `coverage`, `expiry`; lägre `variety`, `weather`.
- Spara vikterna i `packages/recommendation-engine/src/types.ts` som konstanter.
**Testplan:**
- Enhetstest: samma kandidater ger olika ordning beroende på vy.
- Integrationstest: API accepterar `view`-parametern och returnerar skilda topplistor.
- Regressionstest: `default` är oförändrad gentemot idag.
**Estimat:** 2 dagar.
---
### Skiva S5 — Onboarding → minne + smaksignaler
**Innehåll:**
- När användaren slutför onboarding skrivs mål, allergier, favoritkök, undvikanden och spice max till:
- `userPreferences` (redan idag).
- `memory_items` som `user_stated`-poster.
- `taste_signals` för favoritkök (`axis=cuisine`, direction=positiv) och undvikna ingredienser (`axis=ingredient_avoid`, direction=negativ).
- Visa i onboarding: "Detta sparas i "Vad plattformen vet om mig" och du kan ändra det när som helst."
**Testplan:**
- Integrationstest: efter onboarding finns motsvarande `memory_items` och `taste_signals`.
- Enhetstest: onboarding-favoritkök skapar rätt `taste_signals`-poster.
**Estimat:** 2 dagar.
---
### Skiva S6 — i18n, proveniens och eval-pipeline
**Innehåll:**
- Säkerställ att alla nya provenienssträngar finns på 12 språk.
- Uppdatera `memory-client/renderMemorySummary` med nya value-former.
- Lägg till AI-eval-fall i `ai_eval_cases` för:
- "Inga fabricerade minnen"
- "Inga skam-fraser"
- "Rätt språk i proveniens"
- Kör evals mot AAMOS i staging.
**Testplan:**
- Evals körbara via adminpanel eller worker-jobb.
- Manuell granskning av 50 genererade `whySv` per språk.
**Estimat:** 23 dagar.
---
**Totalt estimerat:** 1620 arbetsdagar över 6 skivor, förutsatt att varje skiva godkänns innan nästa påbörjas.
---
## 5. RISKER + hur varje charter-regel testas
| Risk | Mitigering | Test/verifiering |
| --- | --- | --- |
| **AI fabricerar minnen eller påhittar användarpreferenser** | `UPDATE_USER_MEMORY` tar bara events som input; output valideras mot Zod; `ai_inferred`-poster visas med tydlig etikett; användaren kan radera/pausa. | Mock-test där AAMOS föreslår ett minne som inte stöds av events → avvisas. Invariant: alla `memory_items` måste ha `origin` satt. |
| **Personliga data läcker till andra hushållsmedlemmar** | `userHealthProfiles`, `memory_items`, `taste_signals` är användarsatta; hushållsaggregering är begränsad till allergener/undvikanden/spice max. | Integrationstest: medlem A kan inte läsa medlem B:s `memory_items` eller hälsoprofil. |
| **Skam-ton eller värderande copy** | Förbudslista + språkgranskning i `explain.ts` och nudge-generering. | Enhetstest som skannar 100+ genererade förklaringar mot förbudslista; eval-korpus med skam-exempel. |
| **Användaren tappar kontroll över minnet** | Full CRUD + paus + radera allt; alla nya minnen kommer från användaren eller observerade events. | Manuell test av "Vad plattformen vet om mig"-flödet; GDPR-raderingstest. |
| **Personalisering körs utan samtycke** | Hård check på `userConsents.kind='personalization' AND status='granted'` i API och worker. | Integrationstest: utan samtycke fallback till opersonligt beteende; inga `memory_items` skrivs. |
| **Allergener eller hälsa härleds felaktigt av AI** | Allergener kommer från användarpreferenser + canonical-ingredienser; hälsa är användarsatt. AI används aldrig för säkerhetsfiltrering. | Återanvänd befintliga säkerhetstester (`recipe-engine/test/safety.test.ts`) och allergen-invariant-testet. |
| **i18n-försämring / fel språk i proveniens** | Alla nya texter via `localeContext` och språkspecifika mallar; eval per språk. | Manuell granskning per språk; automatisk check att `whySv` inte blandas med `whyEn`. |
| **Prestandaförsämring i "Vad ska vi äta?"** | Läsning av `memory_items` och `taste_signals` är indexerad; begränsa till aktuell användare/hushåll; cache lämpliga aggregeringar. | Lasttest: 200 anrop/minut under 5 minuter; jämför svarstid före/efter. |
| **Cirkulära beroenden** | Alla ändringar i `recommendation-engine` får inte bero på `apps/api`; `memory-client` får inte bero på `recommendation-engine`. | `pnpm typecheck` och dependency-graf (turbo) grön. |
| **Seed/reseed bryter personliga data** | Inga personliga data i seed; seed skrivs aldrig över användardata. | Seed-test: efter `db:test-setup` finns inga `memory_items` eller `taste_signals` för testanvändare. |
---
## Godkännande-checklista för planen
- [x] Persona-charter (`docs/31-persona-personalisering.md`) checkas in och förankras.
- [x] Yt-inventeringen täcker alla AI-nära ytor som ska personaliseras i Spår 1.
- [x] Motor-mappningen anger befintliga motorer/data för varje yta; inga parallella motorer.
- [x] Charter-reglerna (R1R7) är tydliga och testbara.
- [x] Personaliseringstexter = mallar med grundade fakta, inte fri AI-text.
- [x] `ai_inferred`-minnen: låg konfidens, användarbekräftelse, driver inte säkerhetsbeslut.
- [x] `UPDATE_USER_MEMORY`: budgettak + mock-mönster som skanning.
- [x] Skivindelningen är tillräckligt liten; varje skiva har testplan.
- [x] Risker och testmetoder accepteras.
- [x] Ingen kod skrivs förrän denna plan är godkänd.
- [x] S0 får starta; S1 påbörjas efter godkännande.
---
## Revisionslogg
| Datum | Revision | Författare |
| --- | --- | --- |
| 2026-08-10 | Initial plan för granskning | Sven |
| 2026-08-10 | Godkänd med justeringar: R7 välmående, mall-baserade texter, ai_inferred-konfidens, UPDATE_USER_MEMORY-budget, docs/31 incheckad | Sven / Johan |