* feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy Remplace TechStepClassifierService's node-nlp (NlpManager) par services/tech-step-intent-service, un microservice FastAPI/spaCy dedie (PhraseMatcher pour le NER par synonymes, textcat pour la classification d'intention). Corpus (TECH_STEP_TRAINING_DATA) toujours possede par apps/api, pousse au service via POST /v1/train a chaque warm-up ; le service ne touche jamais Postgres (meme posture que services/tech-step-llm-worker). Cote apps/api : - intent-service-client.ts : client HTTP vers le nouveau service - tech-step-matcher.ts : delegue NER + intent classification au client, logique pure (splitIntoClauses, seuil/fallback) inchangee - env.ts : INTENT_SERVICE_BASE_URL/INTENT_SERVICE_SECRET (secret requis, service coeur non optionnel) - server.ts : warm-up avec retry/backoff (service Python demarre a part) - scripts/calibrate-tech-step-threshold.ts : recalibration empirique de CONFIDENCE_THRESHOLD contre le jeu d'eval existant - node-nlp retire (package.json, node-nlp.d.ts, model.nlp du .gitignore) docker-compose.yml : nouveau service tech-step-intent-service (pas de port expose, healthcheck, app en depend). CI : job intent-service-test (pytest) + le job test demarre le service en arriere-plan avant la suite Mocha (jamais de mock d'un service interne, cf specs/dev-conventions.md). Verifie : 26/26 tests pytest du service (dont les offsets caracteres exacts de tech-step-matcher.test.ts), lint + build complets du monorepo, smoke test HTTP reel bout en bout. La suite Mocha et docker compose build/up n'ont pas pu etre executes dans cet environnement (pas de Postgres/Docker disponibles ici) — a confirmer via la CI et en local. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): corrige les matches dupliques et le timeout de warm-up des tests CI Deux bugs reels trouves par la premiere execution CI de la migration node-nlp -> tech-step-intent-service : 1. PhraseMatcher retourne tous les matches y compris chevauchants — un synonyme comme "fondre" litteralement contenu dans "faire fondre" (tous deux synonymes de `melt`) produisait deux candidats separes pour la meme technique, dupliquant son techStepId dans le resultat final. Fixe avec spacy.util.filter_spans (garde le plus long match par position) dans LocalePipeline.process. Test de non-regression ajoute. 2. La suite Mocha construit `app` directement via createApp(), sans jamais passer par server.ts — le warm-up (POST /v1/train fr+en sur le corpus complet) se declenchait donc paresseusement dans le premier test qui appelait le classifieur, depassant le timeout Mocha de 10s par test. Fixe par un root hook plugin Mocha (test-support/mocha-root-hooks.ts, .mocharc.json) qui reset la DB et warm up le classifieur une seule fois avant toute suite, avec son propre timeout de 60s. Verifie : 27/27 tests pytest du service (dont le nouveau test de non-regression), lint + build complets du monorepo. La suite Mocha elle-meme n'a toujours pas pu etre executee dans cet environnement (pas de Postgres disponible ici) — a confirmer via la CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ci(temp): ajoute un run de calibrate-tech-step-threshold.ts pour observation Etape temporaire pour lire le sweep de seuils de confiance contre le vrai service tech-step-intent-service en CI (aucun Postgres/service disponible localement dans cette session) — sera retiree une fois CONFIDENCE_THRESHOLD recalibre dans tech-step-matcher.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): recalibre CONFIDENCE_THRESHOLD pour le nouveau classifieur spaCy 0.75 (calibre a l'origine contre node-nlp) laissait de vrais verdicts corrects sur des clauses sans ancre NER (rien sur quoi retomber) sous le seuil : melt scorait 0.68 sur "jusqu'a ce que le beurre ait disparu dans la poele" (le cas motivant tout ce pipeline), preheat 0.52 sur "mettre la poele sur feu vif" — tous deux corrects, tous deux rejetes a 0.75. Recalibre a 0.45 : marge confortable au-dessus du bruit (texte anglais via le classifieur francais score ~0.04, indiscernable du hasard sur ~26 classes) et sous les deux cas ci-dessus. Confirme par calibrate-tech-step-threshold.ts contre TECH_STEP_EVAL_DATASET (F1 plafonne a 0.987 des 0.45, reste plat jusqu'a 0.95 — 0.45 est deja le seuil le plus bas qui capture tout le gain disponible). Retire l'etape CI temporaire de calibration (ci.yml) une fois la valeur choisie. Verifie : lint + build complets du monorepo, 27/27 pytest du service, sweep de seuils + verification manuelle contre le corpus reel en local (services Python, sans Postgres) et en CI. La suite Mocha complete reste a confirmer sur ce commit (executee en CI, pas localement — pas de Postgres disponible dans cet environnement). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): entraine le textcat plus longtemps pour une confiance reelle Cause racine du dernier test Mocha en echec (getAuditBatch flaguait "Faire mijoter a feu doux" comme peu fiable malgre une ancre NER claire) : avec seulement 30 iterations/dropout 0.2, le textcat retournait le bon intent (argmax correct) mais avec une confiance tres basse et compressee (0.2-0.7 sur l'ensemble du corpus reel, y compris des cas evidents) — un vrai probleme de qualite d'entrainement, pas seulement de seuil. 150 iterations / lot de 16 / dropout 0.1 (mesure localement contre le vrai corpus, sans Postgres) : melt ~0.95, preheat ~0.90, jusqu'a ~0.51 pour le cas le plus faible observe (bake), bruit hors-vocabulaire toujours ~0.05. ~110s d'entrainement par locale (~220s pour fr+en au warm-up) — compromis assume et documente (README du service, commentaires du code), contrairement a l'entrainement quasi instantane de node-nlp. Root hook Mocha (mocha-root-hooks.ts) et sa doc mis a jour avec un timeout de 600s pour couvrir cette duree avec marge. Verifie : 27/27 pytest, lint + build complets du monorepo. Suite Mocha a confirmer sur ce commit via CI (source du diagnostic qui a mene a ce fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(recipes): journalise chaque input/output du pipeline NLP Ajoute un logging JSON structure (meme convention que LoggerService cote apps/api) a services/tech-step-intent-service : chaque appel POST /v1/process journalise locale/texte en entree et entites/intent/score en sortie, chaque POST /v1/train journalise les uid entraines et les compteurs resultants. Chatter interne de spaCy mis a WARNING pour ne pas noyer ces lignes. Bug trouve et corrige en verifiant les octets bruts d'un log reel (pas juste son affichage terminal) : l'encodage par defaut de sys.stdout sur Windows produisait de vrais octets UTF-8 invalides pour tout texte accentue journalise (le francais des etapes de recette) — corrige par sys.stdout.reconfigure(encoding="utf-8") au demarrage. LOG_LEVEL configurable (INFO par defaut), documente dans le README du service et .env.example. Verifie : 30/30 pytest (3 nouveaux tests sur le formateur JSON), smoke test HTTP reel confirmant au niveau des octets que les caracteres accentues sont preserves, lint complet du monorepo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(recipes): rapatrie le corpus NLP cote Python et l'enrichit de 48 techniques Changement d'architecture demande par l'utilisateur : le dataset d'entrainement (TECH_STEP_TRAINING_DATA) quitte apps/api pour vivre entierement dans services/tech-step-intent-service (intent_service/training_data.py). Ce service est desormais autonome : il s'entraine lui-meme une seule fois, a son propre demarrage (PipelineRegistry.initialize, dans le lifespan FastAPI), sans plus dependre d'un POST /v1/train pousse par apps/api (route supprimee). apps/api ne connait plus aucune technique/synonyme, uniquement le resultat de POST /v1/process. Corpus enrichi avec les 48 techniques du lexique fourni (Arroser, Appertiser, Braiser, Caraméliser, Confire, Julienne/Brunoise/Mirepoix/ Paysanne, Cuire à blanc/au bain-marie/à l'étouffée, Déglacer variantes, Emulsionner, Glacer, Pocher, Réduire, Suer, Zester, etc.), soit 74 techniques au total (26 + 48). Integration complete bout en bout : - reference-seed-data.ts : 48 nouvelles entrees TECH_STEPS - apps/web/locales/fr/translation.json : libelles francais correspondants - "Mitonner" fondu comme synonyme de simmer (pas une technique distincte, sa propre definition le dit) - "Blanchir un oeuf" (whiskPale) distingue de "Blanchir un legume" (blanch, existant) via des synonymes en phrase complete plutot qu'au mot nu — filter_spans (deja en place) resout la collision par specificite Impact performance mesure : le corpus elargi (74 classes vs 26) rend l'entrainement bien plus lent a nombre d'iterations egal (150 iterations depassait 17 minutes par run de test) — reduit a 40 iterations apres mesures repetees en local (~200s/locale, ~400s pour fr+en combines). docker-compose.yml (healthcheck start_period 600s), CI (timeout curl 600s) et le README du service documentent ce nouveau temps de demarrage. CONFIDENCE_THRESHOLD recalibre a 0.2 par verification manuelle (0.75 puis 0.45 ne tenaient plus compte tenu du nombre de classes) — marque explicitement comme placeholder en attendant une vraie repasse de calibrate-tech-step-threshold.ts (necessite Postgres, indisponible dans cet environnement). Verifie : 28/28 tests pytest du service (suite complete re-ecrite pour s'entrainer une seule fois par session sur le vrai corpus, fixture partagee dans conftest.py), lint + build complets du monorepo. La suite Mocha d'apps/api reste a confirmer via CI (le root hook mocha n'attend plus l'entrainement, seulement CI's propre attente sur /health). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): corrige l'assertion de taille du catalogue TechStep en dur test/reference.test.ts attendait exactement 26 techniques (l'ancien catalogue) au lieu de deriver la longueur attendue de TECH_STEPS (reference-seed-data.ts) — trouve par la CI apres l'ajout des 48 nouvelles techniques (74 au total). Seul echec du run CI precedent, le service Python (nouveau corpus, self-training) a lui demarre et repondu correctement dans le nouveau delai imparti. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): entraine le textcat plus longtemps pour une confiance reelle Suite a une revue de code sur locale_pipeline.py, trois ameliorations implementees et verifiees contre le vrai corpus (74 techniques) : - spacy.util.fix_random_seed(_TRAINING_SEED) avant nlp.initialize() — random.Random() ne graine que l'ordre de melange des exemples, pas l'init des poids/dropout internes de thinc. - _DiacriticsNormalizer deplace au-dessus de sa factory @Language.factory — plus d'annotation de type en chaine. - Log explicite (logger.warning) quand train() recoit moins de 2 labels et saute la creation du textcat, plus une clarification de la docstring de process() sur les deux cas menant a intent=None. - Early stopping avec suivi de la perte par epoque, _TRAINING_ITERATIONS restant le plafond. Mesure sur le vrai corpus : ne se declenche jamais dans le budget actuel de 40 iterations (la perte continue de baisser significativement jusqu'au bout) — documente honnetement comme filet de securite pour un futur relevement du plafond, pas un gain de temps aujourd'hui. Deux suggestions de la revue examinees et non retenues, avec justification en commentaire : le risque de desalignement pattern/texte via normalize_text (normalize_text opere par token deja tokenise, jamais sur la chaine brute — pas de risque de segmentation differente) ; passer a attr="LOWER" aurait au contraire regresse l'insensibilite aux accents que attr="NORM" fournit deliberement. Verifie : 28/28 pytest (dont le vrai corpus complet via la fixture partagee), lint du monorepo. Temps d'entrainement mesure stable (~200-230s/locale, dans la marge de bruit deja documentee). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(recipes): entraine le textcat sur les synonymes en plus des utterances Suite a une suggestion de revue de code : le textcat n'apprenait jusqu'ici que sur entry.utterances, jamais sur entry.synonyms (deja utilises pour le PhraseMatcher). Ajouter le mot-cle isole comme exemple positif de sa propre technique ameliore radicalement la confiance sur les cas ancres sans paraphrase entrainee. Mesures sur le vrai corpus (74 techniques) : - 40 iterations + synonymes (749 exemples vs 286 avant) : gain de confiance massif (simmer 0.25->0.60, cook 0.33->0.60, bake 0.34->0.86) mais temps d'entrainement multiplie par 2.6 (~535s/locale, ~17min combine pour fr+en — inacceptable). - 15 iterations + synonymes : retour a un temps raisonnable (~205s) mais qualite pire qu'avant (simmer/cook repassent sous le seuil de confiance) — les exemples supplementaires ne compensent pas la perte d'epoques a ce point. - 25 iterations + synonymes (retenu) : ~336s/locale (~670s combine), meilleur compromis — tous les cas mesures s'ameliorent par rapport a la config precedente (simmer 0.25->0.31, cook 0.33->0.38, bake 0.34->0.62, zest 0.64->0.66, julienne 0.56->0.76, compote 0.76->0.78), bruit hors-vocabulaire toujours negligeable (~0.02). CONFIDENCE_THRESHOLD releve de 0.2 a 0.25 (le cas le plus faible mesure est maintenant 0.31, avec plus de marge qu'avant). docker-compose.yml (start_period 900s) et la CI (timeout 900s) ajustes pour le nouveau temps de demarrage (~11 min pour fr+en combines, contre ~7 min avant). Deux autres pistes de la meme revue examinees et non retenues avec justification : classe __OTHER__/negatifs hors-domaine (le bruit mesure est deja bas, ~0.02, sans le symptome que cette classe corrige) et boost de score post-traitement si le NER confirme l'intention predite (casserait la garantie "score brut, jamais corrige par l'ancre" que services/tech-step-llm-worker's audit de faible confiance depend explicitement d'avoir, voir le commentaire de TechStepClauseClassification dans tech-step-matcher.ts). Verifie : 28/28 pytest (dont le vrai corpus complet, ~10.5 min pour la suite complete), lint + build du monorepo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
420 lines
16 KiB
Markdown
420 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) 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`).
|
|
|
|
---
|
|
|
|
## 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é.
|