batchCooking/apps/api/test/recipe-matching/tech-step-eval.test.ts
Nicolas 53d415fddb 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>
2026-08-22 09:48:02 +02:00

50 lines
2.2 KiB
TypeScript

import { expect } from "chai";
import { prisma } from "../../src/db/prisma.js";
import {
MIN_OVERALL_F1,
runTechStepEvalSuite,
} from "../../src/lib/recipe-matching/tech-step-eval-runner.js";
import { resetDatabase } from "../../test-support/reset-db.js";
/**
* Regression gate for `TECH_STEP_TRAINING_DATA` — every change to that
* corpus (including a maintainer applying suggestions from
* `TechStepTrainingSuggestion`, see `scripts/retrain-tech-steps.ts`) must
* keep this suite green. Runs {@link runTechStepEvalSuite} (the real
* trained classifier against `tech-step-eval-dataset.ts`) and asserts the
* aggregate F1 doesn't fall below {@link MIN_OVERALL_F1}.
*
* `MIN_OVERALL_F1` (`tech-step-eval-runner.ts`) is a provisional floor,
* not a target: most of the dataset's cases are built around a
* technique's own registered synonym, which `_classifyClause` always
* resolves correctly via its NER-anchor fallback even when the intent
* classifier itself scores under `CONFIDENCE_THRESHOLD` (see
* `tech-step-matcher.ts`'s doc comment, point 3) — so a healthy run should
* land well above this floor. It's set low enough to tolerate the residual
* uncertainty in a dataset authored without being able to run it against a
* live trained classifier first (no local Postgres was reachable in the
* session that introduced this file — see this feature's plan document).
* Once this suite has actually run once (locally or in CI) and produced
* real numbers, tighten that constant to just below the observed F1, so a
* real future regression still fails loudly instead of hiding under a
* floor that's too forgiving.
*/
describe("tech-step-eval", () => {
beforeEach(async () => {
await resetDatabase();
});
after(async () => {
await prisma.$disconnect();
});
it(`scores at least ${MIN_OVERALL_F1} aggregate F1 against the labeled evaluation set`, async () => {
const { overall, byKey } = await runTechStepEvalSuite();
expect(
overall.f1,
`aggregate F1 ${overall.f1.toFixed(3)} (precision ${overall.precision.toFixed(3)}, recall ${overall.recall.toFixed(3)}) fell below the ${MIN_OVERALL_F1} floor — per-technique breakdown: ${JSON.stringify(byKey)}`,
).to.be.at.least(MIN_OVERALL_F1);
});
});