* feat(recipes): associe ingredients, quantites et ustensiles aux techniques detectees
Etend le pipeline de detection de techniques (tech-step-matcher.ts) pour
resoudre, par clause, les metadonnees qui accompagnent une technique
detectee :
- Ingredients : nouvelle fonction findIngredientMentions (ingredient-matcher.ts)
qui scanne le texte d'une clause contre le catalogue Ingredient existant
(reutilise INGREDIENT_LABELS_FR/EN deja utilise par matchIngredientName),
avec extraction best-effort de la quantite+unite immediatement avant la
mention.
- Ustensiles : nouveau catalogue Utensil (Prisma) + second PhraseMatcher
cote service Python (intent_service/utensil_vocabulary.py), independant
du textcat des techniques (pas d'interpretation necessaire pour un
ustensile). POST /v1/process distingue desormais chaque entite via un
champ kind (technique|utensil).
- Persistance : deux nouvelles tables StepTechStepIngredient/
StepTechStepUtensil, liees a StepTechStep par sa cle composite
(stepId, order), peuplees au moment du matching (recipe.service.ts) et
exposees via StepTechStepView (packages/shared).
Aucune analyse syntaxique ajoutee (le parser spaCy reste exclu du
pipeline) : l'association se fait par appartenance a la clause deja
calculee par splitIntoClauses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): corrige les tests casses par les nouveaux champs ingredients/utensils
recipe-tech-step-correction.test.ts asserte StepTechStepView en dur sans
les nouveaux champs ingredients/utensils (toujours [] pour une correction
manuelle, qui ne repasse jamais par le scan de metadonnees).
Retire aussi le nouveau cas de tech-step-matcher.test.ts qui inventait une
phrase jamais vue par le corpus reel : verifie en CI que le textcat la
classe avec confiance comme caramelize plutot que melt, un artefact du
petit corpus BOW plutot qu'un bug du code de matching. L'extraction
quantite+unite reste couverte integralement et de facon deterministe par
ingredient-matcher.test.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(recipes): equilibre le corpus d'entrainement du textcat a 20 phrases par technique
Chaque technique n'avait que 3 a 7 utterances par locale (moyenne ~3.8),
un desequilibre reel entre classes qui contribue directement a des
classifications confiantes mais fausses sur une formulation jamais vue
(constate concretement dans la PR precedente : une phrase inedite pour
melt classee comme caramelize avec une confiance elevee).
Porte chaque technique a exactement 20 utterances par locale (fr et en) :
- Les utterances existantes sont conservees telles quelles, jamais
reecrites.
- Le complement vient d'augment_utterances.py (nouveau script maintainer,
reutilisable pour une future technique sous-alimentee) : enveloppe
chaque utterance deja a l'imperatif/infinitif dans une tournure modale
grammaticalement valide (il faut/veillez a/make sure to...) plutot que
de dupliquer ou d'inventer du texte generique - vraie diversite de
surface, vocabulaire distinctif de la technique intact.
- tests/test_training_data_balance.py fait respecter l'invariant en CI
(20 minimum, meme nombre fr/en) pour toute future modification.
_TRAINING_ITERATIONS recalibre de 25 a 10 (locale_pipeline.py) pour
compenser les ~2.6x d'exemples par epoque : temps d'entrainement mesure
quasi identique a avant (~687s fr+en combines contre ~670s), confiance
egale ou meilleure sur les cas deja suivis (simmer 0.31 -> 0.48).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): remonte _TRAINING_ITERATIONS a 20, la gate F1 de CI etait sous 0.8 a 10
Le premier passage CI de l'equilibrage du corpus (20 utterances/technique)
a fait chuter le F1 agrege (tech-step-eval.test.ts) a 0.7999... avec
_TRAINING_ITERATIONS=10 : le pari qu'un corpus plus large convergerait en
moins d'epoques relatives etait faux a ce niveau de reduction. Remonte a
20 (mesure : ~699s pour la seule locale fr, previsiblement ~1360s pour
fr+en combines) - confiance nettement retablie sur les techniques
auparavant en echec au spot-check manuel (sweat ~0.99).
Consequence directe : le temps de demarrage du service passe d'environ
11 a environ 23 minutes. start_period (docker-compose.yml) et le timeout
d'attente /health (ci.yml) releves de 900s a 1800s en consequence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): reequilibre le corpus via substitution de synonyme plutot que du remplissage generique
Deux tentatives precedentes de porter chaque technique a 20 utterances
ont mesurablement degrade le F1 agrege (tech-step-eval.test.ts, 0.80 ->
0.79/0.791) au lieu de l'ameliorer : le generateur reposait surtout sur
des tournures modales generiques ("il faut ...", "make sure to ..."),
partagees identiquement par les 74 classes - un textcat bag-of-words lit
ca comme une separabilite reduite entre classes, pas un padding neutre.
augment_utterances.py revu : priorite a la substitution de synonyme
(l'un des synonyms propres a la technique en tete d'une utterance
existante, remplace par un autre - vocabulaire genuinement distinctif),
les tournures modales ne servant plus qu'de complement limite (5 par
locale, pas 12). Resultat : 13 a 20 utterances par technique/locale
(moyenne ~19.7), contre un forcage uniforme a 20 qui necessitait un
remplissage generique disproportionne pour les techniques au vocabulaire
propre pauvre (julienne, sweat, bainMarie - precisement celles qui
echouaient). Confiance mesuree nettement retablie sur ces techniques
(sweat ~0.99, bainMarie ~0.98, julienne ~0.88).
tests/test_training_data_balance.py : plancher abaisse a 12 (vise 20,
garanti seulement si le vocabulaire propre de la technique le permet
sans repasser par le piege ci-dessus) ; suppression de l'exigence
fr/en egaux, plus vraie avec cette strategie (le potentiel de
substitution differe naturellement entre les deux langues).
Suite complete locale : 35/35 verts (22m26s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* revert(recipes): annule le reequilibrage du corpus d'entrainement du textcat
Trois strategies de generation differentes (tournures modales generiques,
tournures reduites + substitution de synonyme, substitution de synonyme
en priorite) ont ete tentees pour porter chaque technique a 20 utterances
par locale. Les trois degradent mesurablement le F1 agrege contre
TECH_STEP_EVAL_DATASET (tech-step-eval.test.ts) en dessous du seuil 0.8 :
0.7999 -> 0.791 -> 0.744 (chaque tentative pire que la precedente).
tech-step-eval-runner.ts documente explicitement ce seuil comme calibre
avec une marge deja tres etroite (0.8 pour un score mesure a 0.815) et
previent contre le fait de l'assouplir pour accommoder un classifieur
plus faible plutot que de corriger le probleme de fond - assouplir le
seuil ou le jeu d'evaluation pour faire passer cette PR irait a l'encontre
de cette convention documentee du projet.
Revient a l'etat d'avant tout reequilibrage (corpus a 3-7 utterances/
technique, _TRAINING_ITERATIONS=25, timeouts a 900s) - le dernier etat
confirme vert en CI sur cette branche. Ameliorer reellement l'equilibre
du corpus necessite du contenu redige a la main et verifie technique par
technique contre ce meme F1, pas une generation programmatique en bloc.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): reequilibre le corpus via substitution de synonyme plutot que du remplissage generique
Trois tentatives precedentes d'egaliser chaque technique a 20 utterances
ont toutes degrade le F1 agrege sous 0.8 (voir le commit revert
precedent). Nouvelle strategie, beaucoup plus conservatrice : egalise
chaque technique vers le maximum DEJA present dans le corpus (7 en fr,
5 en en, portes par cook/preheat), pas vers un nombre choisi dans
l'absolu - +3-4 utterances en moyenne par technique au lieu de +13-17.
augment_utterances.py (nouveau, reutilisable) genere le complement en
priorite par substitution de synonyme (un des synonyms propres a la
technique, en tete d'une utterance existante, remplace par un autre) -
avec un garde-fou supplementaire par rapport aux tentatives precedentes :
le synonyme de remplacement doit lui aussi etre a l'imperatif/infinitif,
pas juste le synonyme d'origine, pour eviter de substituer un groupe
nominal/adjectif ("a petit feu", "gros bouillons") a la place d'un
verbe et produire une phrase grammaticalement cassee. Tournures modales
uniquement en dernier recours pour les techniques dont le vocabulaire
n'apparait qu'en milieu de phrase (julienne, brunoise...).
Resultat : chaque technique a exactement 7 utterances en fr et 5 en en,
sans exception (tests/test_training_data_balance.py fait respecter cet
invariant). _TRAINING_ITERATIONS reste a 25 (inchange). start_period/
timeout d'attente /health releves de 900s a 1200s (temps d'entrainement
mesure ~930s contre ~670s avant, la marge de securite existante etait
devenue trop juste).
Suite complete locale : 35/35 verts (14m41s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: retrigger CI (aucun run genere pour c7116d4, probable incident GitHub Actions)
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
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 - Métadonnées d'action —
Utensil,StepTechStepIngredient,StepTechStepUtensil(ingrédients/quantités/ustensiles associés à une technique détectée, voir plus bas) - Sources externes —
Source,HouseSource - Catalogue ingrédients/unités —
Ingredient,Unit,IngredientDiet,IngredientAllergy,UserProfileDislikedIngredient
Schéma entité-relation
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) |
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) |
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.
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),
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) |
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.
| 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) vivent en code dans le microservice spaCy lui-même
(services/tech-step-intent-service/intent_service/training_data.py), pas
dans une table ni côté apps/api — 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.
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).
Chaque step_tech_step porte en plus les métadonnées trouvées dans sa propre
clause : step_tech_step_ingredient (ingrédient résolu contre le catalogue
ingredients existant, quantity/unit_id optionnels quand une quantité a
pu être extraite juste avant la mention) et step_tech_step_utensil
(ustensile résolu contre un nouveau catalogue utensil, même forme
minimale id/key que tech_step — voir
backend-architecture.md
pour comment chacun est détecté). Les deux référencent step_tech_step par
sa clé composite (step_id, order), onDelete: Cascade comme le reste de
cette chaîne.
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,sourcessont 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/namey sont@uniquepour 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é.