Commit graph

8 commits

Author SHA1 Message Date
e2ffa7d103 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>
2026-08-26 13:38:58 +02:00
84ccfec02c 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>
2026-08-26 12:03:18 +02:00
kyuno053
bf58834aa9
feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy
* 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>
2026-08-26 09:03:52 +02:00
kyuno053
92bea914e8
feat(tech-steps): fiabilise la détection des tech steps (corpus + LLM + corrections utilisateur) (#66)
* feat(tech-steps): fiabilise la detection des tech steps (corpus + LLM + corrections utilisateur)

Une seule feature livree en une seule PR, en 5 phases :

- Phase 1 : enrichit le corpus NLP (tech-step-training-data.ts) et ajoute
  un harness d'evaluation (precision/rappel/F1) avec un jeu de test etiquete
  - la premiere metrique objective de qualite pour ce classifieur.
- Phase 2 : schema Prisma (StepTechStepCorrection, TechStepTrainingSuggestion)
  + endpoints utilisateur (POST/GET corrections, ouverts a tout viewer, pas
  seulement l'auteur) + endpoints internes /internal/tech-steps/* proteges
  par secret partage (requireInternalWorker).
- Phase 3 : UI de highlight/correction cote web (selection de texte ->
  association a une technique, ou clic sur un highlight existant pour le
  corriger/supprimer) - verifiee via Cypress (component + e2e, en Chrome
  reel).
- Phase 4 : worker LLM autonome (services/tech-step-llm-worker, hors du
  monorepo pnpm comme experiments/llm-tech-step-poc) qui audite les clauses
  a faible confiance et transforme les corrections utilisateur en
  suggestions d'entrainement, sans jamais toucher le chemin interactif.
- Phase 5 : script retrain-tech-steps.ts (gate de regression F1 + backfill)
  et list-pending-training-suggestions.ts pour la revue humaine avant
  application au corpus.

Verification effectuee cette session : tsc/biome sur l'ensemble du repo,
build complet (pnpm build), suite Cypress complete (component 39/39, e2e
75/76 - le seul echec est preexistant et sans rapport, cote
recipe-form.feature/ingredient-picker), tests unitaires du worker (6/6) et
son install/typecheck reels contre node-llama-cpp. Les tests Mocha
d'apps/api (Phases 1 et 2) n'ont pas pu etre executes dans cette session
(pas de Postgres local disponible) - a lancer avant merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(tech-steps): calibre le seuil F1 sur une vraie execution et corrige un bug de comptage

Docker etant redevenu disponible dans cette session, j'ai pu lancer pour de
vrai la suite Mocha d'apps/api (334/334, y compris les tests Phase 1/2
qui n'avaient pu etre executes precedemment) ainsi que les scripts de la
Phase 5 contre une vraie base de test.

- tech-step-eval-dataset.ts : corrige un vrai bug d'auteur - "Take the
  plates..." collisionnait avec le synonyme anglais enregistre "plates"
  (technique plate), invalidant ce cas negatif. Remplace par "dishes".
- tech-step-eval-runner.ts : F1 reel mesure = 0.815 (33 TP / 9 FP / 6 FN).
  Documente ce chiffre et les vraies erreurs de classification decouvertes
  (ex: "Blanchissez les haricots verts..." classifie a tort comme "peel")
  - des faiblesses reelles du classifieur que ce harness est cense
  detecter, pas a masquer en ajustant le jeu de test.
- retrain-tech-steps.ts : le script loggait `appliedIds.length`/
  `rejectedIds.length` (ce qui a ete demande) au lieu du `count` reel
  retourne par `updateMany` (ce qui a vraiment ete modifie) - un id
  inexistant faisait afficher un faux succes. Decouvert en executant le
  script pour de vrai avec des ids partiellement invalides.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(tech-steps): corrige un span de correction incorrect sur un highlight existant

Bug reel trouve en lancant l'application pour de vrai et en cliquant sur
un highlight existant : la correction soumise couvrait presque toute la
description au lieu du seul mot-cle cliqué (ex: [6, 56) au lieu de [6, 13)
pour "mijoter").

Cause : StepDescription.tsx capturait `start` dans un `const` par
iteration de `.map()` (correct), mais utilisait `offset` directement (la
variable mutable partagee, pas une valeur capturee) pour `end` dans le
gestionnaire onClick - une fermeture classique sur variable de boucle
encore mutee. Par le temps ou l'utilisateur clique reellement (bien apres
la fin du rendu), `offset` contient sa valeur finale (fin de la
description entiere), pas celle du segment concerne.

Corrige en capturant `end` dans un `const` au meme endroit que `start`.
Renforce aussi l'assertion e2e correspondante (recipes.ts) qui ne
verifiait auparavant que la requete avait ete faite, jamais son contenu -
elle serait passee malgre ce bug.

Verifie en conditions reelles : recette creee via l'UI, correction
soumise, span persiste verifie directement en base (start=6, end=13,
previous=simmer, corrected=grill).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore: ignore les telechargements Cypress (artefact de run local)

* feat(tech-steps): distingue les corrections manuelles des détections auto

Les corrections utilisateur (via TechStepCorrectionPopover) sont
désormais écrites directement dans StepTechStep, avec une colonne
`source` ("auto" | "manual") qui les distingue des matches du
classifieur NLP :

- Migration `step_tech_step_source` ajoutant `source` (défaut "auto")
- `applyManualCorrection`/`renumberStepTechSteps` dans
  recipe-tech-step-correction.service.ts : une correction met à jour
  ou crée l'entrée StepTechStep concernée (source "manual"), la
  réponse de l'endpoint inclut désormais le techSteps à jour du step
  (SubmitTechStepCorrectionResult), pas seulement l'audit de
  correction
- backfill-tech-steps.ts préserve les entrées "manual" existantes :
  seules les entrées "auto" sont recalculées, et un nouveau match
  auto chevauchant une correction manuelle est ignoré plutôt
  qu'inséré en doublon — vérifié en base réelle (une correction
  manuelle survit intacte à un backfill complet)
