import { env } from "./config.js"; /** * Thin fetch wrapper around `apps/api`'s `/internal/tech-steps/*` and * `/reference/tech-steps` — the only two surfaces this worker ever talks * to (see `tech-step-worker.service.ts`/`tech-step-worker.routes.ts` on * that side). No Prisma client, no direct database access at all: every * read/write goes through here, over HTTP, authenticated with * `INTERNAL_WORKER_SECRET` — see `requireInternalWorker` * (`apps/api/src/middlewares/require-internal-worker.ts`) for why that's a * separate mechanism from a user session. */ /** One reference technique, as returned by the public `GET /reference/tech-steps` — this worker's only source of the taxonomy it audits/labels against, never a hardcoded copy (see `tech-step-taxonomy.ts`). */ export interface TechStepReference { id: number; key: string; } /** Mirrors `TechStepAuditClauseView` (`packages/shared`) — duplicated here rather than importing from `@batch-cooking/shared`, since this worker deliberately lives outside the pnpm workspace (see `package.json`'s own doc comment) and so can't depend on a workspace package. */ export interface AuditClause { stepId: number; recipeId: number; clauseText: string; anchorKey: string | null; intentKey: string | null; score: number; locale: string; } /** Mirrors `PendingTechStepCorrectionView` (`packages/shared`) — same "duplicated, not imported" reasoning as {@link AuditClause}. */ export interface PendingCorrection { id: number; stepId: number; recipeId: number; clauseText: string; start: number; end: number; previousTechStepKey: string | null; correctedTechStepKey: string | null; } /** One suggestion `postTrainingSuggestions` submits — mirrors one entry of `SubmitTrainingSuggestionsInput["suggestions"]` (`packages/shared`). */ export interface TrainingSuggestionInput { techStepKey: string; locale: string; suggestedSynonyms: string[]; suggestedUtterances: string[]; sourceType: "correction" | "llm_audit"; sourceCorrectionId?: number | null; } async function request( path: string, init: RequestInit = {}, ): Promise { try { const response = await fetch(`${env.API_BASE_URL}${path}`, { ...init, headers: { "Content-Type": "application/json", "X-Internal-Worker-Secret": env.INTERNAL_WORKER_SECRET, ...init.headers, }, }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status} ${body}`); } return (await response.json()) as TResponseBody; } catch (err) { // Rethrown as-is — every caller (the scheduler's per-job try/catch, // see `scheduler.ts`) already decides what to do with a failed run; // this is just the one place the `await` itself has to sit inside a // try/catch, same convention `apps/api` follows for the same reason. throw err; } } /** The full reference technique catalog — `apps/api`'s `TechStep` table, read fresh (never cached beyond one process's lifetime) so a catalog change is picked up on the worker's next restart without a code change here. */ export function getTechStepReference(): Promise { return request("/reference/tech-steps"); } /** Low-confidence clauses sampled from existing recipes — `audit-low-confidence`'s raw material. */ export function getAuditBatch(locale: string, limit: number): Promise { const params = new URLSearchParams({ locale, limit: String(limit) }); return request(`/internal/tech-steps/audit-batch?${params}`); } /** Corrections not yet turned into a suggestion — `transform-corrections`'s raw material. */ export function getPendingCorrections(limit: number): Promise { const params = new URLSearchParams({ limit: String(limit) }); return request(`/internal/tech-steps/pending-corrections?${params}`); } /** Submits a batch of suggestions — a no-op (resolves immediately) if `suggestions` is empty, so a job with nothing to report doesn't need its own guard at every call site. */ export function postTrainingSuggestions( suggestions: TrainingSuggestionInput[], ): Promise<{ created: number }> { if (suggestions.length === 0) return Promise.resolve({ created: 0 }); return request("/internal/tech-steps/training-suggestions", { method: "POST", body: JSON.stringify({ suggestions }), }); }