* feat(api): remplace la détection des tech steps par un pipeline NLP (node-nlp) Le matching par regex ne généralisait jamais au-delà de son propre vocabulaire — une étape décrivant la fonte du beurre comme "jusqu'à ce que le beurre ait disparu dans la poêle" ne contient aucun verbe sur lequel une regex pourrait s'ancrer, alors que le sens est sans ambiguïté. Nouveau pipeline en 3 étapes (TechStepClassifierService, node-nlp 4.27.0 — la 5.x est encore alpha, non retenue) : 1. NER (entités enum) trouve les mentions candidates + leur position exacte, à partir de listes de synonymes (tech-step-training-data.ts) plutôt que de regex écrites à la main. ner.threshold: 1 (exact, après normalisation) — le défaut à 0.8 faisait matcher "faire" (verbe auxiliaire omniprésent) contre "frire" par pure proximité de chaîne. 2. La description est découpée en clauses autour de ces candidats (splitIntoClauses, pure/testable sans modèle). 3. Le NlpManager classe chaque clause individuellement, entraîné sur des phrases qui n'emploient jamais le verbe de la technique — c'est ce qui apporte la compréhension du sens. En dessous de CONFIDENCE_THRESHOLD (0.65, ajusté empiriquement), retombe sur la technique impliquée par l'ancre NER plutôt que d'abandonner un match clairement ancré sur un mot-clé. TechStepMapping (table de regex par technique/locale) supprimée — migration 20260821130000_drop_tech_step_mapping — plus aucune table n'est interrogée à l'exécution, les données de matching vivent en code. TECH_STEPS (reference-seed-data.ts) simplifié en simple liste de uid, les mappings ayant disparu. Deux pièges trouvés en construisant ce pipeline, corrigés à la source : - db/prisma.ts construisait PrismaClient sans importer config/env.ts — un run de test isolé pouvait faire gagner la course au .env interne de Prisma (dev) contre .env.test. Fixé en important config/env.js en tout premier, pour effet de bord. - NlpManager a autoSave/autoLoad: true par défaut — persiste le modèle entraîné dans model.nlp et le recharge au lieu de ré-entraîner au prochain démarrage. Les deux désactivés explicitement (sinon un modèle obsolète masquerait silencieusement toute mise à jour du corpus/seuil) ; model.nlp ajouté au .gitignore en garde-fou. apps/api/src/db/prisma.ts, recipe.service.ts, sources.service.ts et recipe-translation.ts adaptés à la matching async (le classifieur entraîné remplace le couple loadTechStepMappingRules+matchTechStepSpans synchrone) ; server.ts appelle techStepClassifier.warmUp() avant d'accepter du trafic (le tout premier appel réel à NlpManager.process() charge les ressources par langue de node-nlp, plusieurs secondes). Vérifié : tsc --noEmit, biome check (0 erreur), build complet des 6 packages, 308 tests API (dont un test-support/reset-db.ts corrigé — référençait encore tech_step_mapping dans son TRUNCATE). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(api): ajoute la délimitation de contexte aux tech steps et étoffe le vocabulaire du classifieur Deux évolutions du pipeline NLP de détection des tech steps (PR #63) : 1. Délimitation de contexte — en plus du mot-clé qui déclenche un match (start/end), chaque TechStepMatch porte maintenant contextStart/ contextEnd : la clause complète autour du mot-clé (ex : "poêle chaude" comme mot-clé, "Dans une poêle chaude" comme contexte). Persisté sur StepTechStep (colonnes nullables, migration dédiée), exposé via StepTechStepView, et rendu côté web avec un style plus discret que le mot-clé (StepDescription.tsx, .step-tech-step-context). splitIntoClauses coupe désormais sur l'espace le plus proche du milieu de l'écart entre deux candidats plutôt que sur le milieu brut, pour ne jamais couper un mot en deux (findGapSplitPoint). 2. Vocabulaire du classifieur — synonymes et locutions supplémentaires par technique (FR/EN) pour fiabiliser la détection sur des formulations que le corpus initial ne couvrait pas. Plusieurs bugs de fond trouvés et corrigés en cours de route, tous confirmés par la suite de tests complète (309 tests) : - un synonyme multi-mots qui est un préfixe-mot d'un synonyme plus court déjà enregistré pour la même technique fait matcher les deux comme candidats NER distincts et chevauchants, corrompant le découpage en clauses (parfois jusqu'à une mauvaise classification) — retiré partout où ce motif a été repéré (cook, fry, deglaze, simmer, boil, roast, chop, mince, marinate, preheat, bake, plate, coat) ; - "poêlé"/"poêlée" comme synonymes de panFry sont réduits à la même racine que le nom "poêle" par le stemmer français de node-nlp, provoquant un faux positif sur toute mention nue de "poêle" (dont celle de preheat) — retiré ; - "Fouetter les blancs en neige" était mal classé en foldIn (la phrase d'entraînement de foldIn partage la même locution) — corrigé en ajoutant des phrases d'entraînement dédiées à whisk ; - "Émincer les tomates" est passé sous le seuil de confiance vers melt après l'ajout du nouveau vocabulaire ailleurs dans le corpus — corrigé en élargissant les phrases d'entraînement de mince à un autre légume. Le test unitaire de splitIntoClauses avec un point de coupure obsolète (pré-datant findGapSplitPoint) est aussi corrigé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(api): corrige la découpe des clauses et le seuil de confiance du classifieur de tech steps Trouvé en examinant des vraies recettes déjà en base après le dernier étoffement du vocabulaire : plusieurs étapes bien réelles se faisaient classer sur la mauvaise technique, sans lien avec un mot-clé manquant. - splitIntoClauses coupe désormais sur la limite de phrase (juste après un ".", "!" ou "?") la plus proche du milieu de l'écart entre deux candidats quand il y en a une, plutôt que sur l'espace brut le plus proche du milieu. Une description à deux techniques dans deux phrases distinctes ("Préchauffer le four à 180°C. Dans un saladier, mettre le beurre... et mélanger.") ne coupait qu'au milieu brut, ce qui pouvait trancher en pleine deuxième phrase et envoyer au classifieur une clause tronquée ("...(thermostat 6). Dans un saladier, mettre" sans complément) — assez éloignée des phrases d'entraînement courtes et complètes pour se faire mal classer avec confiance (préchauffer prédit "mix", mélanger prédit "melt"). - CONFIDENCE_THRESHOLD passe de 0.65 à 0.75 : du texte anglais passé dans le classifieur français (qui doit ne rien trouver, garanti par le test d'isolation des locales) scorait 0.69 sur "boil" — du bruit de petit corpus, pas un vrai verdict. Les cas réels que ce seuil sert à faire confiance scorent 0.91 à 1.0 en pratique ; 0.75 sépare proprement le bruit du signal sans rien casser (309 tests toujours verts). - Deux phrases d'entraînement ajoutées à `cook` pour deux clauses réelles mal classées (feu doux + remuant, découvert + laisser cuire) qui n'avaient pourtant pas de mot-clé manquant. Ajoute aussi src/scripts/backfill-tech-steps.ts : la détection ne tourne qu'à la création/modification d'une recette, jamais rétroactivement — ce script recalcule le start/end/contextStart/ contextEnd de chaque étape existante contre le classifieur actuel, pour ne pas avoir à rouvrir et resauvegarder chaque recette à la main après un changement de corpus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(web): rend le contexte des tech steps réellement visible Le style existant (fond teinté à 6% d'opacité, sans autre indice visuel) était structurellement correct — vérifié en base, l'API et le DOM contenaient bien les spans de contexte — mais imperceptible à l'œil sur ce thème sombre : --color-primary n'est pas assez saturé pour qu'une teinte de quelques % se distingue du fond de la carte. Vérifié en créant une recette test dans le navigateur et en zoomant le texte rendu : littéralement aucune différence visible avant, un rectangle net après. Passe à 10% de fond + une bordure basse pleine à 45% d'opacité comme second indice visuel indépendant, tout en gardant le mot-clé (soulignement pointillé + fond à 14% + curseur + tooltip) nettement plus marqué que son contexte. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(web): retire l'affichage visuel du contexte des tech steps Ne touche que le rendu — le backend continue de calculer et de persister contextStart/contextEnd (tech-step-matcher.ts, StepTechStep), et splitDescriptionByTechSteps continue de découper la description autour du contexte (segments isKeyword: false). StepDescription.tsx rend désormais ces segments comme du texte brut, comme un segment sans technique — plus d'encadré/bordure autour de la clause, seul le mot-clé reste surligné avec sa tooltip. .step-tech-step-context (CSS) retirée, devenue inutilisée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
419 lines
16 KiB
Markdown
419 lines
16 KiB
Markdown
# Modèle de données — Projet Batch-cooking
|
|
|
|
> Documentation du schéma de données de l'application de planification de batch-cooking.
|
|
> Source de vérité : `apps/api/prisma/schema.prisma` (abondamment commenté en anglais,
|
|
> chaque écart avec ce document ou avec le doc spec d'origine y est expliqué en place —
|
|
> ce fichier en est un résumé navigable, pas un remplacement).
|
|
|
|
---
|
|
|
|
## Vue d'ensemble
|
|
|
|
Le modèle s'articule désormais autour de cinq grands pôles (le pôle « Recettes »
|
|
a beaucoup grossi depuis la première version : sources externes, catalogue
|
|
d'ingrédients/unités normalisé, techniques détectées, visibilité) :
|
|
|
|
- **Utilisateurs & foyer** — `UserProfile`, `House`, `Diet`, `Category`, `Allergy`,
|
|
`UserPreference` (thème)
|
|
- **Planification** — `Planning`, `PlanningItem`
|
|
- **Recettes** — `Recipe`, `RecipeIngredient`, `Step`, `TechStep`,
|
|
`StepTechStep`, `RecipeDiet`, `RecipeFavorite`
|
|
- **Sources externes** — `Source`, `HouseSource`
|
|
- **Catalogue ingrédients/unités** — `Ingredient`, `Unit`, `IngredientDiet`,
|
|
`IngredientAllergy`, `UserProfileDislikedIngredient`
|
|
|
|
---
|
|
|
|
## Schéma entité-relation
|
|
|
|
```mermaid
|
|
erDiagram
|
|
USER_PROFILE }o--o| HOUSE : "membre de"
|
|
HOUSE ||--|| USER_PROFILE : "admin (adminId)"
|
|
USER_PROFILE }o--o| DIET : "suit"
|
|
USER_PROFILE ||--o| USER_PREFERENCE : "thème"
|
|
HOUSE ||--o{ PLANNING : "planifie"
|
|
PLANNING ||--o{ PLANNING_ITEM : "contient"
|
|
PLANNING_ITEM }o--|| RECIPE : "utilise"
|
|
RECIPE }o--o| SOURCE : "vient de"
|
|
RECIPE }o--|| USER_PROFILE : "auteur (authorId)"
|
|
RECIPE }o--o| HOUSE : "foyer auteur (authorHouseId)"
|
|
HOUSE ||--o{ HOUSE_SOURCE : "sources activées"
|
|
SOURCE ||--o{ HOUSE_SOURCE : ""
|
|
CATEGORY ||--o{ ALLERGY : "classe"
|
|
USER_PROFILE }o--o{ ALLERGY : "a"
|
|
USER_PROFILE }o--o{ RECIPE : "favoris"
|
|
USER_PROFILE }o--o{ INGREDIENT : "n'aime pas"
|
|
RECIPE ||--o{ RECIPE_INGREDIENT : "compose de"
|
|
RECIPE_INGREDIENT }o--|| INGREDIENT : ""
|
|
RECIPE_INGREDIENT }o--|| UNIT : ""
|
|
RECIPE }o--o{ DIET : "tags régime (RecipeDiet)"
|
|
INGREDIENT }o--o{ DIET : "compatible avec"
|
|
INGREDIENT }o--o{ ALLERGY : "contient"
|
|
RECIPE ||--o{ STEP : "compose de"
|
|
STEP }o--o{ TECH_STEP : "techniques détectées (StepTechStep)"
|
|
TECH_STEP ||--o{ TECH_STEP_MAPPING : "règles de détection"
|
|
|
|
USER_PROFILE {
|
|
int id PK
|
|
string firstName
|
|
string lastName
|
|
string email UK
|
|
string passwordHash
|
|
int tokenVersion
|
|
int houseId FK
|
|
int dietId FK
|
|
}
|
|
HOUSE {
|
|
int id PK
|
|
string name
|
|
int adminId FK
|
|
string inviteCode UK
|
|
}
|
|
USER_PREFERENCE {
|
|
int userProfileId PK_FK
|
|
enum theme "LIGHT | DARK | SYSTEM"
|
|
}
|
|
DIET {
|
|
int id PK
|
|
string key UK
|
|
}
|
|
ALLERGY {
|
|
int id PK
|
|
int categoryId FK
|
|
}
|
|
CATEGORY {
|
|
int id PK
|
|
string key UK
|
|
enum kind "ALLERGY | INTOLERANCE"
|
|
}
|
|
PLANNING {
|
|
int id PK
|
|
date startDate
|
|
date finishDate
|
|
int houseId FK
|
|
}
|
|
PLANNING_ITEM {
|
|
int id PK
|
|
int planningId FK
|
|
string weekDay
|
|
string meal
|
|
int recipeId FK
|
|
int portions
|
|
}
|
|
SOURCE {
|
|
int id PK
|
|
string key UK
|
|
string name
|
|
string url
|
|
bool official
|
|
string iconUrl
|
|
}
|
|
HOUSE_SOURCE {
|
|
int houseId PK_FK
|
|
int sourceId PK_FK
|
|
}
|
|
RECIPE {
|
|
int id PK
|
|
string name
|
|
int sourceId FK
|
|
string externalId
|
|
string description
|
|
string picture
|
|
int portions
|
|
int authorId FK
|
|
int authorHouseId FK
|
|
enum visibility "PERSONAL | HOUSE | PUBLIC"
|
|
}
|
|
RECIPE_INGREDIENT {
|
|
int recipeId PK_FK
|
|
int ingredientId PK_FK
|
|
decimal quantity
|
|
int unitId FK
|
|
}
|
|
RECIPE_DIET {
|
|
int recipeId PK_FK
|
|
int dietId PK_FK
|
|
}
|
|
RECIPE_FAVORITE {
|
|
int userProfileId PK_FK
|
|
int recipeId PK_FK
|
|
}
|
|
INGREDIENT {
|
|
int id PK
|
|
string key UK
|
|
enum icon
|
|
enum category
|
|
enum subcategory
|
|
bool reproducible
|
|
}
|
|
UNIT {
|
|
int id PK
|
|
string key UK
|
|
enum type "MASS | VOLUME | COUNT"
|
|
decimal toBaseFactor
|
|
}
|
|
STEP {
|
|
int id PK
|
|
int recipeId FK
|
|
string description
|
|
string picture
|
|
int order
|
|
}
|
|
TECH_STEP {
|
|
int id PK
|
|
string key UK
|
|
}
|
|
TECH_STEP_MAPPING {
|
|
int id PK
|
|
int techStepId FK
|
|
string locale
|
|
string expression
|
|
int weight
|
|
}
|
|
STEP_TECH_STEP {
|
|
int stepId PK_FK
|
|
int techStepId PK_FK
|
|
int order PK
|
|
int start
|
|
int end
|
|
}
|
|
```
|
|
|
|
*(Rendu sur les visualiseurs markdown compatibles mermaid — GitHub, VS Code, Obsidian, etc.
|
|
Champ `PK_FK` = à la fois clé primaire et clé étrangère, `UK` = unique.)*
|
|
|
|
---
|
|
|
|
## Utilisateurs & foyer
|
|
|
|
### `user_profiles` (`UserProfile`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `firstName` / `lastName` | Nom |
|
|
| `email` | Unique |
|
|
| `passwordHash` | Hash argon2 du mot de passe |
|
|
| `tokenVersion` | Compteur incrémenté pour invalider les JWT déjà émis (ex. changement de mot de passe) — mécanisme provisionné, aucun code ne l'incrémente encore aujourd'hui (pas de changement de mot de passe implémenté, voir [backend-architecture.md](./backend-architecture.md)) |
|
|
| `houseId` | FK → `house`, nullable |
|
|
| `dietId` | FK → `diet`, nullable |
|
|
|
|
Relations supplémentaires par rapport au schéma d'origine : `dislikedIngredients`
|
|
(m2m vers `Ingredient`, préférence de goût — voir plus bas), `authoredRecipes`,
|
|
`favoriteRecipes`, `administeredHouses` (le foyer dont ce profil est admin, au
|
|
plus un dans les faits), `preferences` (1-1 vers `UserPreference`).
|
|
|
|
### `house` (`House`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `name` | Nom du foyer |
|
|
| `adminId` | FK → `user_profiles` — le membre qui administre ce foyer (créateur, ou héritier d'adminship si l'admin précédent est parti — voir [backend-architecture.md](./backend-architecture.md#house--foyer-adminship-code-dinvitation-sources-activées)) |
|
|
| `inviteCode` | Code à 8 caractères, unique, généré pour rejoindre le foyer (`POST /house/join`) |
|
|
|
|
### `user_preference` (`UserPreference`)
|
|
|
|
1-1 avec `user_profiles` (`userProfileId` est à la fois clé primaire et clé
|
|
étrangère — un profil a au plus une ligne). `theme` (`LIGHT` / `DARK` / `SYSTEM`,
|
|
défaut `SYSTEM`) — préférence d'affichage, créée à la demande (pas au signup),
|
|
même logique "absence = valeur par défaut" que `dietId`/allergies. Voir
|
|
[frontend-architecture.md](./frontend-architecture.md#thème-clairsombresystème).
|
|
|
|
### `diet` (`Diet`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `key` | Slug anglais stable, unique (ex. `"vegetarian"`) — le libellé affiché vit dans `apps/web/src/locales/fr/translation.json` sous `catalog.diets.<key>`, jamais dans cette table |
|
|
|
|
### `category` / `allergy` (`Category`, `Allergy`)
|
|
|
|
Un allergène = une `Category` (le nom réel, `key` unique) + une `Allergy`
|
|
(juste un id + FK vers sa catégorie). `Category.kind` (`ALLERGY` | `INTOLERANCE`)
|
|
distingue réaction immunitaire classique de réaction non-immunitaire (seuls
|
|
Gluten et Sulfites sont `INTOLERANCE`). 14 allergènes seedés (règlement UE
|
|
1169/2011, annexe II).
|
|
|
|
---
|
|
|
|
## Planification
|
|
|
|
### `planning` (`Planning`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `startDate` / `finishDate` | Lundi → dimanche de la semaine couverte |
|
|
| `houseId` | FK → `house` |
|
|
|
|
Une ligne par semaine et par foyer, créée à la demande (`findOrCreatePlanningForWeek`,
|
|
voir [backend-architecture.md](./backend-architecture.md#planning-semaine-item-portions-import-à-la-volée)),
|
|
pas en avance.
|
|
|
|
### `planning_item` (`PlanningItem`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `planningId` | FK → `planning` |
|
|
| `weekDay` | Jour de la semaine |
|
|
| `meal` | Repas concerné |
|
|
| `recipeId` | FK → `recipe` (pas de cascade — voir `RECIPE_IN_USE` dans [error-handling.md](./error-handling.md)) |
|
|
| `portions` | Nombre de portions **pour ce créneau précis** — indépendant de `Recipe.portions` (le rendement "tel qu'écrit" de la recette) : un créneau peut mettre l'échelle à la hausse/baisse. Le picker web pré-remplit depuis `Recipe.portions` mais stocke une valeur propre |
|
|
|
|
---
|
|
|
|
## Recettes
|
|
|
|
### `sources` (`Source`) et `house_source` (`HouseSource`)
|
|
|
|
`Source` est le catalogue des sources d'import concrètes (un site/une API par
|
|
ligne), synchronisé automatiquement depuis le registre d'adaptateurs en code
|
|
(`recipe-source-registry.ts`) plutôt que maintenu à la main — voir
|
|
[backend-architecture.md](./backend-architecture.md#sources-externes--adaptateur-registre-synchronisation).
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `key` | Slug stable, unique — doit correspondre à `RecipeSourceAdapter.key` |
|
|
| `name` | Nom affiché |
|
|
| `url` | Optionnel |
|
|
| `official` | API officielle vs scraping non-officiel |
|
|
| `iconUrl` | Logo/favicon, optionnel |
|
|
|
|
`HouseSource` (`houseId`, `sourceId`, clé composite) : quelles sources un foyer
|
|
a choisi d'activer — opt-in, aucune ligne = désactivé. Un nouveau foyer démarre
|
|
sans aucune source activée.
|
|
|
|
### `recipe` (`Recipe`)
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `name` | Nom de la recette |
|
|
| `sourceId` | FK → `sources`, nullable (`null` = recette créée à la main) |
|
|
| `externalId` | Identifiant côté source, nullable — avec `sourceId`, `@@unique([sourceId, externalId])` empêche d'importer deux fois le même item (Postgres traite chaque `NULL` comme distinct, donc les recettes manuelles ne se percutent jamais entre elles) |
|
|
| `description` / `picture` | Optionnels |
|
|
| `portions` | Rendement "tel qu'écrit" par la recette |
|
|
| `authorId` | FK → `user_profiles` — requis dès qu'une recette porte un niveau de visibilité |
|
|
| `authorHouseId` | FK → `house`, nullable — **instantané** du foyer de l'auteur *au moment de la création* (comme `Planning.houseId`), ne suit pas l'auteur s'il change de foyer ensuite |
|
|
| `visibility` | `PERSONAL` (auteur seul) / `HOUSE` (membres de `authorHouseId`) / `PUBLIC` (tout utilisateur connecté) — contrôle uniquement la **lecture**, jamais l'édition (toujours réservée à l'auteur, voir `NOT_RECIPE_AUTHOR`) |
|
|
|
|
Relations : `ingredients` (`RecipeIngredient`, m2m avec quantité/unité),
|
|
`steps` (`Step`, one-to-many — pas many-to-many comme documenté dans le doc
|
|
spec d'origine : `order` n'a de sens que dans le cadre d'une seule recette),
|
|
`favoritedBy` (`RecipeFavorite`), `diets` (`RecipeDiet`, tags manuels, pas
|
|
calculés depuis les ingrédients).
|
|
|
|
### `ingredients` (`Ingredient`) et catalogue associé
|
|
|
|
Table de référence (seedée, jamais créée/éditée/supprimée via l'API), comme
|
|
`Diet`/`Allergy`.
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `key` | Slug unique — libellé dans `catalog.ingredients.<key>` (locale) |
|
|
| `icon` | `IngredientIcon` — ~20 pictogrammes génériques par *type* de chose (légume, bouteille d'huile, fromage…), pas un emoji par ingrédient (437 rejetés comme peu pro) — voir `apps/web/src/features/recipes/ingredients/ingredient-icons.tsx` |
|
|
| `category` / `subcategory` | `IngredientCategory` (7 rayons) / `IngredientSubcategory` (racks plus fins) — organisation "rayon de supermarché français" pour permettre le parcours par catégorie dans le picker (400+ ingrédients, la recherche seule ne suffit pas) |
|
|
| `reproducible` | Vrai si raisonnablement faisable maison (un pain burger, une béchamel) plutôt qu'un achat systématique — juste un flag, pas un lien vers une recette précise (un ancien `alternateRecipeId` jamais câblé a été retiré) |
|
|
|
|
`IngredientDiet` (m2m, régimes compatibles — omet volontairement `Omnivore`
|
|
et `Sans gluten`, ce dernier dérivable de `IngredientAllergy`) et
|
|
`IngredientAllergy` (m2m, allergènes contenus) complètent le catalogue.
|
|
`UserProfileDislikedIngredient` (m2m profil ↔ ingrédient) est une préférence
|
|
de **goût personnelle**, pas médicale — jamais un avertissement de sécurité,
|
|
juste un rappel sur la fiche recette (`RecipeDetailPanel`).
|
|
|
|
### `unit` (`Unit`)
|
|
|
|
Catalogue normalisé et fini des unités de mesure — remplace l'ancien texte
|
|
libre (`"g"`, `"grammes"`, `"G"`…) qui ne pouvait jamais être sommé de façon
|
|
fiable.
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `key` | Slug unique (ex. `"tablespoon"`) — libellé dans `catalog.units.<key>` |
|
|
| `type` | `MASS` / `VOLUME` / `COUNT` — seules deux unités du même type sont mutuellement convertibles |
|
|
| `toBaseFactor` | Combien d'unités de base (gramme pour MASS, millilitre pour VOLUME, elle-même pour COUNT) équivaut une unité de ce type — pose les bases d'une future conversion (ex. sommer "500g" + "0.5kg"), pas encore construite |
|
|
|
|
### `step` (`Step`) et techniques détectées
|
|
|
|
| Champ | Description |
|
|
|---|---|
|
|
| `id` | Identifiant |
|
|
| `recipeId` | FK → `recipe` |
|
|
| `description` | Texte de l'étape |
|
|
| `picture` | Optionnel |
|
|
| `order` | Position dans la recette |
|
|
|
|
`tech_step` (`TechStep`, `key` unique, ex. `"simmer"`) est le catalogue des
|
|
techniques (mijoter, préchauffer…) — juste un id/clé stable référencé par
|
|
`step_tech_step`. Les données de détection elles-mêmes (synonymes + phrases
|
|
d'exemple par langue, entraînant un classifieur `node-nlp`) vivent en code
|
|
(`tech-step-training-data.ts`), pas dans une table — l'ancienne
|
|
`tech_step_mapping` (`TechStepMapping`, une regex par technique/locale) a
|
|
été supprimée une fois constaté que les regex ne généralisaient jamais
|
|
au-delà de leur propre vocabulaire — voir
|
|
[backend-architecture.md](./backend-architecture.md#détection-des-techniques--tech-step-matcherts).
|
|
|
|
`step_tech_step` (`StepTechStep`) est la **séquence ordonnée** des techniques
|
|
détectées pour une étape — une instruction peut en impliquer plusieurs (ex.
|
|
"faire chauffer une noix de beurre" = `preheat` + `melt`), d'où une table de
|
|
liaison avec `order` plutôt qu'un simple FK nullable `Step.techStepId`
|
|
(remplacé suite à une revue de code). `start`/`end` (nullable, non rétro-remplis
|
|
— une ligne d'avant l'ajout de ces colonnes n'a simplement pas de
|
|
surlignage tant que sa recette n'est pas resauvegardée) sont le span détecté
|
|
dans `Step.description`, utilisé pour le surlignage côté web
|
|
(`highlight-tech-steps.ts`).
|
|
|
|
---
|
|
|
|
## Relations
|
|
|
|
### Many-to-one (clés étrangères)
|
|
|
|
| Table source | Champ FK | Table cible |
|
|
|---|---|---|
|
|
| `user_profiles` | `houseId` | `house` |
|
|
| `user_profiles` | `dietId` | `diet` |
|
|
| `house` | `adminId` | `user_profiles` |
|
|
| `planning` | `houseId` | `house` |
|
|
| `planning_item` | `planningId` | `planning` |
|
|
| `planning_item` | `recipeId` | `recipe` |
|
|
| `allergy` | `categoryId` | `category` |
|
|
| `recipe` | `sourceId` | `sources` |
|
|
| `recipe` | `authorId` | `user_profiles` |
|
|
| `recipe` | `authorHouseId` | `house` |
|
|
| `step` | `recipeId` | `recipe` |
|
|
|
|
### Many-to-many (tables de jointure explicites, avec ou sans champ additionnel)
|
|
|
|
| Table A | Table B | Table de jointure | Détail |
|
|
|---|---|---|---|
|
|
| `user_profiles` | `allergy` | `user_profile_allergy` | Simple |
|
|
| `user_profiles` | `ingredients` | `user_profile_disliked_ingredient` | Simple — préférence de goût |
|
|
| `user_profiles` | `recipe` | `recipe_favorite` | Simple |
|
|
| `house` | `sources` | `house_source` | Simple — opt-in |
|
|
| `recipe` | `ingredients` | `recipe_ingredient` | Porte `quantity` + `unitId` |
|
|
| `recipe` | `diet` | `recipe_diet` | Simple — tags manuels |
|
|
| `ingredients` | `diet` | `ingredient_diet` | Simple |
|
|
| `ingredients` | `allergy` | `ingredient_allergy` | Simple |
|
|
| `step` | `tech_step` | `step_tech_step` | Porte `order` + `start`/`end` (span détecté) |
|
|
|
|
---
|
|
|
|
## Règles de modélisation
|
|
|
|
- Toute relation qualifiée d'**« association »** entre deux tables est une relation **many-to-many**.
|
|
- `category`, `diet`, `ingredients`, `unit`, `tech_step`, `sources` sont des tables de
|
|
référence/énumération : seedées (`apps/api/src/db/reference-seed-data.ts`),
|
|
jamais créées/éditées/supprimées via l'API applicative. `key`/`name` y sont
|
|
`@unique` pour permettre un seed idempotent (`upsert`).
|
|
- Chaque écart avec le document de conception d'origine (colonnes ajoutées,
|
|
cardinalité changée, table nouvelle) est expliqué en commentaire directement
|
|
dans `schema.prisma`, à l'endroit concerné — s'y référer en cas de doute,
|
|
ce document est un résumé, pas la source de vérité.
|