ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s

This commit is contained in:
Sven (AAMOS AI)
2026-08-13 17:25:14 +07:00
parent 61d60931ad
commit c32a7e33c7
141 changed files with 40555 additions and 45210 deletions
+5 -5
View File
@@ -122,8 +122,8 @@ separat `image_training`-samtycke. Personligt minne är aldrig träningsdata (§
## Lägen
| `AAMOS_MODE` | Användning |
|---|---|
| `mock` | dev/test utan nätverk. Produkten vägrar starta i mock. |
| `http` | Riktig AAMOS Core när matmodell finns. |
| `gemini` | Gemini 2.5 Flash som lärar-tier för kylskåpsskanning (Skiva 1). |
| `AAMOS_MODE` | Användning |
| ------------ | --------------------------------------------------------------- |
| `mock` | dev/test utan nätverk. Produkten vägrar starta i mock. |
| `http` | Riktig AAMOS Core när matmodell finns. |
| `gemini` | Gemini 2.5 Flash som lärar-tier för kylskåpsskanning (Skiva 1). |
+25 -25
View File
@@ -28,18 +28,18 @@ eller råa bilder (se §56/§58).
Varje rad i `ai_corrections` representerar **ett item** från en skanning:
| Fält | Innehåll |
|---|---|
| `scanJobId` | Källskanningen (`scan_jobs.id`). |
| `taskType` | T.ex. `ANALYZE_FRIDGE_IMAGE`, `READ_RECEIPT`. |
| `aiOutput` | Hela AI-raw-resultatet från `scan_jobs.result`. |
| `proposal` | Det specifika AI-förslag item:et kom från (`detectedName`, `canonicalIngredientId`, `brand`, `estimatedQuantity`, `unit`, `bestBeforeDate`, `confidence`, `requiresConfirmation`). |
| `userCorrection` | `{ action: "accept" \| "edit" \| "reject" \| "add", corrected?: {...} }` |
| `corrected` (inbäddad) | Användarens slutgiltiga värden vid accept/edit/add: `displayName`, `canonicalIngredientId`, `brand`, `quantity`, `unit`, `bestBeforeDate`, `useByDate`. |
| `imageS3Key` | Första lagrade bildnyckeln från skanningen, **endast om** `image_training`-samtycke fanns vid bekräftelsen. Annars `null`. |
| `modelVersion` / `promptVersion` | Vilken modell och prompt som producerade förslaget. |
| `consentSnapshot` | `{ anonymized_improvement: "granted"\|"denied", image_training: "granted"\|"denied", ... }` som JSON vid bekräftelsetillfället. |
| `createdAt` | Tidsstämpel för bekräftelsen. |
| Fält | Innehåll |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scanJobId` | Källskanningen (`scan_jobs.id`). |
| `taskType` | T.ex. `ANALYZE_FRIDGE_IMAGE`, `READ_RECEIPT`. |
| `aiOutput` | Hela AI-raw-resultatet från `scan_jobs.result`. |
| `proposal` | Det specifika AI-förslag item:et kom från (`detectedName`, `canonicalIngredientId`, `brand`, `estimatedQuantity`, `unit`, `bestBeforeDate`, `confidence`, `requiresConfirmation`). |
| `userCorrection` | `{ action: "accept" \| "edit" \| "reject" \| "add", corrected?: {...} }` |
| `corrected` (inbäddad) | Användarens slutgiltiga värden vid accept/edit/add: `displayName`, `canonicalIngredientId`, `brand`, `quantity`, `unit`, `bestBeforeDate`, `useByDate`. |
| `imageS3Key` | Första lagrade bildnyckeln från skanningen, **endast om** `image_training`-samtycke fanns vid bekräftelsen. Annars `null`. |
| `modelVersion` / `promptVersion` | Vilken modell och prompt som producerade förslaget. |
| `consentSnapshot` | `{ anonymized_improvement: "granted"\|"denied", image_training: "granted"\|"denied", ... }` som JSON vid bekräftelsetillfället. |
| `createdAt` | Tidsstämpel för bekräftelsen. |
### Åtgärder som sparas
@@ -56,19 +56,19 @@ Alla fyra åtgärder sparas. Bara accept/edit/add leder till att ett
När `BUILD_TRAINING_SAMPLE` kör (veckoschema i worker) bankas rader med
`anonymized_improvement = granted` till `ai_training_bank`:
| Fält | Innehåll |
|---|---|
| `correctionId` | Referens till `ai_corrections.id` (cascade delete). |
| `scanJobId` | Källskanningen. |
| `version` | Dataset-version, t.ex. `v1`. Bumpar när formatet ändras. |
| `taskType` | Samma som källan. |
| `imageS3Key` | Kopierad från `ai_corrections.image_s3_key` (kan vara `null`). |
| `proposal` | AI-förslaget för just det item:et. |
| `action` | Användarens åtgärd. |
| `corrected` | Slutgiltiga värden, eller `null` vid reject. |
| `modelVersion` / `promptVersion` | Spårbarhet till modell/prompt. |
| `consentSnapshot` | Kopia av samtyckesläget. |
| `exportedAt` | När raden bankades. |
| Fält | Innehåll |
| -------------------------------- | -------------------------------------------------------------- |
| `correctionId` | Referens till `ai_corrections.id` (cascade delete). |
| `scanJobId` | Källskanningen. |
| `version` | Dataset-version, t.ex. `v1`. Bumpar när formatet ändras. |
| `taskType` | Samma som källan. |
| `imageS3Key` | Kopierad från `ai_corrections.image_s3_key` (kan vara `null`). |
| `proposal` | AI-förslaget för just det item:et. |
| `action` | Användarens åtgärd. |
| `corrected` | Slutgiltiga värden, eller `null` vid reject. |
| `modelVersion` / `promptVersion` | Spårbarhet till modell/prompt. |
| `consentSnapshot` | Kopia av samtyckesläget. |
| `exportedAt` | När raden bankades. |
Banken ägs av cibello och är versionerad. Ingen extern leverantör
anropas under exporten — jobbet får aldrig kasta på grund av att
+137 -130
View File
@@ -5,6 +5,7 @@
> **Status:** ✅ Implementerad i efterföljande commits. Residual-testet `apps/api/test/me.residual.test.ts` ingår permanent i `pnpm test` och failar automatiskt om nya `user_id`-liknande kolumner läggs till utan klassning.
>
> **Audit gjord mot:**
>
> - `packages/database/src/schema/*.ts`
> - `infrastructure/migrations/*.sql`
> - `apps/api/src/routes/me.ts` (nuvarande `DELETE /v1/me`)
@@ -28,14 +29,14 @@
### 2.1 Användarkonto & autentisering — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|---|---|---|
| `users` | Personliga grunduppgifter (e-post, namn). Soft-delete/anonymisering accepteras för att bevara referensintegritet, men övriga personfält måste skrubbas. | ✅ anonymiserar `email`, `displayName`, sätter `deletedAt`. |
| `user_credentials` | Lösenordshash. Ingen laglig grund efter radering. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `admin_totp` | TOTP-secret. Säkerhetsuppgift. | ❌ **saknas** — cascade fyrar inte. |
| `email_verification_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `password_reset_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `refresh_tokens` | Sessions-token-hash, IP, user-agent. | ✅ raderas explicit. |
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `users` | Personliga grunduppgifter (e-post, namn). Soft-delete/anonymisering accepteras för att bevara referensintegritet, men övriga personfält måste skrubbas. | ✅ anonymiserar `email`, `displayName`, sätter `deletedAt`. |
| `user_credentials` | Lösenordshash. Ingen laglig grund efter radering. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `admin_totp` | TOTP-secret. Säkerhetsuppgift. | ❌ **saknas** — cascade fyrar inte. |
| `email_verification_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `password_reset_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `refresh_tokens` | Sessions-token-hash, IP, user-agent. | ✅ raderas explicit. |
**Bedömning:** Fyra auth-tabeller är ohanterade eftersom `users` soft-deletas. Måste explicit raderas.
@@ -43,12 +44,12 @@
### 2.2 Hälsa, preferenser, samtycken & locale — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|---|---|---|
| `user_health_profiles` | Hälsodata (längd, vikt, födelseår, kön, aktivitet). Känslig persondata. | ✅ raderas explicit. |
| `user_preferences` | Kost/mål/allergier/intoleranser. Personlig profilering. | ✅ raderas explicit. |
| `user_consents` | Samtyckeshistorik kopplad till person. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `user_locale_preferences` | Språk, region, tidszon, valuta. Personlig preferens. | ❌ **saknas** — cascade fyrar inte. |
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `user_health_profiles` | Hälsodata (längd, vikt, födelseår, kön, aktivitet). Känslig persondata. | ✅ raderas explicit. |
| `user_preferences` | Kost/mål/allergier/intoleranser. Personlig profilering. | ✅ raderas explicit. |
| `user_consents` | Samtyckeshistorik kopplad till person. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `user_locale_preferences` | Språk, region, tidszon, valuta. Personlig preferens. | ❌ **saknas** — cascade fyrar inte. |
**Bedömning:** `user_consents` och `user_locale_preferences` är nya luckor.
@@ -56,13 +57,14 @@
### 2.3 Hushåll & medlemskap — BESLUT KRÄVS
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|---|---|---|
| `household_members` | Länkar användare till hushåll. Ingen laglig grund att behålla kopplingen efter radering. | ❌ **saknas helt**. |
| `households` | Om användaren är **enda medlemmen/ägaren** innehåller hushållet personliga spår (namn, budget, aktiveringsdatum, storlek). Om flera medlemmar finns är hushållet en delad resurs. | ❌ **saknas helt**. |
| `storage_locations` | Hushålls-specifika förvaringsplatser. Delad resurs om hushållet har flera medlemmar. | ❌ **saknas helt**. |
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `household_members` | Länkar användare till hushåll. Ingen laglig grund att behålla kopplingen efter radering. | ❌ **saknas helt**. |
| `households` | Om användaren är **enda medlemmen/ägaren** innehåller hushållet personliga spår (namn, budget, aktiveringsdatum, storlek). Om flera medlemmar finns är hushållet en delad resurs. | ❌ **saknas helt**. |
| `storage_locations` | Hushålls-specifika förvaringsplatser. Delad resurs om hushållet har flera medlemmar. | ❌ **saknas helt**. |
**Bedömning:** Detta är den största strukturella luckan. Förslag (se öppna frågor nedan):
- Ta bort användarens `household_members`-rad(er).
- Om användaren är ensam ägare/medlem i ett hushåll: **radera hela hushållet och dess cascading data** (`storage_locations`, `inventory_items`, `inventory_transactions`, `shopping_lists`, `week_plans`, `meal_boxes`, `receipts`, `cooking_sessions`, etc.).
- Om andra medlemmar finns: låt hushållet leva, anonymisera användarreferenser i delade data.
@@ -73,24 +75,24 @@
### 2.4 Lager, inköp, matplanering — hushållsdata (ANONYMISERA eller RADERA beroende på hushåll)
| Tabell | Klassning | Motivering |
|---|---|---|
| `inventory_items` | ANONYMISERA / RADERA | Hushållsdata. Ingen personlig info i sig, men `source`, `verifiedByUser` etc. kan kopplas till användaren. Om hushållet överlever: sätt `verifiedByUser = false` där aktuell användare verifierat. Om hushållet raderas: cascade bort. |
| `inventory_transactions` | ANONYMISERA | `actor_user_id` är en personlig referens. Sätt `NULL`. Data behövs för hushållets saldo/historik. |
| `inventory_item_decay_profile` | BEHÅLL / RADERA | Teknisk koppling till `inventory_items`. Följer hushållets öde. |
| `inventory_conflicts` | ANONYMISERA | `resolved_by_user_id` ska vara `NULL`. Övriga fält är hushållsdata. |
| `shopping_lists` | BEHÅLL | Hushållsdata, ingen PII. |
| `shopping_list_items` | ANONYMISERA | `added_by_user_id` ska vara `NULL`. |
| `receipts` | RADERA / ANONYMISERA | Kvittobild (`image_url`) kan innehålla personuppgifter (butik, kortnummer, artiklar). Om hushållet överlever och kvittot är delat: radera bild-URL:en i objektlagring och anonymisera. Om hushållet raderas: cascade bort. |
| `receipt_lines` | RADERA / ANONYMISERA | `raw_text` är OCR-text från kvitto (potentiellt personligt). Samma logik som `receipts`. |
| `price_observations` | BEHÅLL | Aggregatdata, anonymiserad prisinformation. `store_name` är inte personligt i GDPR-mening. |
| `meal_boxes` | ANONYMISERA | `reserved_for_user_id` ska vara `NULL`. |
| `meals` | RADERA | Direkt personlig måltidslogg (`user_id`, `photo_url`). Raderas explicit idag. |
| `week_plans` | BEHÅLL | Hushållsdata, ingen PII. |
| `week_plan_entries` | BEHÅLL | Hushållsdata. |
| `cooking_sessions` | ANONYMISERA | `started_by_user_id` ska vara `NULL`. Sessionen är hushållshistorik. |
| `cooking_assumption_profiles` | BEHÅLL / RADERA | Hushållsaggregat. Följer hushållets öde. |
| `waste_summaries` | BEHÅLL | Hushållsaggregat, ingen PII. |
| Tabell | Klassning | Motivering |
| ------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inventory_items` | ANONYMISERA / RADERA | Hushållsdata. Ingen personlig info i sig, men `source`, `verifiedByUser` etc. kan kopplas till användaren. Om hushållet överlever: sätt `verifiedByUser = false` där aktuell användare verifierat. Om hushållet raderas: cascade bort. |
| `inventory_transactions` | ANONYMISERA | `actor_user_id` är en personlig referens. Sätt `NULL`. Data behövs för hushållets saldo/historik. |
| `inventory_item_decay_profile` | BEHÅLL / RADERA | Teknisk koppling till `inventory_items`. Följer hushållets öde. |
| `inventory_conflicts` | ANONYMISERA | `resolved_by_user_id` ska vara `NULL`. Övriga fält är hushållsdata. |
| `shopping_lists` | BEHÅLL | Hushållsdata, ingen PII. |
| `shopping_list_items` | ANONYMISERA | `added_by_user_id` ska vara `NULL`. |
| `receipts` | RADERA / ANONYMISERA | Kvittobild (`image_url`) kan innehålla personuppgifter (butik, kortnummer, artiklar). Om hushållet överlever och kvittot är delat: radera bild-URL:en i objektlagring och anonymisera. Om hushållet raderas: cascade bort. |
| `receipt_lines` | RADERA / ANONYMISERA | `raw_text` är OCR-text från kvitto (potentiellt personligt). Samma logik som `receipts`. |
| `price_observations` | BEHÅLL | Aggregatdata, anonymiserad prisinformation. `store_name` är inte personligt i GDPR-mening. |
| `meal_boxes` | ANONYMISERA | `reserved_for_user_id` ska vara `NULL`. |
| `meals` | RADERA | Direkt personlig måltidslogg (`user_id`, `photo_url`). Raderas explicit idag. |
| `week_plans` | BEHÅLL | Hushållsdata, ingen PII. |
| `week_plan_entries` | BEHÅLL | Hushållsdata. |
| `cooking_sessions` | ANONYMISERA | `started_by_user_id` ska vara `NULL`. Sessionen är hushållshistorik. |
| `cooking_assumption_profiles` | BEHÅLL / RADERA | Hushållsaggregat. Följer hushållets öde. |
| `waste_summaries` | BEHÅLL | Hushållsaggregat, ingen PII. |
**Bedömning:** Dessa tabeller är i princip ohanterade idag. De flesta är hushållsdata och ska anonymiseras snarare än raderas, förutsatt att hushållet överlever.
@@ -98,10 +100,10 @@
### 2.5 Skanningar, AI-korrigeringar & träningsbank — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|---|---|---|
| `scan_jobs` | Innehåller uppladdade bilder (`s3_keys`) och användarspecifika resultat. | ✅ raderas explicit. |
| `ai_corrections` | Innehåller personliga korrigeringar och bildnyckel. | ✅ raderas explicit. |
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------ | ---------------------------------------------------------------------------- | ---------------------------------------- |
| `scan_jobs` | Innehåller uppladdade bilder (`s3_keys`) och användarspecifika resultat. | ✅ raderas explicit. |
| `ai_corrections` | Innehåller personliga korrigeringar och bildnyckel. | ✅ raderas explicit. |
| `ai_training_bank` | Innehåller träningsbild och korrigeringsdata. Cascadar via `ai_corrections`. | ✅ raderas via `ai_corrections` cascade. |
**Bedömning:** Godkänd i föregående fix. Ingen ytterligare åtgärd.
@@ -110,17 +112,17 @@
### 2.6 Recept & matminnen — RADERA eller ANONYMISERA
| Tabell | Klassning | Motivering |
|---|---|---|
| `recipes` | ANONYMISERA | Publik/verifierad receptdata är inte personlig, men `creator_user_id` och `creator_display_name` är personliga. **Draft/private**-recept däremot är personliga och bör raderas. |
| `recipe_ratings` | RADERA | Användarbetyg och kommentar är personligt. | ✅ raderas ej idag (lucka). |
| `recipe_favorites` | RADERA | Användarens favoriter är personligt. | ❌ **saknas** (lucka). |
| `recipe_cooks` | RADERA / ANONYMISERA | Logg över vilka recept användaren lagat. Personligt. | ❌ **saknas** (lucka). |
| `food_memories` | RADERA | Matminne med eventuell bild (`photo_url`). Personligt. | ❌ **saknas** (lucka). |
| `memory_items` | RADERA | AAMOS-minne — personligt. | ✅ raderas explicit. |
| `taste_signals` | RADERA | Smakpreferenser kopplade till användaren. | ✅ raderas explicit. |
| `creator_stats` | RADERA | Creator-profil, följare, statistik. Personligt. | ❌ **saknas** (lucka). |
| `creator_follows` | RADERA | Följer/följare-relationer. Personliga. | ❌ **saknas** (lucka). |
| Tabell | Klassning | Motivering |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recipes` | ANONYMISERA | Publik/verifierad receptdata är inte personlig, men `creator_user_id` och `creator_display_name` är personliga. **Draft/private**-recept däremot är personliga och bör raderas. |
| `recipe_ratings` | RADERA | Användarbetyg och kommentar är personligt. | ✅ raderas ej idag (lucka). |
| `recipe_favorites` | RADERA | Användarens favoriter är personligt. | ❌ **saknas** (lucka). |
| `recipe_cooks` | RADERA / ANONYMISERA | Logg över vilka recept användaren lagat. Personligt. | ❌ **saknas** (lucka). |
| `food_memories` | RADERA | Matminne med eventuell bild (`photo_url`). Personligt. | ❌ **saknas** (lucka). |
| `memory_items` | RADERA | AAMOS-minne — personligt. | ✅ raderas explicit. |
| `taste_signals` | RADERA | Smakpreferenser kopplade till användaren. | ✅ raderas explicit. |
| `creator_stats` | RADERA | Creator-profil, följare, statistik. Personligt. | ❌ **saknas** (lucka). |
| `creator_follows` | RADERA | Följer/följare-relationer. Personliga. | ❌ **saknas** (lucka). |
**Bedömning:** Receptinteraktioner är i stort sett ohanterade.
@@ -128,10 +130,10 @@
### 2.7 Notiser & push — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
|---|---|---|
| `notifications` | Push-/inapp-notiser till användaren. | ❌ **saknas** (cascade fyrar inte). |
| `push_tokens` | Push-token för Expo. Teknisk identifierare kopplad till användaren. | ❌ **saknas** (cascade fyrar inte). |
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| --------------- | ------------------------------------------------------------------- | ----------------------------------- |
| `notifications` | Push-/inapp-notiser till användaren. | ❌ **saknas** (cascade fyrar inte). |
| `push_tokens` | Push-token för Expo. Teknisk identifierare kopplad till användaren. | ❌ **saknas** (cascade fyrar inte). |
**Bedömning:** Båda är luckor.
@@ -139,14 +141,14 @@
### 2.8 Betalningar & prenumerationer — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
| Tabell | Klassning | Motivering |
|---|---|---|
| `subscriptions` | BEHÅLL | Bokförings- och rättslig grund för att bevisa köp/åtkomst. `user_id` bör pseudonymiseras eller sättas `NULL` efter en viss period, men transaktionens existens måste kunna verifieras. Retention: minst 7 år (Bokföringslagen) eller tills rättslig tvist är avslutad. |
| `subscription_events` | BEHÅLL | Händelselogg för prenumeration. Samma grund. `user_id` kan anonymiseras. |
| `store_transactions` | BEHÅLL | Råa store-transaktioner för revision. `raw_payload` kan innehålla PII (store-kvitto) — **ska skrubbas** så att endast transaktions-ID, produkt-ID och belopp återstår. `user_id` anonymiseras. Retention: 7 år. |
| `store_notifications` | BEHÅLL | App/Play-servernotiser. Ingen direkt PII i sig, men kan innehålla transaktions-ID. Behålls för spårbarhet. |
| `trials` | RADERA / BEHÅLL | Trial-spårning. För rättvis kvot/fair-use kan vi behöva spara att denna person redan provat. Men efter kontoradering bör raden raderas eller pseudonymiseras (t.ex. hash av ursprungligt user_id för anti-fraud). **Förslag:** radera vid GDPR-radering; anti-fraud hanteras via store-transaktioner istället. | ❌ **saknas** idag. |
| `ai_usage_counters` | RADERA | Månatlig AI-användning. Ingen laglig grund efter kontoradering. | ❌ **saknas** idag. |
| Tabell | Klassning | Motivering |
| --------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptions` | BEHÅLL | Bokförings- och rättslig grund för att bevisa köp/åtkomst. `user_id` bör pseudonymiseras eller sättas `NULL` efter en viss period, men transaktionens existens måste kunna verifieras. Retention: minst 7 år (Bokföringslagen) eller tills rättslig tvist är avslutad. |
| `subscription_events` | BEHÅLL | Händelselogg för prenumeration. Samma grund. `user_id` kan anonymiseras. |
| `store_transactions` | BEHÅLL | Råa store-transaktioner för revision. `raw_payload` kan innehålla PII (store-kvitto) — **ska skrubbas** så att endast transaktions-ID, produkt-ID och belopp återstår. `user_id` anonymiseras. Retention: 7 år. |
| `store_notifications` | BEHÅLL | App/Play-servernotiser. Ingen direkt PII i sig, men kan innehålla transaktions-ID. Behålls för spårbarhet. |
| `trials` | RADERA / BEHÅLL | Trial-spårning. För rättvis kvot/fair-use kan vi behöva spara att denna person redan provat. Men efter kontoradering bör raden raderas eller pseudonymiseras (t.ex. hash av ursprungligt user_id för anti-fraud). **Förslag:** radera vid GDPR-radering; anti-fraud hanteras via store-transaktioner istället. | ❌ **saknas** idag. |
| `ai_usage_counters` | RADERA | Månatlig AI-användning. Ingen laglig grund efter kontoradering. | ❌ **saknas** idag. |
**Bedömning:** Finansiella poster får inte raderas, men PII måste skalas bort. `subscriptions`, `subscription_events`, `store_transactions` behöver en anonymiseringsfunktion, inte deletion.
@@ -154,38 +156,38 @@
### 2.9 Analytics & audit — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
| Tabell | Klassning | Motivering |
|---|---|---|
| `product_analytics_events` | RADERA | Pseudonyma events, men `anonymous_id` kan länka till användaren. Hard-delete görs redan via `deleteUserAnalyticsEvents`. | ✅ raderas explicit. |
| `analytics_daily_snapshots` | BEHÅLL | Hushållsaggregat, ingen PII. |
| `audit_logs` | BEHÅLL | Säkerhets-/rättslig grund. `actor_user_id`, `ip` och ev. PII i `metadata` ska anonymiseras (`actor_user_id``NULL`, `ip``NULL`, metadata skrubbas). Retention: minst 2 år, alternativt tills rättslig tvist är avslutad. | ❌ **saknas** idag. |
| `domain_events` | BEHÅLL / ANONYMISERA | Event-sourcing outbox. `user_id` och PII i `payload` ska anonymiseras för att inte bryta downstream-konsumenter. | ❌ **saknas** idag. |
| `idempotency_keys` | RADERA | Caching av API-svar kopplade till användaren. Ingen laglig grund. | ❌ **saknas** idag. |
| Tabell | Klassning | Motivering |
| --------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product_analytics_events` | RADERA | Pseudonyma events, men `anonymous_id` kan länka till användaren. Hard-delete görs redan via `deleteUserAnalyticsEvents`. | ✅ raderas explicit. |
| `analytics_daily_snapshots` | BEHÅLL | Hushållsaggregat, ingen PII. |
| `audit_logs` | BEHÅLL | Säkerhets-/rättslig grund. `actor_user_id`, `ip` och ev. PII i `metadata` ska anonymiseras (`actor_user_id``NULL`, `ip``NULL`, metadata skrubbas). Retention: minst 2 år, alternativt tills rättslig tvist är avslutad. | ❌ **saknas** idag. |
| `domain_events` | BEHÅLL / ANONYMISERA | Event-sourcing outbox. `user_id` och PII i `payload` ska anonymiseras för att inte bryta downstream-konsumenter. | ❌ **saknas** idag. |
| `idempotency_keys` | RADERA | Caching av API-svar kopplade till användaren. Ingen laglig grund. | ❌ **saknas** idag. |
---
### 2.10 Globala system- och översättningstabeller — BEHÅLL (ingen PII)
| Tabell | Klassning | Motivering |
|---|---|---|
| `canonical_ingredients` | BEHÅLL | Global produktdata. |
| `ingredient_translations` | BEHÅLL | Översättningar av globala ingredienser. |
| `unit_translations` | BEHÅLL | Översättningar av enheter. |
| `recipe_translations` | BEHÅLL | Översättningar av recept. |
| `recipe_step_translations` | BEHÅLL | Översättningar av receptsteg. |
| `recipe_source_registry` | BEHÅLL | Juridisk källregister. |
| `recipe_ingredients` | BEHÅLL | Receptdata. |
| `recipe_steps` | BEHÅLL | Receptdata. |
| `recipe_similarities` | BEHÅLL | Systemdata. |
| `substitutions` | BEHÅLL | Systemdata. |
| `nutrition_display_profiles` | BEHÅLL | Systemdata. |
| `allergen_market_rules` | BEHÅLL | Systemdata. |
| `season_events` | BEHÅLL | Systemdata. |
| `feature_flags` | BEHÅLL | Systemdata. |
| `release_gates` | BEHÅLL | Systemdata. |
| `inventory_decay_profiles` | BEHÄLL | Systemdata. |
| `ai_eval_cases` | BEHÅLL | Testdata. |
| `ai_eval_runs` | BEHÅLL | Testkörningsresultat. |
| Tabell | Klassning | Motivering |
| ---------------------------- | --------- | --------------------------------------- |
| `canonical_ingredients` | BEHÅLL | Global produktdata. |
| `ingredient_translations` | BEHÅLL | Översättningar av globala ingredienser. |
| `unit_translations` | BEHÅLL | Översättningar av enheter. |
| `recipe_translations` | BEHÅLL | Översättningar av recept. |
| `recipe_step_translations` | BEHÅLL | Översättningar av receptsteg. |
| `recipe_source_registry` | BEHÅLL | Juridisk källregister. |
| `recipe_ingredients` | BEHÅLL | Receptdata. |
| `recipe_steps` | BEHÅLL | Receptdata. |
| `recipe_similarities` | BEHÅLL | Systemdata. |
| `substitutions` | BEHÅLL | Systemdata. |
| `nutrition_display_profiles` | BEHÅLL | Systemdata. |
| `allergen_market_rules` | BEHÅLL | Systemdata. |
| `season_events` | BEHÅLL | Systemdata. |
| `feature_flags` | BEHÅLL | Systemdata. |
| `release_gates` | BEHÅLL | Systemdata. |
| `inventory_decay_profiles` | BEHÄLL | Systemdata. |
| `ai_eval_cases` | BEHÅLL | Testdata. |
| `ai_eval_runs` | BEHÅLL | Testkörningsresultat. |
---
@@ -193,39 +195,39 @@
Nedan är tabeller som **idag varken raderas, anonymiseras eller motiveras att behållas** vid kontoradering.
| # | Tabell | Kategori | Risk | Föreslagen åtgärd |
|---|---|---|---|---|
| 1 | `user_credentials` | Auth | Lösenordshash överlever radering. | `DELETE FROM user_credentials WHERE user_id = $1` |
| 2 | `admin_totp` | Auth | TOTP-secret överlever. | `DELETE` |
| 3 | `email_verification_tokens` | Auth | Tokens överlever. | `DELETE` |
| 4 | `password_reset_tokens` | Auth | Tokens överlever. | `DELETE` |
| 5 | `user_consents` | Samtycken | Samtyckeshistorik överlever. | `DELETE` |
| 6 | `user_locale_preferences` | Preferenser | Locale-preferenser överlever. | `DELETE` |
| 7 | `household_members` | Hushåll | Användaren kvarstår som medlem. | `DELETE` |
| 8 | `households` (+ cascading data) | Hushåll | Enpersonshushåll läcker persondata. | **Beslut krävs** (se §2.3) |
| 9 | `inventory_transactions` | Hushåll | `actor_user_id` kvarstår. | `SET actor_user_id = NULL` |
| 10 | `inventory_conflicts` | Hushåll | `resolved_by_user_id` kvarstår. | `SET resolved_by_user_id = NULL` |
| 11 | `shopping_list_items` | Hushåll | `added_by_user_id` kvarstår. | `SET added_by_user_id = NULL` |
| 12 | `meal_boxes` | Hushåll | `reserved_for_user_id` kvarstår. | `SET reserved_for_user_id = NULL` |
| 13 | `cooking_sessions` | Hushåll | `started_by_user_id` kvarstår. | `SET started_by_user_id = NULL` |
| 14 | `receipts` | Hushåll | Kvittobild (`image_url`) kan innehålla PII. | Radera bild i lagring, sätt `image_url = NULL` |
| 15 | `receipt_lines` | Hushåll | OCR-text kan innehålla PII. | Skrubba `raw_text` eller radera rader om hushåll raderas. |
| 16 | `recipe_ratings` | Recept | Betyg/kommentar överlever. | `DELETE` |
| 17 | `recipe_favorites` | Recept | Favoriter överlever. | `DELETE` |
| 18 | `recipe_cooks` | Recept | Lagningslogg överlever. | `DELETE` |
| 19 | `food_memories` | Matminnen | Matminnen + bild överlever. | `DELETE` (+ bild i lagring) |
| 20 | `creator_stats` | Creator | Creator-profil överlever. | `DELETE` |
| 21 | `creator_follows` | Creator | Följer/följare överlever. | `DELETE` |
| 22 | `notifications` | Notiser | Notiser överlever. | `DELETE` |
| 23 | `push_tokens` | Push | Push-token överlever. | `DELETE` |
| 24 | `trials` | Trial | Trial-info överlever. | `DELETE` |
| 25 | `ai_usage_counters` | Usage | AI-användning överlever. | `DELETE` |
| 26 | `audit_logs` | Audit | `actor_user_id`, `ip`, metadata överlever. | Anonymisera (`NULL` + skrubba metadata) |
| 27 | `domain_events` | Events | `user_id` + PII i payload överlever. | Anonymisera (`NULL` + skrubba payload) |
| 28 | `idempotency_keys` | API | Cachade svar kopplade till användaren överlever. | `DELETE` |
| 29 | `subscriptions` | Betalning | `user_id` kvarstår för evigt. | Pseudonymisera efter retention (eller direkt vid radering om ekonomiskt spår finns kvar i `store_transactions`) |
| 30 | `subscription_events` | Betalning | `user_id` + payload kvarstår. | Anonymisera (`user_id = NULL`, skrubba payload) |
| 31 | `store_transactions` | Betalning | `raw_payload` kan innehålla PII. | Skrubba `raw_payload`, anonymisera `user_id` |
| # | Tabell | Kategori | Risk | Föreslagen åtgärd |
| --- | ------------------------------- | ----------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| 1 | `user_credentials` | Auth | Lösenordshash överlever radering. | `DELETE FROM user_credentials WHERE user_id = $1` |
| 2 | `admin_totp` | Auth | TOTP-secret överlever. | `DELETE` |
| 3 | `email_verification_tokens` | Auth | Tokens överlever. | `DELETE` |
| 4 | `password_reset_tokens` | Auth | Tokens överlever. | `DELETE` |
| 5 | `user_consents` | Samtycken | Samtyckeshistorik överlever. | `DELETE` |
| 6 | `user_locale_preferences` | Preferenser | Locale-preferenser överlever. | `DELETE` |
| 7 | `household_members` | Hushåll | Användaren kvarstår som medlem. | `DELETE` |
| 8 | `households` (+ cascading data) | Hushåll | Enpersonshushåll läcker persondata. | **Beslut krävs** (se §2.3) |
| 9 | `inventory_transactions` | Hushåll | `actor_user_id` kvarstår. | `SET actor_user_id = NULL` |
| 10 | `inventory_conflicts` | Hushåll | `resolved_by_user_id` kvarstår. | `SET resolved_by_user_id = NULL` |
| 11 | `shopping_list_items` | Hushåll | `added_by_user_id` kvarstår. | `SET added_by_user_id = NULL` |
| 12 | `meal_boxes` | Hushåll | `reserved_for_user_id` kvarstår. | `SET reserved_for_user_id = NULL` |
| 13 | `cooking_sessions` | Hushåll | `started_by_user_id` kvarstår. | `SET started_by_user_id = NULL` |
| 14 | `receipts` | Hushåll | Kvittobild (`image_url`) kan innehålla PII. | Radera bild i lagring, sätt `image_url = NULL` |
| 15 | `receipt_lines` | Hushåll | OCR-text kan innehålla PII. | Skrubba `raw_text` eller radera rader om hushåll raderas. |
| 16 | `recipe_ratings` | Recept | Betyg/kommentar överlever. | `DELETE` |
| 17 | `recipe_favorites` | Recept | Favoriter överlever. | `DELETE` |
| 18 | `recipe_cooks` | Recept | Lagningslogg överlever. | `DELETE` |
| 19 | `food_memories` | Matminnen | Matminnen + bild överlever. | `DELETE` (+ bild i lagring) |
| 20 | `creator_stats` | Creator | Creator-profil överlever. | `DELETE` |
| 21 | `creator_follows` | Creator | Följer/följare överlever. | `DELETE` |
| 22 | `notifications` | Notiser | Notiser överlever. | `DELETE` |
| 23 | `push_tokens` | Push | Push-token överlever. | `DELETE` |
| 24 | `trials` | Trial | Trial-info överlever. | `DELETE` |
| 25 | `ai_usage_counters` | Usage | AI-användning överlever. | `DELETE` |
| 26 | `audit_logs` | Audit | `actor_user_id`, `ip`, metadata överlever. | Anonymisera (`NULL` + skrubba metadata) |
| 27 | `domain_events` | Events | `user_id` + PII i payload överlever. | Anonymisera (`NULL` + skrubba payload) |
| 28 | `idempotency_keys` | API | Cachade svar kopplade till användaren överlever. | `DELETE` |
| 29 | `subscriptions` | Betalning | `user_id` kvarstår för evigt. | Pseudonymisera efter retention (eller direkt vid radering om ekonomiskt spår finns kvar i `store_transactions`) |
| 30 | `subscription_events` | Betalning | `user_id` + payload kvarstår. | Anonymisera (`user_id = NULL`, skrubba payload) |
| 31 | `store_transactions` | Betalning | `raw_payload` kan innehålla PII. | Skrubba `raw_payload`, anonymisera `user_id` |
**Totalt:** 31 tabell-förekomster som behöver åtgärdas (många är `SET NULL`, inte radering).
@@ -235,12 +237,12 @@ Nedan är tabeller som **idag varken raderas, anonymiseras eller motiveras att b
Förutom redan hanterade `scan_jobs.s3_keys`, `ai_corrections.image_s3_key` och `ai_training_bank.image_s3_key` finns följande användarbilder:
| Källa | Kolumn | Risk | Åtgärd vid GDPR-radering |
|---|---|---|---|
| `meals.photo_url` | Foto på måltid | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `food_memories.photo_url` | Foto från matminne | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `receipts.image_url` | Kvittobild | Potentiellt personlig (kortnummer, butik, köp). | Radera objektet i lagring; sätt `image_url = NULL`. |
| `recipes.image_urls` | Receptbild | Om receptet är **draft/private** och skapats av användaren: personlig. Om publikt/verifierat: delad resurs. | För draft/private: radera bilderna. För publika: behåll (om användaren samtyckt) eller anonymisera skaparen. |
| Källa | Kolumn | Risk | Åtgärd vid GDPR-radering |
| ------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `meals.photo_url` | Foto på måltid | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `food_memories.photo_url` | Foto från matminne | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `receipts.image_url` | Kvittobild | Potentiellt personlig (kortnummer, butik, köp). | Radera objektet i lagring; sätt `image_url = NULL`. |
| `recipes.image_urls` | Receptbild | Om receptet är **draft/private** och skapats av användaren: personlig. Om publikt/verifierat: delad resurs. | För draft/private: radera bilderna. För publika: behåll (om användaren samtyckt) eller anonymisera skaparen. |
**Notering:** Det finns ingen dedikerad profilbildskolumn i `users`. Om sådan läggs till i framtiden måste den inkluderas i residual-testet.
@@ -277,6 +279,7 @@ describe("DELETE /v1/me — GDPR residual test", () => {
### 5.2 Hjälpfunktioner
`seedAllPersonalTables(userId, token)` ska skapa exempeldata i:
- `user_credentials` (skapas vid registrering)
- `user_health_profiles`, `user_preferences`, `user_locale_preferences`, `user_consents`
- `households` + `household_members` (+ `storage_locations`)
@@ -291,6 +294,7 @@ describe("DELETE /v1/me — GDPR residual test", () => {
- `receipts` + `receipt_lines`
`assertNoPersonalTraces(db, userId)` ska:
1. För **RADERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId`.
2. För **ANONYMISERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId` (eftersom `user_id` ska vara `NULL`).
3. För **BEHÅLL**-tabeller: tillåta rader med `user_id = $userId` **men** kontrollera att känsliga fält (`raw_payload`, `metadata`, `ip`) är skrubbade.
@@ -347,6 +351,7 @@ Följande punkter behöver ditt (Johans) godkännande eller vägledning:
## 7. Sammanfattning & rekommenderad ordningsföljd
### 7.1 Snabbfix (högsta riskerna)
1. Radera auth-tabeller: `user_credentials`, `admin_totp`, `email_verification_tokens`, `password_reset_tokens`.
2. Radera preferenser/samtycken: `user_consents`, `user_locale_preferences`.
3. Radera notiser/push: `notifications`, `push_tokens`.
@@ -357,11 +362,13 @@ Följande punkter behöver ditt (Johans) godkännande eller vägledning:
8. Hantera kvitton: radera `receipts.image_url` i lagring, skrubba `receipt_lines.raw_text`.
### 7.2 Större beslut
9. Hushållsstrategi: radera enpersonshushåll, anonymisera delade hushåll.
10. Finansiella poster: anonymisera `subscriptions`/`subscription_events`/`store_transactions` enligt retention.
11. Audit/domain events: anonymisera men behåll enligt retention.
### 7.3 Permanent vakt
12. Implementera residual-testet i `apps/api/test/me.residual.test.ts` med schema-diff-assertion.
---
+4
View File
@@ -5,22 +5,26 @@
## Befintliga motorer
### Sökmotor: `/v1/inventory`
- Enkel `ILIKE` mot `inventory_items.display_name` och `brand`.
- Inga synonymer, ingen fonetisk matchning, ingen felstavningstolerans.
- Filtrering på `storage_location_id`, `expiry_status`, paginering.
### Förbättrad sökmotor: `/v1/inventory/natural-search`
- Kombinerar `unaccent(...) ILIKE unaccent(...)` med PostgreSQL `pg_trgm`-similarity.
- Söker även mot kanoniska ingrediensnamn (`name_sv`, `name_en`).
- Sorterar på `GREATEST(similarity(...))`.
- Returnerar både strukturerade träffar och ett naturligt-språkligt svar.
### Trust-motor (`@app/inventory-engine`)
- `computeTrust(...)` ger `trustState` (`trusted`/`decaying`/`stale`/`unverified`) och `trustScore` 0100.
- Baserat på `confidence`, `verifiedByUser`, ålder och förruttnelseprofil.
- Används redan i `/v1/inventory` och vid skanning/konsumtion.
### FoodTwin-data
- `storage_locations`: namn, typ, sublocations.
- `inventory_items`: `storage_location_id`, `sublocation`, kvantitet, enhet, `trust_state`.
- `canonical_ingredients`: kanoniskt namn, synonymer, hållbarhetsriktlinjer.
+39 -39
View File
@@ -3,7 +3,7 @@
> Detta dokument definierar **en gång** hur Cibello ska kännas personlig, så att
> alla AI-nära funktioner (sök, "vad ska vi äta", rekommendationer, proaktiva
> puffar, receptpresentation) byggs mot samma själ i stället för att uppfinnas
> per funktion. Det är en kravspec för *känslan* inte en teknisk migrering.
> per funktion. Det är en kravspec för _känslan_ inte en teknisk migrering.
> Allt nedan förankras i motorer som redan finns i plattformen.
---
@@ -13,7 +13,7 @@
Cibello är inte "ännu en receptsök". Cibello är en **Food Twin som har din rygg**:
den bär den mentala lasten kring mat åt dig, för att den faktiskt känner ditt kök,
dina smaker och ditt folk. Målet är att en vanlig, trött människa ska känna att
appen *ser* dem och gör vardagen enklare aldrig att de blir övervakade eller
appen _ser_ dem och gör vardagen enklare aldrig att de blir övervakade eller
bedömda.
Ledstjärna i en mening: **"Den vet var räkosten står, förstår att du tycker den
@@ -24,10 +24,10 @@ Ledstjärna i en mening: **"Den vet var räkosten står, förstår att du tycker
## 2. Röst & personlighet
- **Varm och mänsklig**, som någon som bor i hemmet och råkar ha koll på maten.
Erkänner suget ("jag fattar, den är ju god"), dömer aldrig.
Erkänner suget ("jag fattar, den är ju god"), dömer aldrig.
- **Kompetent men ödmjuk**. Säker när den vet, öppen när den gissar. Skryter inte.
- **Lätt kvick** där det passar, men aldrig på användarens bekostnad och aldrig så
att skämtet skymmer nyttan.
att skämtet skymmer nyttan.
- **Kort och konkret**. En trött människa vill ha svar, inte en essä.
- **Aldrig en robot, aldrig en tjatig coach.** Ingen pekpinne, ingen skam.
@@ -39,19 +39,19 @@ hårdkodas per skärm.
## 3. Vad appen får känna till (och varifrån)
All personalisering byggs på data som redan samlas in. "Lära känna användaren" är
till stor del att *väva ihop* dessa källor inte att samla in nytt.
till stor del att _väva ihop_ dessa källor inte att samla in nytt.
| Vad appen kan veta | Källa som redan finns |
| --- | --- |
| Vad du har hemma och var | `inventory_items` + `storage_locations` (+ `sublocation`) |
| Hur säker den är på att det stämmer | Trust-motorn (`trusted`/`decaying`/`stale`) |
| Vad du gillar/ogillar | `taste_signals`, betyg, laghistorik |
| Hur mycket ni brukar äta | `cooking_assumption_profiles` |
| Vilka ni är i hushållet | `household_members`, personer/portioner |
| Allergener & kostbehov | `user_health_profiles` (**alltid användarsatt**) |
| Uttalade mål & preferenser | onboarding-mål, `user_preferences` |
| Uttryckliga minnen | `memory_items` / `food_memories` ("barnen ogillar broccoli") |
| Språk | locale-preferens (×12) |
| Vad appen kan veta | Källa som redan finns |
| ----------------------------------- | ------------------------------------------------------------ |
| Vad du har hemma och var | `inventory_items` + `storage_locations` (+ `sublocation`) |
| Hur säker den är på att det stämmer | Trust-motorn (`trusted`/`decaying`/`stale`) |
| Vad du gillar/ogillar | `taste_signals`, betyg, laghistorik |
| Hur mycket ni brukar äta | `cooking_assumption_profiles` |
| Vilka ni är i hushållet | `household_members`, personer/portioner |
| Allergener & kostbehov | `user_health_profiles` (**alltid användarsatt**) |
| Uttalade mål & preferenser | onboarding-mål, `user_preferences` |
| Uttryckliga minnen | `memory_items` / `food_memories` ("barnen ogillar broccoli") |
| Språk | locale-preferens (×12) |
---
@@ -61,14 +61,14 @@ Detta är regeln som gör det personliga **magiskt i stället för trasigt eller
creepy**, och den är icke-förhandlingsbar:
- Appen påstår **bara** fakta som finns i verklig data. "Står i kyldörren" endast
om lager + plats säger det. Ingen påhittad hylla, ingen påhittad vara.
om lager + plats säger det. Ingen påhittad hylla, ingen påhittad vara.
- Osäkerhet kommer från **trust-motorn**, aldrig från ett påklistrat skämt. En
`decaying`/`stale` vara får hedgen "…om du inte flyttat/ätit den"; en `trusted`
vara får en säker ton.
`decaying`/`stale` vara får hedgen "…om du inte flyttat/ätit den"; en `trusted`
vara får en säker ton.
- Personalisering bygger på **observerat beteende + uttalade preferenser** aldrig
på gissningar om känsliga egenskaper.
på gissningar om känsliga egenskaper.
- Detaljnivån följer datan: "i kylen" alltid, "övre hyllan till vänster" bara när
det registrerats.
det registrerats.
Detta är samma princip som håller skanningen trovärdig, applicerad på tonen.
@@ -80,10 +80,10 @@ Detta är samma princip som håller skanningen trovärdig, applicerad på tonen.
osynlig. Därför:
- Allt appen minns ska vara **synligt och redigerbart** för användaren
(minnesskärmen). Inget hemligt profilbygge.
(minnesskärmen). Inget hemligt profilbygge.
- Personalisering är **samtyckesgatad** och kan stängas av per kategori.
- Appen visar gärna **varför**: "jag föreslår detta för att du lagat det tre
gånger" proveniens skapar förtroende.
gånger" proveniens skapar förtroende.
- Inget känsligt härleds tyst. Allergener och kostbehov **sätter användaren**.
- Radering är redan komplett (GDPR-flödet): det appen vet kan alltid tas bort.
@@ -95,9 +95,9 @@ En app som känner din mat måste vara snäll. Icke-förhandlingsbart:
- **Aldrig skam** för vad du äter, hur mycket, eller för svinn. Hjälp, polisa inte.
- **Ingen skadlig kaloripolisiär.** Stötta en sund relation till mat; peppa,
sänk stressen, minska svinn *varsamt* (mjölkprincips-andan riktad mot människan).
sänk stressen, minska svinn _varsamt_ (mjölkprincips-andan riktad mot människan).
- **Säkerhet går före personlighet.** Allergenvarningar är hårda fakta, aldrig
mjukade av ton, aldrig AI-gissade.
mjukade av ton, aldrig AI-gissade.
- **Familjesäkert.** Hushåll har barn; håll språk och innehåll åldersanpassat.
---
@@ -107,14 +107,14 @@ En app som känner din mat måste vara snäll. Icke-förhandlingsbart:
Samma själ, konsekvent applicerad:
- **"Vad ska vi äta?" / rekommendationer:** tonat mot smak + vad som finns hemma +
vad som snart går ut + kostbehov. Förklarar valet kort.
vad som snart går ut + kostbehov. Förklarar valet kort.
- **Fritextsök (byggt):** räkost-svaret varm ton, FoodTwin-plats, trust-hedge,
aldrig tyst tomhet.
aldrig tyst tomhet.
- **Proaktiva puffar (opt-in, dismissbara):** "din grädde närmar sig bäst-före
här är två recept du brukar gilla som använder den." Aldrig tjat.
här är två recept du brukar gilla som använder den." Aldrig tjat.
- **Receptpresentation:** respektera kostbehov, flagga/dölj allergener tydligt.
- **Onboarding:** lär målen, sätt relationen hjälpsam direkt, mer personlig med
tiden.
tiden.
---
@@ -148,15 +148,15 @@ i18n.
## 11. Koppling till befintliga motorer (så det blir ihopkoppling, inte nybygge)
| Persona-behov | Befintlig motor att väva in |
| --- | --- |
| "Vet var grejen står" | inventory + storage_locations + sublocation |
| "Ärlig om osäkerhet" | trust-motorn (states + score) |
| "Kommer ihåg mig" | memory_items / food_memories |
| "Vet vad jag gillar" | taste_signals + laghistorik |
| "Vet hur mycket vi äter" | cooking_assumption_profiles |
| "Respekterar mina behov" | user_health_profiles (användarsatt) |
| "Jag har kontroll" | user_consents + GDPR-radering (klart) |
| "Pratar mitt språk" | i18n ×12 |
| Persona-behov | Befintlig motor att väva in |
| ------------------------ | ------------------------------------------- |
| "Vet var grejen står" | inventory + storage_locations + sublocation |
| "Ärlig om osäkerhet" | trust-motorn (states + score) |
| "Kommer ihåg mig" | memory_items / food_memories |
| "Vet vad jag gillar" | taste_signals + laghistorik |
| "Vet hur mycket vi äter" | cooking_assumption_profiles |
| "Respekterar mina behov" | user_health_profiles (användarsatt) |
| "Jag har kontroll" | user_consents + GDPR-radering (klart) |
| "Pratar mitt språk" | i18n ×12 |
Nästan varje personlig känsla har redan en motor. Uppgiften är att ge den röst.
+51 -43
View File
@@ -15,65 +15,73 @@
### 1. `meatball_pork_beef` Köttbullar (färdiga)
| Fält | Nuvarande | Kommentar |
|---|---|---|
| `allergens` | `[]` | Färdiga köttbullar innehåller nästan alltid ströbröd (vetemjöl) och ofta ägg och/eller mjölk som bindemedel. |
| `isPork` | `true` | ✅ |
| `isBeef` | `true` | ✅ |
| Fält | Nuvarande | Kommentar |
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| `allergens` | `[]` | Färdiga köttbullar innehåller nästan alltid ströbröd (vetemjöl) och ofta ägg och/eller mjölk som bindemedel. |
| `isPork` | `true` | ✅ |
| `isBeef` | `true` | ✅ |
**Föreslagen rättning:**
```ts
allergens: ["gluten", "eggs", "milk"]
containsGluten: true
containsLactose: true
allergens: ["gluten", "eggs", "milk"];
containsGluten: true;
containsLactose: true;
```
**Motivering:** Standardrecept för svenska färdiga köttbullar innehåller ströbröd/vetemjöl, ägg och mjölk. Detta är den konservativa taggningen för produkttypen.
---
### 2. `falukorv` Falukorv
| Fält | Nuvarande | Kommentar |
|---|---|---|
| `allergens` | `[]` | Falukorv innehåller ofta potatismjöl eller vetemjöl samt mjölkpulver. |
| `isPork` | `true` | ✅ |
| `isBeef` | `true` | ✅ |
| Fält | Nuvarande | Kommentar |
| ----------- | --------- | --------------------------------------------------------------------- |
| `allergens` | `[]` | Falukorv innehåller ofta potatismjöl eller vetemjöl samt mjölkpulver. |
| `isPork` | `true` | ✅ |
| `isBeef` | `true` | ✅ |
**Föreslagen rättning:**
```ts
allergens: ["gluten", "milk"]
containsGluten: true
containsLactose: true
allergens: ["gluten", "milk"];
containsGluten: true;
containsLactose: true;
```
**Motivering:** Falukorv är en köttkorv som traditionellt innehåller späck, potatismjöl/vetemjöl och ofta mjölkpulver. Konservativ taggning för produkttypen.
---
### 3. `red_curry_paste` Röd currypasta
| Fält | Nuvarande | Kommentar |
|---|---|---|
| `allergens` | `[]` | Thailändsk röd currypasta (khrueang kaeng phet) innehåller traditionellt khrung (räkor eller fisk) som en av basingredienserna. |
| Fält | Nuvarande | Kommentar |
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `allergens` | `[]` | Thailändsk röd currypasta (khrueang kaeng phet) innehåller traditionellt khrung (räkor eller fisk) som en av basingredienserna. |
**Föreslagen rättning:**
```ts
allergens: ["crustaceans", "fish"]
allergens: ["crustaceans", "fish"];
```
**Motivering:** Röd currypasta tillverkas ofta med shrimp paste (kapi) eller fisk. Det finns veganska varianter, men produkttypen som sådan är högrisk för både fisk och skaldjur.
---
### 4. `taco_spice` Tacokrydda
| Fält | Nuvarande | Kommentar |
|---|---|---|
| `allergens` | `[]` | Kommersiell tacokrydda varierar kraftigt mellan märken; vissa innehåller selleri, gluten, mjölk eller soja. |
| Fält | Nuvarande | Kommentar |
| ----------- | --------- | ----------------------------------------------------------------------------------------------------------- |
| `allergens` | `[]` | Kommersiell tacokrydda varierar kraftigt mellan märken; vissa innehåller selleri, gluten, mjölk eller soja. |
**Föreslagen rättning:**
```ts
allergens: []
allergens: [];
// men lägg till mayContainAllergens: ["celery", "gluten", "milk", "soy"]
```
**Motivering:** Det finns inte en enhetlig sammansättning för tacokrydda. I stället för att hårdtagga bör vi markera "kan innehålla" för de vanligaste dolda allergenerna, så att användare med allvarliga allergier varnas utan att vi blockerar alla recept.
> **Notering:** `mayContainAllergens` finns redan i `IngredientSafetyInfo` men används inte i seed-modellen idag. Detta kräver ett mindre schema-API-tillägg.
@@ -82,8 +90,8 @@ allergens: []
### 5. `chicken_stock_cube` Kycklingbuljongtärning
| Fält | Nuvarande | Kommentar |
|---|---|---|
| Fält | Nuvarande | Kommentar |
| ----------- | ------------ | ----------------------------------------------------- |
| `allergens` | `["celery"]` | ✅ Korrekt. Buljongtärningar innehåller ofta selleri. |
**Föreslagen rättning:** Ingen ändring av huvudallergener. Överväg `mayContainAllergens: ["gluten", "milk", "soy", "egg"]` eftersom kommersiella tärningar ofta innehåller maltodextrin, jästextrakt eller mjölkprotein beroende på märke.
@@ -92,8 +100,8 @@ allergens: []
### 6. `vegetable_stock_cube` Grönsaksbuljongtärning
| Fält | Nuvarande | Kommentar |
|---|---|---|
| Fält | Nuvarande | Kommentar |
| ----------- | ------------ | ----------- |
| `allergens` | `["celery"]` | ✅ Korrekt. |
**Föreslagen rättning:** Ingen ändring av huvudallergener. Överväg `mayContainAllergens: ["gluten", "milk", "soy", "egg"]` av samma skäl som ovan.
@@ -104,21 +112,21 @@ allergens: []
Följande sammansatta ingredienser har redan korrekta allergen-taggar och behöver ingen åtgärd:
| Ingrediens | Nuvarande `allergens` | Bedömning |
|---|---|---|
| `noodles_egg` | `["gluten", "eggs"]` | ✅ |
| `bread_sourdough` | `["gluten"]` | ✅ |
| `tortilla` | `["gluten"]` | ✅ |
| `hamburger_bun` | `["gluten", "sesame"]` | ✅ |
| `breadcrumbs` | `["gluten"]` | ✅ |
| `soy_sauce` | `["soy", "gluten"]` | ✅ |
| `fish_sauce` | `["fish"]` | ✅ |
| `teriyaki_sauce` | `["soy", "gluten"]` | ✅ |
| `mayonnaise` | `["eggs", "mustard"]` | ✅ |
| `peanut_butter` | `["peanuts"]` | ✅ |
| `mustard` | `["mustard"]` | ✅ |
| `sesame_seeds` | `["sesame"]` | ✅ |
| `tofu` | `["soy"]` | ✅ |
| Ingrediens | Nuvarande `allergens` | Bedömning |
| ----------------- | ---------------------- | --------- |
| `noodles_egg` | `["gluten", "eggs"]` | ✅ |
| `bread_sourdough` | `["gluten"]` | ✅ |
| `tortilla` | `["gluten"]` | ✅ |
| `hamburger_bun` | `["gluten", "sesame"]` | ✅ |
| `breadcrumbs` | `["gluten"]` | ✅ |
| `soy_sauce` | `["soy", "gluten"]` | ✅ |
| `fish_sauce` | `["fish"]` | ✅ |
| `teriyaki_sauce` | `["soy", "gluten"]` | ✅ |
| `mayonnaise` | `["eggs", "mustard"]` | ✅ |
| `peanut_butter` | `["peanuts"]` | ✅ |
| `mustard` | `["mustard"]` | ✅ |
| `sesame_seeds` | `["sesame"]` | ✅ |
| `tofu` | `["soy"]` | ✅ |
## Rekommendation
+60 -31
View File
@@ -8,15 +8,16 @@
## 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. |
| 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).
@@ -29,13 +30,13 @@
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. |
| 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:**
@@ -61,6 +62,7 @@ För varje yta anger tabellen **vilken befintlig data/motor** som är primär k
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).
@@ -70,29 +72,34 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
- 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.
- 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}}."`
@@ -104,6 +111,7 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
- 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.
@@ -117,7 +125,9 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
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`.
@@ -125,6 +135,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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.
@@ -134,7 +145,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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.
@@ -145,6 +158,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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`.
@@ -156,7 +170,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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.
@@ -167,6 +183,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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.
@@ -177,7 +194,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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).
@@ -189,6 +208,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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).
@@ -199,7 +219,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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`.
@@ -208,6 +230,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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.
@@ -217,7 +240,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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.
@@ -225,6 +250,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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.
@@ -233,7 +259,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
---
### 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:
@@ -243,6 +271,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
- 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.
@@ -256,18 +285,18 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
## 5. RISKER + hur varje charter-regel testas
| Risk | Mitigering | Test/verifiering |
| --- | --- | --- |
| 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. |
| **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. |
---
@@ -289,9 +318,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
## 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 |
| 2026-08-10 | S1 implementerat: "Vad ska vi äta?" med provenansmallar, samtyckesgrind, personliga delpoäng, integrationstester; commit `0885f5b` | Sven / Johan |
| 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 |
| 2026-08-10 | S1 implementerat: "Vad ska vi äta?" med provenansmallar, samtyckesgrind, personliga delpoäng, integrationstester; commit `0885f5b` | Sven / Johan |
| 2026-08-10 | S2 implementerat: proaktiva puffar (expiring-ingredient) med dubbelgrind, 12-språksmallar, frekvens-/duplicate-gate, UTC-matte, integrationstester; commit `f494b18` | Sven / Johan |
+68 -37
View File
@@ -7,6 +7,7 @@
## Hermetiska testregler
Alla integrationstester ska vara hermetiska:
- De får inte bero på innehållet i `.env` (värden som `AAMOS_MODE`, `EMAIL_MODE`, `S3_MODE` skrivs över i `setup-env.ts`).
- De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
- De får inte bero på databasinnehåll som skapats utanför testet (exempelvis seedade recept från en tidigare körning). Testkörningen ansvarar själv för att testdatabasen är migrerad **och** seedad innan testerna startar; seeden är idempotent.
@@ -14,27 +15,29 @@ Alla integrationstester ska vara hermetiska:
## 1. REUSE/GAP-matris mot befintligt matlagningsflöde
| §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|------------------|-------------------------|--------|----------------------|
| **Lifecycle PLANNED → STARTED → COMPLETED/CANCELLED** | Saknas. `recipe_cooks` är en enda rad skapad vid `/cook` ("COMPLETED" implicit). Ingen separat sessionstabell. | **Saknas** | Inför `cooking_sessions` (se nedan). Behåll `recipe_cooks` som aggregerad historik för rekommendationer/variation. |
| **FEFO-avdrag från inventory (§6)** | `/v1/recipes/:id/cook` kör `allocateFefo()` och skriver `inventory_transactions.type = 'cook_use'` med `refType='recipe_cook'`. | **Finns** | Återanvänd oförändrad. Lägg till `cookingSessionId` i transaktionen för att kunna ångra en hel session. |
| **portioner / portionsCooked** | `cookRecipeInputSchema.portionsCooked` (124). Skalning i receptdetalj. | **Finns** | Återanvänd. |
| **Individuell förbrukning per ätare** | `eaters[]` med `portionFraction` loggas i `meals`. | **Finns** | Återanvänd. |
| **Minimala efterfrågor efteråt (§6.2)** | Efterflödet i `cooking/[id].tsx` frågar idag bara antal matlådor. Inga profiler för "hur mycket blev det kvar?" | **Delvis** | Inför 23 snabba frågor som extraheras till `cooking_sessions.actualPortionsEaten`, `leftoverEstimate` och `cookingSessionId` på meals. |
| **Partiell förbrukning (§6.3)** | Saknas. `/cook` drar allt på en gång. Användaren kan efteråt manuellt justera via `/v1/inventory/items/:id/transactions`. | **Saknas** | Låt `/cook` skapa `cook_use`-transaktioner med `undoUntil = cookingSessionId`; vid efterfrågan skriv reverseringstransaktioner för det som inte åts. |
| **undo_until (ångra hel session)** | Transaktionsmodellen stöder bara enskilda nya transaktioner. Ingen koppling tillbaka till en session. | **Saknas** | Lägg `cookingSessionId``inventory_transactions`, `meals`, `meal_boxes` och tillåt `POST /v1/cooking-sessions/:id/undo`. |
| **Rester till BEFINTLIGA meal_boxes (§6.4)** | `meal_boxes` finns. `/cook` kan skapa EN ny matlåda per tillfälle. | **Delvis** | Utöka så att rester kan läggas i en **befintlig** `meal_box` (matcha på recipeId + hushåll + `status=available`) istället för att alltid skapa ny. |
| **Anti-dubblettkartan: ingen prepared_food_batches-tabell** | Finns ingen sådan tabell. | **OK** | Behåll. Använd `meal_boxes` som enda restbehållare. Motivering: samma koncept (portioner kvar, ät-senast-datum, näringssnapshot), sparar dubblettlogik. |
| **Näringssnapshot på rester** | `meal_boxes.nutritionPerPortion` sparas vid skapande. | **Finns** | Återanvänd. |
| **recommendations läser recipe_cooks** | `recommendations.ts` gör `SELECT recipeId, max(cookedAt) FROM recipe_cooks GROUP BY recipeId`. | **Finns** | **Risk:** om vi ändrar semantiken i `recipe_cooks` måste denna fråga vara bakåtkompatibel. Rekommendation ska fortsätta se sista tillfället ett recept lagades. |
| **Testprotokoll steg 5** | "Laga nu → cooking mode → klart → logga → ingredienser dras från lagret; måltiden syns i Min dag". | **Finns** | Måste förbli sant. Nytt flöde får inte kräva fler steg än idag; nya frågor ska vara valbara/skipbara. |
| **Testprotokoll steg 8** | "Matlådor: laga recept med portioner till lådor → lådan syns med ät-senast-datum". | **Finns** | Måste förbli sant. Nya rest-flödet ska INTE bryta skapandet av nya lådor. |
| **Alla strängar ×12** | i18next + 12 locales. | **Finns** | Lägg till nya nycklar i alla 12 språk. |
| **Hermetiska tester** | `apps/api/test/*` använder `TEST_DATABASE_URL` och setup-env. | **Finns** | Fortsätt samma mönster. |
| **Mjölkprincipen i texter** | Alla UI-strängar undviker "kassera automatiskt" och skiljer bäst-före/sista-förbrukningsdag. | **Finns** | Fortsätt. Inga texter får garantera matsäkerhet. |
| §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Lifecycle PLANNED → STARTED → COMPLETED/CANCELLED** | Saknas. `recipe_cooks` är en enda rad skapad vid `/cook` ("COMPLETED" implicit). Ingen separat sessionstabell. | **Saknas** | Inför `cooking_sessions` (se nedan). Behåll `recipe_cooks` som aggregerad historik för rekommendationer/variation. |
| **FEFO-avdrag från inventory (§6)** | `/v1/recipes/:id/cook` kör `allocateFefo()` och skriver `inventory_transactions.type = 'cook_use'` med `refType='recipe_cook'`. | **Finns** | Återanvänd oförändrad. Lägg till `cookingSessionId` i transaktionen för att kunna ångra en hel session. |
| **portioner / portionsCooked** | `cookRecipeInputSchema.portionsCooked` (124). Skalning i receptdetalj. | **Finns** | Återanvänd. |
| **Individuell förbrukning per ätare** | `eaters[]` med `portionFraction` loggas i `meals`. | **Finns** | Återanvänd. |
| **Minimala efterfrågor efteråt (§6.2)** | Efterflödet i `cooking/[id].tsx` frågar idag bara antal matlådor. Inga profiler för "hur mycket blev det kvar?" | **Delvis** | Inför 23 snabba frågor som extraheras till `cooking_sessions.actualPortionsEaten`, `leftoverEstimate` och `cookingSessionId` på meals. |
| **Partiell förbrukning (§6.3)** | Saknas. `/cook` drar allt på en gång. Användaren kan efteråt manuellt justera via `/v1/inventory/items/:id/transactions`. | **Saknas** | Låt `/cook` skapa `cook_use`-transaktioner med `undoUntil = cookingSessionId`; vid efterfrågan skriv reverseringstransaktioner för det som inte åts. |
| **undo_until (ångra hel session)** | Transaktionsmodellen stöder bara enskilda nya transaktioner. Ingen koppling tillbaka till en session. | **Saknas** | Lägg `cookingSessionId``inventory_transactions`, `meals`, `meal_boxes` och tillåt `POST /v1/cooking-sessions/:id/undo`. |
| **Rester till BEFINTLIGA meal_boxes (§6.4)** | `meal_boxes` finns. `/cook` kan skapa EN ny matlåda per tillfälle. | **Delvis** | Utöka så att rester kan läggas i en **befintlig** `meal_box` (matcha på recipeId + hushåll + `status=available`) istället för att alltid skapa ny. |
| **Anti-dubblettkartan: ingen prepared_food_batches-tabell** | Finns ingen sådan tabell. | **OK** | Behåll. Använd `meal_boxes` som enda restbehållare. Motivering: samma koncept (portioner kvar, ät-senast-datum, näringssnapshot), sparar dubblettlogik. |
| **Näringssnapshot på rester** | `meal_boxes.nutritionPerPortion` sparas vid skapande. | **Finns** | Återanvänd. |
| **recommendations läser recipe_cooks** | `recommendations.ts` gör `SELECT recipeId, max(cookedAt) FROM recipe_cooks GROUP BY recipeId`. | **Finns** | **Risk:** om vi ändrar semantiken i `recipe_cooks` måste denna fråga vara bakåtkompatibel. Rekommendation ska fortsätta se sista tillfället ett recept lagades. |
| **Testprotokoll steg 5** | "Laga nu → cooking mode → klart → logga → ingredienser dras från lagret; måltiden syns i Min dag". | **Finns** | Måste förbli sant. Nytt flöde får inte kräva fler steg än idag; nya frågor ska vara valbara/skipbara. |
| **Testprotokoll steg 8** | "Matlådor: laga recept med portioner till lådor → lådan syns med ät-senast-datum". | **Finns** | Måste förbli sant. Nya rest-flödet ska INTE bryta skapandet av nya lådor. |
| **Alla strängar ×12** | i18next + 12 locales. | **Finns** | Lägg till nya nycklar i alla 12 språk. |
| **Hermetiska tester** | `apps/api/test/*` använder `TEST_DATABASE_URL` och setup-env. | **Finns** | Fortsätt samma mönster. |
| **Mjölkprincipen i texter** | Alla UI-strängar undviker "kassera automatiskt" och skiljer bäst-före/sista-förbrukningsdag. | **Finns** | Fortsätt. Inga texter får garantera matsäkerhet. |
### Slutsats av matrisen
Cooking Sessions är en **utbyggnad**, inte ett parallellsystem:
- **Kärnan** (`recipe_cooks`, FEFO-avdrag, meals, meal_boxes) finns redan.
- **Det som saknas** är en session-boog (`cooking_sessions`) som knyter samman påbörjat/klart/ångrat, plus möjligheten att (a) skjuta upp delar av förbrukningen till efterfrågan och (b) lägga rester i befintliga matlådor.
@@ -43,6 +46,7 @@ Cooking Sessions är en **utbyggnad**, inte ett parallellsystem:
## 2. Anti-dubblettkartan varför INGEN `prepared_food_batches`-tabell
Befintlig `meal_boxes` kan redan representera:
- Portioner som återstår
- Receptursprung (`recipeId`)
- Näringsvärden per portion (`nutritionPerPortion`)
@@ -52,6 +56,7 @@ Befintlig `meal_boxes` kan redan representera:
- Reservering för användare
Att lägga till `prepared_food_batches` skulle skapa:
- Dubbla konsumtions-endpoints
- Dubbla "ät senast"-logiker
- Dubbla måltidsloggningsvägar
@@ -65,9 +70,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
## 3. STEGPLAN
### Steg 3a Lifecycle + planned usage ovanpå befintlig `/cook`
**Mål:** En cooking session kan startas, slutföras eller avbrytas utan att bryta dagens `/cook`-beteende.
**Ändringar:**
1. Ny tabell `cooking_sessions`:
- `id`, `recipeId`, `householdId`, `startedByUserId`
- `status`: `planned`, `started`, `completed`, `cancelled`
@@ -86,6 +93,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
4. **Bakåtkompatibilitet:** Behåll `POST /v1/recipes/:id/cook` som ett kortkommando som skapar session + complete i samma anrop (så testprotokoll steg 5 fortsätter fungera).
**Berörda filer:**
- `packages/database/src/schema/recipes.ts` (cooking_sessions, recipe_cooks-kolumn)
- `packages/database/src/schema/meals.ts` (meal_boxes-kolumn)
- `packages/database/src/schema/inventory.ts` (inventory_transactions-kolumn)
@@ -100,6 +108,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
**Migration:** 0011_cooking_sessions.sql
**Testplan:**
- POST /cook/start → status started
- POST /cooking-sessions/:id/complete → samma resultat som idag: inventory dras, meals skapas, mealBox skapas
- POST /recipes/:id/cook (gammalt) fortfarande grönt
@@ -108,9 +117,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
---
### Steg 3b Minimala efterfrågor §6.2 + antagandeprofiler
**Mål:** Efter "klart" ställs max 23 snabba frågor. Appen kan fylla i svar automatiskt från profiler (t.ex. "vi brukar äta alla portioner" / "vi brukar ha 2 lådor över").
**Ändringar:**
1. Lägg kolumner på `cooking_sessions`:
- `actualPortionsEaten` (nullable int)
- `leftoverEstimatePortions` (nullable int)
@@ -124,6 +135,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
- Frågorna ska kunna skippas med ett tryck (default antas).
**Berörda filer:**
- `packages/database/src/schema/recipes.ts` (kolumner)
- `packages/validation/src/recipes.ts`
- `apps/api/src/routes/cooking-sessions.ts`
@@ -133,6 +145,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
**Migration:** 0012_cooking_questions.sql
**Testplan:**
- Profil sparas och återanvänds nästa session
- Skippa frågor → defaults används
- Validering: `actualPortionsEaten + leftoverEstimatePortions ≤ portionsCooked`
@@ -140,9 +153,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
---
### Steg 3c Partiell förbrukning §6.3 + undo_until
**Mål:** Om användaren säger att de bara åt 3 av 4 portioner, ska FEFO-avdraget justeras så att motsvarande råvaror återstår. En completed session kan ångras inom 24 h via append-only reverseringstransaktioner.
**Ändringar:**
1. Vid complete: använd `actualPortionsEaten` för att räkna om råvaror.
- Exempel: 4 planerade portioner → 3 ätna = använd 75 % av varje ingrediens.
- Om `actualPortionsEaten` är null, använd `portionsCooked` (dagens beteende).
@@ -165,6 +180,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
5. Lägg `cookingSessionId` i inventory item detail så användaren kan ångra enskilda transaktioner därifrån också.
**Berörda filer:**
- `apps/api/src/lib/cooking.ts` (`completeCookingSession`, `undoCookingSession`)
- `apps/api/src/routes/cooking-sessions.ts`
- `apps/api/src/lib/i18n.ts`
@@ -177,6 +193,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
**Migration:** 0014_expand_event_types.sql (nya domänhändelser i `event_type`-enum)
**Testplan:**
- Laga 4 portioner, ät 3 → lager innehåller 25 % kvar av varje ingrediens
- Undo → allt återställs
- Invariant: `computeBalance(transaktioner) === item.quantity` efter (a) complete, (b) undo, (c) undo + ny complete
@@ -187,9 +204,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
---
### Steg 3d Rester §6.4 via befintliga meal_boxes
**Mål:** Efterfrågade rester hamnar i antingen (1) befintlig available matlåda med samma `recipeId`, eller (2) ny matlåda.
**Ändringar:**
1. Vid complete, om `leftoverEstimatePortions > 0`:
- Sök `meal_boxes` där `recipeId = recipe.id`, `householdId`, `status = 'available'`, `frozen = false` (eller matcha valt frystillstånd), sorterat på `recommendedUseBy`.
- Om träff: öka `portions` och `portionsRemaining` med `leftoverEstimatePortions`, uppdatera `recommendedUseBy` till det tidigare av de två datumen (mjölkprincipen: behåll kortaste hållbarhet).
@@ -198,6 +217,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
3. Frysval: om användaren väljer frys, skapa/utöka fryst matlåda med `frozen=true`, useBy=90 dagar.
**Berörda filer:**
- `packages/database/src/schema/meals.ts`
- `apps/api/src/routes/recipes.ts` eller `cooking-sessions.ts`
- `apps/mobile/src/app/cooking/[id].tsx`
@@ -206,6 +226,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
**Migration:** 0013_meal_box_leftover_source.sql
**Testplan:**
- Två cooking sessions med samma recept → en matlåda har ökade portioner
- Frysta rester → separat fryst låda
- Ingen dubblett i rekommendationer: matlådor-först-sektionen räknar fortfarande `portionsRemaining > 0`
@@ -214,29 +235,29 @@ Att lägga till `prepared_food_batches` skulle skapa:
## 4. RISKER
| Risk | Påverkan | Minskning |
|------|----------|-----------|
| `recommendations.ts` använder `recipe_cooks` för "senast lagat". Om `cookedAt` ändras semantik bryts variation. | Medel | Behåll `recipe_cooks.cookedAt` som tidpunkt för färdig session. Lägg separata tidsstämplar på `cooking_sessions`. |
| Testprotokoll steg 5 kräver att "Laga nu → klart → logga" fortsätter fungera utan extra kranar. | Hög | Behåll `/recipes/:id/cook` som kortkommando. Nya frågor ska vara skipbara med tydliga defaults. |
| Testprotokoll steg 8 kräver att nya matlådor skapas. | Hög | Nya rest-flödet ska inte blockera skapandet; befintlig-låda-utökning är opt-in. |
| Partiell förbrukning kan leda till brutna invarianter om omräkning blir fel. | Hög | Invariantstest efter varje complete/undo; använd samma enhetskonvertering som FEFO. |
| Undo kan radera måltider som andra flöden refererar till. | Medel | Använd soft-delete (`deletedAt` på meals) eller markera som `cancelled` istället för hård radering. |
| UI-blödning: nya frågor kan kännas som ett formulär. | Medel | Designa som kort med +/- och "kom ihåg mitt val". |
| Matsäkerhetstexter kan osynligt bli för skarpa. | Medel | Varje ny i18n-nyckel granskas: aldrig "säkert att äta", alltid "lukta och smaka" / "rekommenderas före". |
| Hermetiska tester kan läcka `.env`. | Låg | Fortsätt `setup-env.ts` + `vitest.config.ts`. |
| Risk | Påverkan | Minskning |
| --------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `recommendations.ts` använder `recipe_cooks` för "senast lagat". Om `cookedAt` ändras semantik bryts variation. | Medel | Behåll `recipe_cooks.cookedAt` som tidpunkt för färdig session. Lägg separata tidsstämplar på `cooking_sessions`. |
| Testprotokoll steg 5 kräver att "Laga nu → klart → logga" fortsätter fungera utan extra kranar. | Hög | Behåll `/recipes/:id/cook` som kortkommando. Nya frågor ska vara skipbara med tydliga defaults. |
| Testprotokoll steg 8 kräver att nya matlådor skapas. | Hög | Nya rest-flödet ska inte blockera skapandet; befintlig-låda-utökning är opt-in. |
| Partiell förbrukning kan leda till brutna invarianter om omräkning blir fel. | Hög | Invariantstest efter varje complete/undo; använd samma enhetskonvertering som FEFO. |
| Undo kan radera måltider som andra flöden refererar till. | Medel | Använd soft-delete (`deletedAt` på meals) eller markera som `cancelled` istället för hård radering. |
| UI-blödning: nya frågor kan kännas som ett formulär. | Medel | Designa som kort med +/- och "kom ihåg mitt val". |
| Matsäkerhetstexter kan osynligt bli för skarpa. | Medel | Varje ny i18n-nyckel granskas: aldrig "säkert att äta", alltid "lukta och smaka" / "rekommenderas före". |
| Hermetiska tester kan läcka `.env`. | Låg | Fortsätt `setup-env.ts` + `vitest.config.ts`. |
---
## 5. Översiktliga berörda tabeller
| Tabell | Förändring |
|--------|------------|
| `cooking_sessions` | **Ny** |
| `recipe_cooks` | `cookingSessionId` nullable |
| `inventory_transactions` | `cookingSessionId` nullable |
| `meals` | `cookingSessionId` nullable, ev. `deletedAt` |
| `meal_boxes` | `cookingSessionId` nullable, `leftoverSource` text |
| `users` / `households` | post-cook-profil jsonb (steg 3b) |
| Tabell | Förändring |
| ------------------------ | -------------------------------------------------- |
| `cooking_sessions` | **Ny** |
| `recipe_cooks` | `cookingSessionId` nullable |
| `inventory_transactions` | `cookingSessionId` nullable |
| `meals` | `cookingSessionId` nullable, ev. `deletedAt` |
| `meal_boxes` | `cookingSessionId` nullable, `leftoverSource` text |
| `users` / `households` | post-cook-profil jsonb (steg 3b) |
---
@@ -251,15 +272,18 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
---
*Rapport färdig. Inväntar godkännande innan kod påbörjas.*
_Rapport färdig. Inväntar godkännande innan kod påbörjas._
## 7. Steg 3d Restflöde till matlådor: merge, undo och mjölkprincipen
### 7.1 Semantik: alla rester blir matlådor
När en cooking session avslutas skapas eller uppdateras alltid en `meal_box` för `leftoverEstimatePortions` (default = `mealBoxPortions` om användaren inte anger något annat). `mealBoxPortions` är endast en validering: `leftoverEstimatePortions >= mealBoxPortions`. Det är `leftoverEstimatePortions` som driver boxens totala portionsantal.
### 7.2 Merge-regel
En befintlig `meal_box` får rester tillagda endast om:
- samma `recipeId`
- samma `cookedAt`-datum (UTC-datum, inte timestamp)
- samma `frozen`-status
@@ -270,12 +294,15 @@ Om ingen match finns skapas en ny `meal_box`.
`recommendedUseBy` sätts från sessionens lagningsdatum + 3 dagar (kyl) eller + 90 dagar (frys). Vid merge sätts `recommendedUseBy` till det tidigare av de två datumen.
### 7.3 Spårbarhet och deterministisk undo
Vid complete sparas `mealBoxMutations` JSONB på `cooking_sessions`. Varje mutation innehåller:
- `mealBoxId`: UUID för den berörda boxen
- `deltaPortions`: hur många portioner som lades till
- `frozen`: boxens frystillstånd (redundans för felsäkerhet)
Undo av en session:
1. Itererar `mealBoxMutations`.
2. För varje mutation minskas både `portions` och `portionsRemaining` med `deltaPortions`.
3. Om `portions` blir 0 sätts `status = 'discarded'` och `portionsRemaining = 0`.
@@ -284,16 +311,20 @@ Undo av en session:
6. Sessionens status sätts till `undone`.
### 7.4 Mjölkprincipen
`recommendedUseBy` är en kvalitetssignal. UI använder texter som "Ät senast" och "lukta och smaka". Det finns ingen automatisk kassering när datum passeras; användaren avgör alltid med sinnena.
### 7.5 UI: undo-knapp
- **Efter matlagning** (`cooking/[id].tsx`): efter att användaren tryckt "Klart" visas en skärm med en "Ångra"-knapp inom 24 h.
- **Matlådelistan** (`meal-boxes.tsx`): varje box som har en `cookingSessionId` och är skapad inom de senaste 24 timmarna visar en "Ångra"-knapp. Knappen anropar `POST /v1/cooking-sessions/:cookingSessionId/undo`.
- Felfall visar serverns lokaliserade felmeddelande (aldrig hårdkodade strängar).
- i18n-nycklar: `common.undo`, `mealbox.undoConfirmTitle`, `mealbox.undoConfirmBody`, `mealbox.undoSuccess`.
### 7.6 Wrapper-beteende (OBLIGATORISK PUNKT 1)
`completeCookingSession()` i `apps/api/src/lib/cooking.ts` är den enda vägen för både legacy `/v1/recipes/:id/cook` och explicit `/v1/cooking-sessions/:id/complete`. I `c1ab2c8` stängdes hålet där legacy `/cook` tidigare lämnade fälten null. Wrappern:
1. Beräknar `actualPortionsEaten` om det saknas: `Math.max(0, plannedPortions - mealBoxPortions)`.
2. Beräknar `leftoverEstimatePortions` om det saknas: samma som `mealBoxPortions`.
3. Skickar båda värdena explicit in i `completeCookingSessionCore`.
+67 -37
View File
@@ -9,6 +9,7 @@
## Hermetiska testregler (samma som Fas 3)
Alla integrationstester ska vara hermetiska:
- De får inte bero på innehållet i `.env` (värden som `AAMOS_MODE`, `EMAIL_MODE`, `S3_MODE` skrivs över i `setup-env.ts`).
- De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
- De får inte bero på databasinnehåll som skapats utanför testet. Testkörningen ansvarar själv för att testdatabasen är migrerad **och** seedad innan testerna startar; seeden är idempotent.
@@ -18,26 +19,28 @@ Alla integrationstester ska vara hermetiska:
## 1. REUSE/GAP-matris mot befintligt hushållsflöde
| §7-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|------------------|-------------------------|--------|----------------------|
| **Hushållsgrundstruktur** | Tabellerna `households` och `household_members` finns. `households.invite_code` är unik. Roller: `owner`, `adult`, `member`, `child`. | **Finns** | Återanvänd oförändrat. Utöka med tidsbunden inbjudanslänk, inte ny inbjudningsmodell. |
| **Gå med via kod** | `POST /v1/households/join` med `{ inviteCode }`. Nya medlemmar får roll `adult`. Plan-gräns `maxHouseholdMembers` kontrolleras. | **Finns** | Återanvänd. Utöka med djuplänk + QR så att koden kan fyllas i automatiskt, men backend-logiken är densamma. |
| **Medlemslista** | `GET /v1/households/:id` returnerar medlemmar med `userId`, `displayName`, `role`, `portionFactor`, `joinedAt`. | **Finns** | Återanvänd. Lägg till UI för att visa/ändra. |
| **Ändra roll** | `PATCH /v1/households/:id/members/:userId` med `role`. Endast `owner` får ändra roller. | **Finns** | Återanvänd. Säkerställ att `child` inte kan upphöjas till `owner`. |
| **Ta bort medlem** | `DELETE /v1/households/:id/members/:userId`. Endast `owner` kan ta bort andra; själv kan lämna. | **Finns** | Återanvänd. GDPR: avgöra om matdata ska behållas eller anonymiseras (se avsnitt 4). |
| **Portionsfaktor** | `household_members.portion_factor` per medlem, default 1, validerat 0.13. | **Finns** | Återanvänd. Exponera i mobil-UI för ägare/vuxna. |
| **Delade vs privata data** | Lager, plan, inköpslista, matlådor, budget är hushållsdelat. Mål, allergier, smakprofil, måltidshistorik är individuellt (§7/§56). | **Finns** | Fortsätt. Inga ändringar. |
| **Länkinbjudan / djuplänk** | Saknas. Idag måste användaren manuellt skriva in `inviteCode`. | **Saknas** | Inför tidsbunden inbjudnings-token (`household_invite_links`) med djuplänk via `brand.config.json` `urlScheme`. |
| **QR-inbjudan** | Saknas. | **Saknas** | Generera QR-kod i mobilen som kodar samma djuplänk. Ingen ny backend-modell. |
| **Återkalla inbjudan** | Saknas. När en kod/länk delats finns den kvar tills hushållet byter kod. | **Saknas** | Lägg till `revoked_at` på inbjudningslänk och/eller `invite_code_expires_at` på hushållet. |
| **Anti-dubblettkartan: inga parallella invite- eller medlemsmodeller** | `households.invite_code` + `household_members` är navet. | **OK** | Behåll. Alla inbjudningsvägar ska peka mot samma `households.invite_code`. |
| **Händelsespårning** | `HOUSEHOLD_MEMBER_ADDED` finns i `EVENT_TYPES`. | **Finns** | Lägg till `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
| **Analytics** | Analytics-events är consent-gatade via `trackProductAnalytics`. | **Finns** | Lägg till `household_member_added`, `household_member_removed`, `household_invite_link_used` (inga PII, inga fritext-fält). |
| **Notifieringar till medlemmar** | `notifications`-tabell + push_tokens finns. | **Delvis** | Återanvänd. Skicka push när någon går med eller lämnar (opt-in via notis-samtycke). |
| **Plan-gränser** | `loadEntitlements(app.db, owner.userId)` ger `maxHouseholdMembers`. | **Finns** | Återanvänd vid både kod- och länkinbjudan. |
| §7-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Hushållsgrundstruktur** | Tabellerna `households` och `household_members` finns. `households.invite_code` är unik. Roller: `owner`, `adult`, `member`, `child`. | **Finns** | Återanvänd oförändrat. Utöka med tidsbunden inbjudanslänk, inte ny inbjudningsmodell. |
| **Gå med via kod** | `POST /v1/households/join` med `{ inviteCode }`. Nya medlemmar får roll `adult`. Plan-gräns `maxHouseholdMembers` kontrolleras. | **Finns** | Återanvänd. Utöka med djuplänk + QR så att koden kan fyllas i automatiskt, men backend-logiken är densamma. |
| **Medlemslista** | `GET /v1/households/:id` returnerar medlemmar med `userId`, `displayName`, `role`, `portionFactor`, `joinedAt`. | **Finns** | Återanvänd. Lägg till UI för att visa/ändra. |
| **Ändra roll** | `PATCH /v1/households/:id/members/:userId` med `role`. Endast `owner` får ändra roller. | **Finns** | Återanvänd. Säkerställ att `child` inte kan upphöjas till `owner`. |
| **Ta bort medlem** | `DELETE /v1/households/:id/members/:userId`. Endast `owner` kan ta bort andra; själv kan lämna. | **Finns** | Återanvänd. GDPR: avgöra om matdata ska behållas eller anonymiseras (se avsnitt 4). |
| **Portionsfaktor** | `household_members.portion_factor` per medlem, default 1, validerat 0.13. | **Finns** | Återanvänd. Exponera i mobil-UI för ägare/vuxna. |
| **Delade vs privata data** | Lager, plan, inköpslista, matlådor, budget är hushållsdelat. Mål, allergier, smakprofil, måltidshistorik är individuellt (§7/§56). | **Finns** | Fortsätt. Inga ändringar. |
| **Länkinbjudan / djuplänk** | Saknas. Idag måste användaren manuellt skriva in `inviteCode`. | **Saknas** | Inför tidsbunden inbjudnings-token (`household_invite_links`) med djuplänk via `brand.config.json` `urlScheme`. |
| **QR-inbjudan** | Saknas. | **Saknas** | Generera QR-kod i mobilen som kodar samma djuplänk. Ingen ny backend-modell. |
| **Återkalla inbjudan** | Saknas. När en kod/länk delats finns den kvar tills hushållet byter kod. | **Saknas** | Lägg till `revoked_at` på inbjudningslänk och/eller `invite_code_expires_at` på hushållet. |
| **Anti-dubblettkartan: inga parallella invite- eller medlemsmodeller** | `households.invite_code` + `household_members` är navet. | **OK** | Behåll. Alla inbjudningsvägar ska peka mot samma `households.invite_code`. |
| **Händelsespårning** | `HOUSEHOLD_MEMBER_ADDED` finns i `EVENT_TYPES`. | **Finns** | Lägg till `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
| **Analytics** | Analytics-events är consent-gatade via `trackProductAnalytics`. | **Finns** | Lägg till `household_member_added`, `household_member_removed`, `household_invite_link_used` (inga PII, inga fritext-fält). |
| **Notifieringar till medlemmar** | `notifications`-tabell + push_tokens finns. | **Delvis** | Återanvänd. Skicka push när någon går med eller lämnar (opt-in via notis-samtycke). |
| **Plan-gränser** | `loadEntitlements(app.db, owner.userId)` ger `maxHouseholdMembers`. | **Finns** | Återanvänd vid både kod- och länkinbjudan. |
### Slutsats av matrisen
Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
- **Kärnan** (`households`, `household_members`, `invite_code`, roller, plan-gränser) finns redan.
- **Det som saknas** är (a) djuplänk/QR för smidigare inbjudan, (b) tidsstyrning/återkallning av inbjudningar, (c) medlemskaps-händelser och analytics, (d) GDPR-klar hantering när medlem lämnar.
@@ -46,7 +49,9 @@ Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
## 2. Anti-dubblettkartan varför INGA nya parallella modeller
### 2.1 Varför inte en separat `household_invites`-tabell med egen kod?
`households.invite_code` är redan unik och fungerar. Att skapa en separat tabell med egna koder skulle ge:
- Dubbla källor på sanning: vilken kod gäller?
- Dubbla join-endpoints och valideringar.
- Risk att gamla och nya inbjudningar krockar.
@@ -54,13 +59,16 @@ Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
**Beslut:** Använd `households.invite_code` som kanonisk permanent kod (multi-use fallback). En separat `household_invite_links`-tabell får endast lagra **metadata om länken**: `tokenHash`, `createdBy`, `expiresAt`, `revokedAt`, `usedAt`. Själva inbjudningskoden hämtas alltid från `households.invite_code` när länken löses in.
### 2.2 Varför inte en separat `household_member_history`-tabell?
Medlemskapet är PK på `(household_id, user_id)`. Aktuellt medlemskap finns i `household_members`. Om vi behöver historik (vem var medlem när?) kan vi använda:
- `audit_logs` (som redan loggar `household.member_removed`)
- `domain_events` (`HOUSEHOLD_MEMBER_ADDED`, `HOUSEHOLD_MEMBER_REMOVED`)
Ingen ytterligare tabell behövs i Fas 4.
### 2.3 Varför inte en separat `household_permissions`-tabell?
Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för Fas 4. Rollbaserade behörigheter hårdkodas i routes (som idag). Om vi senare behöver finare rättigheter kan vi migrera då.
---
@@ -68,9 +76,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
## 3. STEGPLAN
### Steg 4a Djuplänk/QR för inbjudan
**Mål:** En användare kan trycka på en länk/QR och automatiskt hamna i appen med inbjudningskoden ifylld.
**Ändringar:**
1. Ny tabell `household_invite_links`:
- `id` uuid PK
- `householdId` FK → households
@@ -98,6 +108,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
6. `DELETE /v1/households/:id/invite-links/:linkId` (owner/adult): återkalla (sätt `revokedAt`).
**Berörda filer:**
- `packages/database/src/schema/households.ts` (`household_invite_links`)
- `packages/validation/src/household.ts` (nya schemas)
- `apps/api/src/routes/households.ts`
@@ -110,6 +121,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
**Migration:** `0019_household_invite_links.sql`
**Testplan:**
- Skapa länk → token returneras EN gång.
- Länk med giltig token → användare blir medlem med roll `adult`.
- Utgången länk → 410 Gone med i18n-nyckel `household.inviteExpired`.
@@ -122,9 +134,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
---
### Steg 4b Medlemshantering i mobilen
**Mål:** Ägare/vuxna kan se medlemmar, ändra roll, ta bort medlem; alla kan lämna hushållet.
**Ändringar:**
1. Uppdatera `apps/mobile/src/app/household.tsx`:
- Visa medlemmar med roll och portionsfaktor.
- Ägare kan ändra roll och ta bort (utom sig själv om enda ägaren).
@@ -135,12 +149,14 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
- `DELETE /v1/households/:id/members/:userId`
**Berörda filer:**
- `apps/mobile/src/app/household.tsx`
- `apps/mobile/src/locales/*/common.json` (+12)
**Migration:** ingen.
**Testplan:**
- Ägare ändrar `member``adult`.
- Ägare tar bort medlem → medlem försvinner.
- Enda ägaren kan inte ta bort sig själv (409).
@@ -149,9 +165,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
---
### Steg 4c Händelser, analytics och notiser
**Mål:** Hushållsförändringar syns i domain_events, analytics och (opt-in) push-notiser.
**Ändringar:**
1. Lägg till i `EVENT_TYPES`:
- `HOUSEHOLD_MEMBER_REMOVED`
- `HOUSEHOLD_MEMBER_ROLE_CHANGED`
@@ -169,6 +187,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
4. Push-notis (opt-in via notis-samtycke) till övriga medlemmar när någon går med eller lämnar.
**Berörda filer:**
- `packages/shared-types/src/enums.ts`
- `packages/events/src/index.ts`
- `packages/analytics/src/builders.ts`
@@ -178,6 +197,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
**Migration:** `0020_expand_event_types_household.sql`
**Testplan:**
- Efter join: `HOUSEHOLD_MEMBER_ADDED` + analytics.
- Efter borttag: `HOUSEHOLD_MEMBER_REMOVED` + audit-log.
- Efter rolländring: `HOUSEHOLD_MEMBER_ROLE_CHANGED`.
@@ -187,30 +207,30 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
## 4. RISKER
| Risk | Påverkan | Minskning |
|------|----------|-----------|
| **GDPR: data när medlem lämnar** | Hög | Att lämna/tas bort ur ett hushåll är **inte kontoradering**. Vid borttag av `household_members`-raden tas endast **åtkomsten** bort. Personens `meals`, `taste_signals`, `memory_items`, `user_health_profiles` och `push_tokens` tillhör kontot och rörs inte — användaren kan gå med i ett annat hushåll. Hushållets historiska aggregat (budget, svinn, gemensamma matlådor, inventory-transaktioner) ändras inte retroaktivt. Radering av persondata sker endast via den befintliga kontoraderingsprocessen (`DELETE /v1/me`, dokumenterad i docs/12). Beslutet skrivs in explicit innan 4b kodas. |
| **Behörigheter: vem får bjuda in/ta bort** | Hög | Bjud in: `owner` och `adult`. Ta bort andra: endast `owner`. Ta bort sig själv: alla utom enda ägaren. Ändra roller: endast `owner`. `child` får aldrig bjuda in eller ta bort. |
| **Inbjudningslänkar läcker** | Medel | Token är slumpmässig ≥128 bitar, tidsbegränsad, engångsbruk. Servern lagrar endast sha256(token). Återkallning sätts omedelbart. Inga PII i länken. |
| **Parallella inbjudningsmodeller** | Hög | Enda kanoniska koden är `households.invite_code`. `household_invite_links` är endast metadata. `/v1/households/join` och `/v1/households/join-link` konvergerar till samma join-kärna. |
| **Plan-gränser kringgås** | Medel | Både kod- och länkinbjudan kontrollerar `maxHouseholdMembers` mot ägarens entitlements före insert. |
| **i18n-paritet fallerar** | Låg | Alla nya nycklar läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`. |
| **Djuplänk hårdkodar app-namn** | Medel | `urlScheme` läses dynamiskt från `brand.config.json`. Ingen hårdkodning av app-namn. |
| **Testdata läcker mellan tester** | Låg | Hermetiska tester skapar egna hushåll per test; `invite_code` genereras unikt. |
| Risk | Påverkan | Minskning |
| ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **GDPR: data när medlem lämnar** | Hög | Att lämna/tas bort ur ett hushåll är **inte kontoradering**. Vid borttag av `household_members`-raden tas endast **åtkomsten** bort. Personens `meals`, `taste_signals`, `memory_items`, `user_health_profiles` och `push_tokens` tillhör kontot och rörs inte — användaren kan gå med i ett annat hushåll. Hushållets historiska aggregat (budget, svinn, gemensamma matlådor, inventory-transaktioner) ändras inte retroaktivt. Radering av persondata sker endast via den befintliga kontoraderingsprocessen (`DELETE /v1/me`, dokumenterad i docs/12). Beslutet skrivs in explicit innan 4b kodas. |
| **Behörigheter: vem får bjuda in/ta bort** | Hög | Bjud in: `owner` och `adult`. Ta bort andra: endast `owner`. Ta bort sig själv: alla utom enda ägaren. Ändra roller: endast `owner`. `child` får aldrig bjuda in eller ta bort. |
| **Inbjudningslänkar läcker** | Medel | Token är slumpmässig ≥128 bitar, tidsbegränsad, engångsbruk. Servern lagrar endast sha256(token). Återkallning sätts omedelbart. Inga PII i länken. |
| **Parallella inbjudningsmodeller** | Hög | Enda kanoniska koden är `households.invite_code`. `household_invite_links` är endast metadata. `/v1/households/join` och `/v1/households/join-link` konvergerar till samma join-kärna. |
| **Plan-gränser kringgås** | Medel | Både kod- och länkinbjudan kontrollerar `maxHouseholdMembers` mot ägarens entitlements före insert. |
| **i18n-paritet fallerar** | Låg | Alla nya nycklar läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`. |
| **Djuplänk hårdkodar app-namn** | Medel | `urlScheme` läses dynamiskt från `brand.config.json`. Ingen hårdkodning av app-namn. |
| **Testdata läcker mellan tester** | Låg | Hermetiska tester skapar egna hushåll per test; `invite_code` genereras unikt. |
---
## 5. Översiktliga berörda tabeller
| Tabell | Förändring |
|--------|------------|
| `households` | Ingen schemaändring. `invite_code` fortsätter vara kanonisk. |
| `household_members` | Ingen schemaändring. Befintlig roll- och delete-logik återanvänds. |
| `household_invite_links` | **Ny** (metadata om djuplänkar). |
| `users` | Ingen ändring, men vid kontoradering används befintligt GDPR-flöde (docs/12). |
| `domain_events` | Nya event-typer: `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
| `audit_logs` | Återanvänds för `household.member_removed` (finns redan). |
| `notifications` / `push_tokens` | Återanvänds för opt-in-notiser. |
| Tabell | Förändring |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `households` | Ingen schemaändring. `invite_code` fortsätter vara kanonisk. |
| `household_members` | Ingen schemaändring. Befintlig roll- och delete-logik återanvänds. |
| `household_invite_links` | **Ny** (metadata om djuplänkar). |
| `users` | Ingen ändring, men vid kontoradering används befintligt GDPR-flöde (docs/12). |
| `domain_events` | Nya event-typer: `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
| `audit_logs` | Återanvänds för `household.member_removed` (finns redan). |
| `notifications` / `push_tokens` | Återanvänds för opt-in-notiser. |
---
@@ -227,6 +247,7 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
## 7. Länk-invites detaljerade regler
### 7.1 Token och djuplänk
- Token: slumpmässig ≥128 bitar, base64url → 22 tecken. Skickas endast till klienten vid skapande.
- Servern lagrar **sha256(token)** i `household_invite_links.tokenHash`; uppslag vid inbjudan görs via hash.
- Djuplänk: `<urlScheme>://households/join?token=<token>` där `urlScheme` kommer från `brand.config.json`.
@@ -238,9 +259,11 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
- **Owner-transfer ingår inte i Fas 4.**
### 7.2 Ingen PII i länken
Länken innehåller endast token. Inget hushållsnamn, ingen e-post, inga medlemsnamn. Den som har länken kan gå med, därför ska användare dela den via säkra kanaler.
### 7.3 Token-säkerhet och användningsregler
- Token genereras slumpmässigt med ≥128 bitar.
- Servern lagrar **endast sha256(token)** i `tokenHash`; klartext-token returneras bara vid skapande.
- Länkar är **single-use by design**: `usedAt` sätts vid lyckad join.
@@ -250,7 +273,9 @@ Länken innehåller endast token. Inget hushållsnamn, ingen e-post, inga medlem
- **Owner-transfer ingår inte i Fas 4.**
### 7.4 Konvergens med kod-inbjudan
Både `POST /v1/households/join` (kod) och `POST /v1/households/join-link` (token) ska i slutändan anropa samma interna `joinHouseholdCore(db, householdId, userId, role)` som:
- kontrollerar plan-gräns,
- sätter in `household_members` med angiven roll (`adult` för länk),
- emittar `HOUSEHOLD_MEMBER_ADDED`,
@@ -258,12 +283,15 @@ Både `POST /v1/households/join` (kod) och `POST /v1/households/join-link` (toke
- skickar notis till övriga medlemmar (opt-in).
### 7.5 i18n
Nya nycklar för 4a:
- `household.inviteLink`, `household.inviteLinkShare`, `household.inviteLinkCopied`
- `household.inviteExpired`, `household.inviteRevoked`, `household.inviteUsed`
- `household.qrScanHint`
Nya nycklar för 4b:
- `household.leaveConfirmTitle`, `household.leaveConfirmBody`
- `household.removeMemberConfirmTitle`, `household.removeMemberConfirmBody`
- `household.changeRoleConfirmTitle`
@@ -271,6 +299,7 @@ Nya nycklar för 4b:
Alla läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`.
### 7.6 Analytics
- `household_member_added`: `{ household_id_hash, role, via: 'code' | 'link' }` — hashat hushålls-id, inga personuppgifter.
- `household_member_removed`: `{ household_id_hash }`.
- `household_invite_link_used`: `{ household_id_hash, link_id }`**aldrig token eller invite_code**.
@@ -278,8 +307,9 @@ Alla läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`
Alla går via `trackProductAnalytics` och är därmed consent-gatade.
### 7.7 Webbfallback
Om mottagaren inte har appen installerad och trycker på länken i en webbläsare krävs en associerad domän + universal link / app link. Detta är ett **fas-1-spår** som ligger utanför Fas 4. I Fas 4 förutsätts att mottagaren har Expo Go/appen installerad.
---
*Rapport färdig. Inväntar godkännande innan kod påbörjas.*
_Rapport färdig. Inväntar godkännande innan kod påbörjas._
+22 -22
View File
@@ -17,16 +17,16 @@ Planfil: infrastructure/terraform/cibello.plan
### Huvudkomponenter
| Komponent | Beskrivning |
|-----------|-------------|
| VPC | `cibello-prod-vpc` 10.0.0.0/16, 1 publikt + 2 privata subnät (eu-north-1a/1b) |
| Compute | EC2 `t3.small` Ubuntu 22.04, gp3 30 GB, EIP, docker/compose/git bootstrap |
| Databas | RDS PostgreSQL 16 `db.t4g.micro`, krypterad, 7 dagars backup, deletion protection |
| Lagring | S3 `cibello-production` (lifecycle temporary/7d, scans/90d) + `cibello-web` (CloudFront OAC) |
| IAM | `cibello-app`-roll + instansprofil, least-privilege S3/SSM/KMS + SSM Core |
| Secrets | 4 genererade SSM SecureString + 6 tomma platshållare under `/cibello/prod/*` |
| DNS/CDN | Route53 zon `cibello.app`, ACM-cert (us-east-1), CloudFront-distribution |
| Mail | SES identitet `mail.cibello.app` + DKIM + config set `cibello-prod` |
| Komponent | Beskrivning |
| --------- | -------------------------------------------------------------------------------------------- |
| VPC | `cibello-prod-vpc` 10.0.0.0/16, 1 publikt + 2 privata subnät (eu-north-1a/1b) |
| Compute | EC2 `t3.small` Ubuntu 22.04, gp3 30 GB, EIP, docker/compose/git bootstrap |
| Databas | RDS PostgreSQL 16 `db.t4g.micro`, krypterad, 7 dagars backup, deletion protection |
| Lagring | S3 `cibello-production` (lifecycle temporary/7d, scans/90d) + `cibello-web` (CloudFront OAC) |
| IAM | `cibello-app`-roll + instansprofil, least-privilege S3/SSM/KMS + SSM Core |
| Secrets | 4 genererade SSM SecureString + 6 tomma platshållare under `/cibello/prod/*` |
| DNS/CDN | Route53 zon `cibello.app`, ACM-cert (us-east-1), CloudFront-distribution |
| Mail | SES identitet `mail.cibello.app` + DKIM + config set `cibello-prod` |
### DNS-poster planerade
@@ -36,18 +36,18 @@ Planfil: infrastructure/terraform/cibello.plan
## Grov månadskostnad (uppskattning)
| Post | Uppskattad kostnad |
|------|-------------------|
| EC2 t3.small (on-demand) | ~$15/mån |
| RDS db.t4g.micro + 20 GB gp3 | ~$12/mån |
| EBS 30 GB gp3 (EC2) | ~$3.20/mån |
| S3 (lagring + förfrågningar) | ~$110/mån beroende på volym |
| CloudFront (dataöverföring) | ~$020/mån beroende på trafik |
| Route53 hosted zone | $0.50/mån |
| ACM-certifikat | $0 |
| SES (första 3 000 mail/mån inom gratisnivån, därefter ~$0.10/1 000) | ~$01/mån |
| Dataöverföring ut | tillkommer vid användning |
| **Totalt** | **~$3262/mån + dataöverföring** |
| Post | Uppskattad kostnad |
| ------------------------------------------------------------------- | -------------------------------- |
| EC2 t3.small (on-demand) | ~$15/mån |
| RDS db.t4g.micro + 20 GB gp3 | ~$12/mån |
| EBS 30 GB gp3 (EC2) | ~$3.20/mån |
| S3 (lagring + förfrågningar) | ~$110/mån beroende på volym |
| CloudFront (dataöverföring) | ~$020/mån beroende på trafik |
| Route53 hosted zone | $0.50/mån |
| ACM-certifikat | $0 |
| SES (första 3 000 mail/mån inom gratisnivån, därefter ~$0.10/1 000) | ~$01/mån |
| Dataöverföring ut | tillkommer vid användning |
| **Totalt** | **~$3262/mån + dataöverföring** |
## Domänkostnad
+15 -15
View File
@@ -8,21 +8,21 @@
## Vad som är grönt ✅
| Kontroll | Resultat |
|----------|----------|
| Branch / repo-kontroll | HEAD stämmer, trädet rent |
| `pnpm typecheck` | 19/19 paket gröna |
| `pnpm test --force` | 34/34 tasks, 328 tester gröna (kört 2× på färsk DB) |
| `pnpm build` | `@app/admin`, `@app/api`, `@app/worker` byggda |
| `brand-guard.sh` | OK varumärket lever endast i `brand.config.json` |
| Databas | `cibello_test` färskt skapad med `DROP DATABASE + CREATE DATABASE … OWNER app_user`, migrerad + seedad med 254 recept/102 ingredienser/76 tabeller |
| Allergen-invariant | `@app/recipe-generation/test/allergen-invariant.test.ts` grön varje seedat recept har `allergens` som matchar färsk derivering från `canonical_ingredients` |
| Safety canary | Worker-testet grön; canary-kontraktet är `{allergen_invariant_brott, overifierade_visade, food_safety_lint_avvisade_7d}` |
| GDPR-radering | `DELETE /v1/me` tömmer `memory_items`, `taste_signals`, `feedback`, `ai_corrections`, `ai_training_bank`, `scan_jobs` och tillhörande bilder (residual-test grön) |
| i18n-paritet ×12 | Tester för locale-preferenser, minnesrendering och enhetsetiketter gröna |
| Consent-grindar | Ingen personalisering/impact utan `personalization=granted` (recommendations + memory-impact tester gröna) |
| Integritetsspärrar | Impact för användare A returnerar aldrig användare B:s data |
| Ops-kontraktet | `/ops/v1/summary` har 14 toppnycklar + `wall` med 6 tiles; `/ops/v1/health` svarar `{"ok":true,"app":"cibello"}`; tester gröna |
| Kontroll | Resultat |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Branch / repo-kontroll | HEAD stämmer, trädet rent |
| `pnpm typecheck` | 19/19 paket gröna |
| `pnpm test --force` | 34/34 tasks, 328 tester gröna (kört 2× på färsk DB) |
| `pnpm build` | `@app/admin`, `@app/api`, `@app/worker` byggda |
| `brand-guard.sh` | OK varumärket lever endast i `brand.config.json` |
| Databas | `cibello_test` färskt skapad med `DROP DATABASE + CREATE DATABASE … OWNER app_user`, migrerad + seedad med 254 recept/102 ingredienser/76 tabeller |
| Allergen-invariant | `@app/recipe-generation/test/allergen-invariant.test.ts` grön varje seedat recept har `allergens` som matchar färsk derivering från `canonical_ingredients` |
| Safety canary | Worker-testet grön; canary-kontraktet är `{allergen_invariant_brott, overifierade_visade, food_safety_lint_avvisade_7d}` |
| GDPR-radering | `DELETE /v1/me` tömmer `memory_items`, `taste_signals`, `feedback`, `ai_corrections`, `ai_training_bank`, `scan_jobs` och tillhörande bilder (residual-test grön) |
| i18n-paritet ×12 | Tester för locale-preferenser, minnesrendering och enhetsetiketter gröna |
| Consent-grindar | Ingen personalisering/impact utan `personalization=granted` (recommendations + memory-impact tester gröna) |
| Integritetsspärrar | Impact för användare A returnerar aldrig användare B:s data |
| Ops-kontraktet | `/ops/v1/summary` har 14 toppnycklar + `wall` med 6 tiles; `/ops/v1/health` svarar `{"ok":true,"app":"cibello"}`; tester gröna |
---
+2
View File
@@ -19,6 +19,7 @@ DEPLOY_MODE=staging ./infrastructure/deployment/first-deploy.sh
```
När skriptet är klart kontrollerar du:
- API health: `curl -fsS http://192.168.8.123:4000/healthz`
- `API_BASE_URL` som skrivs ut måste vara `http://192.168.8.123:4000` för LAN-telefontest.
@@ -60,6 +61,7 @@ Verifiera utdata:
3. Testa fotoflödet: skapa → ladda upp bild → granska.
Om "Network request failed" uppstår:
- Kontrollera att datorn och telefonen är på samma Wi-Fi.
- Kontrollera att `API_BASE_URL` i `.env.telefontest` är datorns LAN-IP.
- Kontrollera att Expo-loggen visar `exp://192.168.8.123:8081`.
+11 -11
View File
@@ -6,17 +6,17 @@ Inga handkopior får drifta.
## SSM-parameter → env-var
| SSM-parameter | Miljövariabel | Kommentar |
|---------------|---------------|-----------|
| `/cibello/prod/aamos-api-key` | `AAMOS_API_KEY` | Endast när `AAMOS_MODE=http` |
| `/cibello/prod/db-password` | byggd in i `DATABASE_URL` / `TEST_DATABASE_URL` | RDS-användare `cibello_admin` |
| `/cibello/prod/entitlement-signing-secret` | `ENTITLEMENT_SIGNING_SECRET` | Signerar prenumerationsrättigheter |
| `/cibello/prod/eoc-push-token` | `EOC_PUSH_TOKEN` | Ops-summary push till EOC |
| `/cibello/prod/gemini-api-key` | `GEMINI_API_KEY` | AAMOS_MODE=gemini |
| `/cibello/prod/google-service-account-json` | `GOOGLE_SERVICE_ACCOUNT_JSON` | Google Play service account (JSON) |
| `/cibello/prod/jwt-access-secret` | `JWT_ACCESS_SECRET` | Access-token-signering |
| `/cibello/prod/jwt-refresh-secret` | `JWT_REFRESH_SECRET` | Refresh-token-signering |
| `/cibello/prod/smtp-pass` | `SMTP_PASS` | SMTP-lösen för `hello@cibello.app` |
| SSM-parameter | Miljövariabel | Kommentar |
| ------------------------------------------- | ----------------------------------------------- | ---------------------------------- |
| `/cibello/prod/aamos-api-key` | `AAMOS_API_KEY` | Endast när `AAMOS_MODE=http` |
| `/cibello/prod/db-password` | byggd in i `DATABASE_URL` / `TEST_DATABASE_URL` | RDS-användare `cibello_admin` |
| `/cibello/prod/entitlement-signing-secret` | `ENTITLEMENT_SIGNING_SECRET` | Signerar prenumerationsrättigheter |
| `/cibello/prod/eoc-push-token` | `EOC_PUSH_TOKEN` | Ops-summary push till EOC |
| `/cibello/prod/gemini-api-key` | `GEMINI_API_KEY` | AAMOS_MODE=gemini |
| `/cibello/prod/google-service-account-json` | `GOOGLE_SERVICE_ACCOUNT_JSON` | Google Play service account (JSON) |
| `/cibello/prod/jwt-access-secret` | `JWT_ACCESS_SECRET` | Access-token-signering |
| `/cibello/prod/jwt-refresh-secret` | `JWT_REFRESH_SECRET` | Refresh-token-signering |
| `/cibello/prod/smtp-pass` | `SMTP_PASS` | SMTP-lösen för `hello@cibello.app` |
**Föråldrade / dubbla parametrar** (rensas vid tillfälle): `/cibello/prod/entitlement-signing`, `/cibello/prod/jwt-access`, `/cibello/prod/jwt-refresh`.
+7 -7
View File
@@ -5,13 +5,13 @@
## Verifierat grönt (senaste körning)
| Kontroll | Resultat |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm typecheck` | 19/19 paket |
| `pnpm test` | 34 tasks, 326 tester deterministic: full svit körd två gånger mot nyskapad Postgres 16 utan cache, grönt båda körningarna |
| `pnpm build` | api + worker + admin |
| `pnpm db:migrate && pnpm db:seed` | 76 tabeller, 254 verifierade recept, kanoniska ingredienser, enhetsetiketter, marknadsprofiler, ops/EOC-integrering |
| E2E-smoke | register → hushåll (valuta) → lager (kanoniska enheter) → rekommendationer → AI-översättning → publicering → engelsk användare får engelskt innehåll → accentokänslig sök → marknadsprofiler |
| Kontroll | Resultat |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm typecheck` | 19/19 paket |
| `pnpm test` | 34 tasks, 326 tester deterministic: full svit körd två gånger mot nyskapad Postgres 16 utan cache, grönt båda körningarna |
| `pnpm build` | api + worker + admin |
| `pnpm db:migrate && pnpm db:seed` | 76 tabeller, 254 verifierade recept, kanoniska ingredienser, enhetsetiketter, marknadsprofiler, ops/EOC-integrering |
| E2E-smoke | register → hushåll (valuta) → lager (kanoniska enheter) → rekommendationer → AI-översättning → publicering → engelsk användare får engelskt innehåll → accentokänslig sök → marknadsprofiler |
## Färdigbyggt