- Le front distingue visuellement les deux (StepDescription.tsx,
  recipes.scss : `.step-tech-step--manual`, couleur Turmeric au lieu
  de Basil), avec un tooltip "(correction manuelle)" et un indicateur
  de découvrabilité de la fonctionnalité dans RecipeDetailPanel

Corrige aussi deux bugs trouvés en testant en conditions réelles :
- StepDescription.tsx : le clic sur un highlight existant lisait la
  variable `offset` (mutable, partagée par la boucle) au lieu d'une
  valeur capturée, envoyant un `end` erroné (fin de la description
  entière au lieu du span du mot cliqué)
- backfill-tech-steps.ts : le garde `import.meta.url ===
  file://${process.argv[1]}` ne matche jamais sur Windows (chemins à
  antislash), le script ne faisait donc rien en exécution directe ;
  remplacé par `pathToFileURL(process.argv[1]).href`

335 tests apps/api passants, 40/40 composants Cypress, 75/76 e2e
Cypress (1 flake pré-existant sans rapport, non touché ici).

* fix(worker): corrige le build Docker de tech-step-llm-worker

docker compose build tech-step-llm-worker échouait sur deux problèmes
en cascade, tous deux liés à l'isolation volontaire de ce service hors
du monorepo pnpm (seul son propre package.json/tsconfig.json est copié
dans son contexte de build) :

- pnpm install --ignore-workspace --frozen-lockfile échouait
  (ERR_PNPM_IGNORED_BUILDS) : sans "packageManager" dans son
  package.json, corepack télécharge le pnpm le plus récent
  (11.22.0), qui a durci en erreur bloquante ce qui n'était qu'un
  avertissement sur les builds de dépendances ignorés
  (esbuild/node-llama-cpp). Le reste du repo est épargné parce que
  apps/api/Dockerfile copie le package.json racine, qui pinne déjà
  pnpm@10.12.4 — ce pin ne pouvait pas atteindre ce service isolé.
  Fixé en pinnant la même version ici.
- tsc échouait ensuite (TS5083 puis erreurs en cascade dans les .d.ts
  de node-llama-cpp) : tsconfig.json de ce service extends le
  tsconfig.base.json racine (skipLibCheck notamment), jamais copié
  dans le contexte de build. Fixé en le copiant avant tsconfig.json.

Vérifié : `docker compose build tech-step-llm-worker` complet en local.

* fix(tech-steps): empêche le contexte d'un match d'avaler une correction manuelle voisine

La correction manuelle ne s'affichait pas quand elle portait sur du texte
qui n'était pas une technique à l'origine — reproduit en live : une
description avec un seul match auto-détecté ("mijoter") voit son
contexte de clause s'étendre sur toute la description dès que
splitIntoClauses (tech-step-matcher.ts) n'a trouvé qu'un seul candidat
NER (le cas courant), même quand ce candidat n'a aucun rapport avec le
reste du texte. splitDescriptionByTechSteps avançait alors son curseur
jusqu'à la fin de ce contexte large, ce qui faisait purement et
simplement disparaître (silencieusement, sans erreur) toute correction
manuelle ajoutée plus loin dans la même description — un mot pourtant
sans aucun rapport avec la technique auto-détectée.

