* 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>
434 lines
17 KiB
Markdown
434 lines
17 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`
|
|
- **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
|
|
|
|
```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) 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](./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`).
|
|
|
|
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](./backend-architecture.md#détection-des-techniques--tech-step-matcherts)
|
|
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`, `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é.
|