batchCooking/apps/api/src/scripts/backfill-tech-steps.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

70 lines
2.9 KiB
TypeScript

import { prisma } from "../db/prisma.js";
import { techStepClassifier } from "../lib/recipe-matching/tech-step-matcher.js";
/**
* Recomputes every existing `Step`'s `StepTechStep` sequence against the
* *current* classifier (`tech-step-matcher.ts`/`tech-step-training-data.ts`),
* the same way `updateRecipe` does when a user resaves a recipe through the
* UI — always `"fr"` (`DEFAULT_TECH_STEP_LOCALE` in `recipe.service.ts`;
* there's no persisted per-recipe locale to recover for a step that already
* exists, so this matches real resave behavior exactly rather than
* guessing).
*
* Needed because tech-step detection only ever runs at create/update time
* (`recipe.service.ts`'s `matchStepsTechSteps`), never retroactively — a
* step saved before a classifier/corpus change (new vocabulary, or the
* `contextStart`/`contextEnd` columns a previous session added) keeps
* whatever it was matched with at the time until it's next resaved.
*
* Exported (not just called from this file's own CLI guard below) so
* `retrain-tech-steps.ts` can run it as one step of its own larger
* maintainer workflow, without shelling out to a second process.
*
* Safe to re-run: each step's technique sequence is fully replaced (delete
* + recreate) from the classifier's current output, same as a real edit —
* running it twice in a row with no corpus change in between is a no-op.
*/
export async function backfillTechSteps(): Promise<{ total: number; changed: number }> {
const steps = await prisma.step.findMany({ select: { id: true, description: true } });
console.info(`Recomputing tech steps for ${steps.length} step(s)...`);
let changed = 0;
for (const step of steps) {
const matches = await techStepClassifier.matchTechStepSpans(step.description, "fr");
await prisma.$transaction([
prisma.stepTechStep.deleteMany({ where: { stepId: step.id } }),
prisma.stepTechStep.createMany({
data: matches.map((match, order) => ({
stepId: step.id,
techStepId: match.techStepId,
order,
start: match.start,
end: match.end,
contextStart: match.contextStart,
contextEnd: match.contextEnd,
})),
}),
]);
changed += 1;
}
console.info(`Done — ${changed} step(s) recomputed.`);
return { total: steps.length, changed };
}
// Only runs when this file is executed directly (`tsx
// src/scripts/backfill-tech-steps.ts`), not when `retrain-tech-steps.ts`
// imports `backfillTechSteps` above — the standard ESM "is this the entry
// module" check, first needed in this codebase by that new script; every
// prior script here (`seed-runtime.ts`) was always only ever run directly,
// never imported.
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
backfillTechSteps()
.then(() => prisma.$disconnect())
.catch(async (err) => {
console.error(err);
await prisma.$disconnect();
process.exit(1);
});
}