Le contexte d'un match est purement cosmétique (StepDescription.tsx le
rend identique à du texte brut depuis que sa mise en valeur dédiée a
été désactivée) et ne doit donc jamais coûter son propre highlight à
un *autre* match. splitDescriptionByTechSteps distingue maintenant
deux notions : le chevauchement entre les spans *keyword* stricts de
deux entrées (toujours un vrai conflit, l'entrée la plus tardive est
toujours ignorée, comportement inchangé) et le chevauchement du
contexte *cosmétique* d'une entrée sur le keyword d'une autre (jamais
un vrai conflit désormais : le contexte est simplement rogné pour
laisser la place, plutôt que l'entrée voisine entière étant abandonnée).

Vérifié en conditions réelles (Docker) : une correction manuelle sur
"materiel" dans "Faire mijoter la sauce, puis ranger le materiel."
s'affiche maintenant correctement à côté du highlight auto "mijoter",
et survit à un rechargement complet de la page.

Nouveau test de régression dans highlight-tech-steps.cy.tsx
reproduisant exactement ce cas ; les 18 tests du fichier (dont tous
les cas de contexte/malformation déjà couverts) passent toujours.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 15:57:57 +02:00
266f540d88 fix(api): make the session cookie's Secure flag overridable
Found on http://batch.dev.kyuno.fr/: login/signup succeeded (200/201,
profile in the body) but every subsequent request 401'd. Cause: the
session cookie is `secure: NODE_ENV === "production"`, and
docker-compose.yml sets NODE_ENV=production regardless of whether the
deployment actually has TLS in front of it. A Secure cookie is silently
never sent back by the browser over plain HTTP — no error, just a cookie
that never round-trips.

Adds COOKIE_SECURE, independent from NODE_ENV, to override the flag per
deployment. Unset (default) keeps prior behavior — secure in production.
Set COOKIE_SECURE=false only for a deployment reachable over plain HTTP
(no TLS yet), like this dev instance.

Verified locally: docker compose up with COOKIE_SECURE=false persists
and round-trips the cookie (signup -> /auth/me 200); without it, the
cookie still gets Secure as before. Full pnpm --filter api test / test:bdd
suites still pass (66 + 25 scenarios).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:49:05 +02:00
c2cc773c7b Fusionner web+api en une image, séparer la CI, ajouter la release
- express-tools: ExpressServer.serveStaticFrontend() sert le build du
  frontend (assets + fallback SPA), monté après les routes API et avant
  le 404 JSON. Opt-in via FRONTEND_DIST_DIR (uniquement défini dans
  l'image Docker) — le dev natif (dev:api/dev:web) est inchangé.
- apps/api/Dockerfile: build aussi apps/web, embarque son dist dans le
  runtime ; corrige au passage l'oubli de packages/date-tools. Supprime
  apps/web/Dockerfile et nginx.conf (plus de conteneur nginx séparé).
- docker-compose.yml: un seul service "app" (postgres + app), un seul
  port APP_PORT, plus de WEB_PORT/CORS_ORIGIN à coordonner entre deux
  origines. Garde `build:` (pas de registre — Portainer build depuis le
  repo Git).
- ci.yml: éclate le job unique lint-and-test+e2e en 4 jobs indépendants
  (lint/test/build/e2e), sans chaînage, déclenchés sur chaque push
  (toute branche) + PR vers main.
- release.yml (nouveau): sur tag vX.Y.Z, sanity-build de l'image Docker,
  GitHub Release avec changelog auto-généré, puis notification best-effort
  du webhook Portainer (secret PORTAINER_WEBHOOK_URL).
- README: documente le conteneur unique et le pipeline de release.
2026-08-17 23:11:26 +02:00
kyuno053
bc582b75d6
Add Docker packaging for local functional review (api + web) (#5)
apps/api/Dockerfile: multi-stage build on node:22-slim (not alpine —
avoids musl-vs-glibc native binding surprises for argon2/Prisma's
engine binaries; same base image family for build and runtime stages
keeps "native" binaries compatible across stages). Runtime stage
copies the monorepo structure as-is rather than flattening to a single
package, so pnpm's symlinked node_modules stay valid. Container runs
`prisma migrate deploy` on startup before starting the server, so the
review environment's schema is always in sync automatically.

Installs openssl explicitly in the base image: without it, Prisma
can't detect the right engine binary and silently defaults to a guess
that may not match what's actually on the image — caught by checking
the build log, not just a successful build.

apps/web/Dockerfile: builds with Vite, serves the static output via
nginx (not a Node static server) — avoids the devDependency problem of
needing `vite preview` in a --prod-deployed image, and is the more
standard way to serve a built SPA. nginx.conf has an SPA fallback
(try_files ... /index.html) ready for when client-side routing lands.

docker-compose.yml: adds `api` and `web` services alongside the
existing `postgres`. api's DATABASE_URL targets the `postgres` service
name over the compose network (not localhost/POSTGRES_PORT, which is
only the host-side mapping). Both new services require JWT_SECRET/
ports via env vars with no defaults, consistent with the project's
existing no-hardcoded-credentials rule.

.dockerignore added — without it, the Windows-built node_modules
(with Windows-specific native binaries) would get copied into the
Linux build context.

Verified: full build (api + web images), `docker compose up -d`
brought up all three containers, curled /health and the web root,
ran a real signup through the containerized stack end-to-end.
2026-08-16 14:13:51 +02:00
kyuno053
746100e257
Scaffold generic pnpm monorepo (api + web + shared) (#1)
* Scaffold generic pnpm monorepo (api + web + shared)

Sets up the initial project infrastructure only, no business modules yet:

- apps/api: Express/TypeScript backend skeleton (healthcheck route, zod-validated
  env config, error handling, Prisma initialized with no models yet, Postgres
  as the target DB)
- apps/web: React/Vite/TypeScript frontend skeleton, Capacitor-ready for the
  future mobile app
- packages/shared: empty placeholder for types/schemas shared between api and
  web once the data model is defined
- Tooling: Biome (lint/format), Mocha+Chai+Supertest (api tests), Cypress
  (web e2e smoke test), GitHub Actions CI (lint + test + build + e2e)
- docker-compose.yml for local Postgres
- README documents setup steps, including the Cypress binary caveat (pnpm
  install doesn't always fetch the native binary — needs `cypress install`
  run locally per machine)

Fixes along the way:
- apps/web/cypress.config.ts: disable GPU on browser launch for
  headless/sandboxed environments
- apps/web/tsconfig.*: split into solution/app/node tsconfig files (standard
  Vite pattern) — the previous single-file setup caused `tsc -b` to emit
  compiled .js/.d.ts next to vite.config.ts and cypress.config.ts

* Remove hardcoded credentials from committed env/compose files

.env.example and apps/api/.env.example had a real usable default
credential pair (batchcooking/batchcooking) baked in, and
docker-compose.yml fell back to the same values via ${VAR:-default}
if .env was missing. Neither should ship a working credential:

- .env.example / apps/api/.env.example now use "changeme" placeholders
  that must be edited before use.
- docker-compose.yml uses ${VAR:?...} instead of ${VAR:-default} for
  POSTGRES_USER/PASSWORD/DB, so compose fails loudly if .env isn't set
  up rather than silently falling back to a guessable credential.
  Healthcheck reads the container's own env var ($$POSTGRES_USER)
  instead of duplicating the value in the compose file.
- README updated to say .env.example must be edited, not just copied.

Verified: `docker compose config` fails with a clear message when
.env is absent, and resolves correctly once .env is filled in.

* Fix CI: remove pnpm version conflict with packageManager field

pnpm/action-setup@v4 errored with "Multiple versions of pnpm
specified" because the workflow pinned version: 10 while
package.json's packageManager field pins pnpm@10.12.4. The action
already reads packageManager automatically, so drop the redundant
version input.

* Fix CI: install Cypress binary explicitly before running e2e

Same root cause as the README caveat: pnpm install doesn't reliably
trigger Cypress's postinstall binary download, so `cypress run` failed
in CI with "The cypress npm package is installed, but the Cypress
binary is missing." Add an explicit `cypress install` step, and cache
~/.cache/Cypress keyed on the lockfile so subsequent runs don't
re-download it.

* Fix Cypress config loading: give the solution tsconfig a module system

apps/web/tsconfig.json (the tsc -b "solution" file) had no
compilerOptions, only files/references. Cypress's bundled ts-node
picks the nearest tsconfig.json to transpile cypress.config.ts, and
with no "module" specified it defaulted to CommonJS while
package.json declares "type": "module" — causing:

  ReferenceError: exports is not defined in ES module scope

Adding module/moduleResolution to the solution config (harmless for
tsc -b itself, since it only builds the referenced projects) fixes
the mismatch. This regressed after the earlier fix for the stray
vite.config.js emission and was never re-verified against Cypress
until CI caught it.
2026-08-16 10:55:13 +02:00