import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { recordJudgedOutcomesUseCase } from "../../application/routing/record-judged-outcomes.js"; import { YamlRoutingConfigReader } from "../../infrastructure/adapters/filesystem/yaml-routing-config-reader.js"; import { JsonlRoutingDecisionReader } from "../../infrastructure/adapters/jsonl/jsonl-routing-decision-reader.js"; import { JsonlRoutingOutcomeReader } from "../../infrastructure/adapters/jsonl/routing-outcome-jsonl-reader.js"; import { JsonlRoutingOutcomeWriter } from "../../infrastructure/adapters/jsonl/routing-outcome-jsonl-writer.js"; import { createClosableStateStoresUnchecked } from "../../infrastructure/adapters/sqlite/create-state-stores.js"; import { resolvePluginRoot } from "../../infrastructure/plugin-root.js"; import { type CommandSchema, parseFlags } from "../utils/flag-parser.js"; import { resolveSliceId } from "../utils/resolve-id.js"; import { resolveRoutingPaths } from "../utils/routing-paths.js"; import { isOk, sliceLabelFor, PreconditionViolationError } from "@tff/core"; export const routingJudgeRecordSchema: CommandSchema = { name: "routing:judge-record", purpose: "Persist JudgeVerdicts emitted by the tff-outcome-judge agent as model-judge outcomes", mutates: true, requiredFlags: [{ name: "slice", type: "string", description: "Slice label (M##-S##) or UUID" }], optionalFlags: [ { name: "verdicts-path", type: "string", description: "Path to a JSON file containing { verdicts: [...] }", }, { name: "verdicts-json", type: "string", description: "Inline JSON string (alternative to --verdicts-path)", }, { name: "evidence-truncated", type: "boolean", description: "Prefix reasons with [evidence_truncated]", }, ], examples: ["routing:judge-record --slice M01-S02 --verdicts-path /tmp/verdicts.json"], }; export interface RoutingJudgeRecordFactoryOverrides { sliceStatusLookup?: (sliceId: string) => Promise; sliceLabelLookup?: (sliceId: string) => Promise; clearPendingJudgment?: (sliceId: string) => void; } export const routingJudgeRecordCmd = async ( args: string[], overrides: RoutingJudgeRecordFactoryOverrides = {}, ): Promise => { const parsed = parseFlags(args, routingJudgeRecordSchema); if (!parsed.ok) return JSON.stringify(parsed); const flags = parsed.data as { slice: string; "verdicts-path"?: string; "verdicts-json"?: string; "evidence-truncated"?: boolean; }; const projectRoot = process.cwd(); const pluginRoot = resolvePluginRoot(); const configReader = new YamlRoutingConfigReader({ projectRoot, pluginRoot }); const configRes = await configReader.readConfig(); if (!isOk(configRes)) return JSON.stringify({ ok: false, error: configRes.error }); if (!configRes.data.enabled) { return JSON.stringify({ ok: true, data: { skipped: true, reason: "routing_disabled" } }); } const modelJudgeCfg = configRes.data.calibration?.model_judge; const modelJudgeEnabled = modelJudgeCfg?.enabled ?? false; if (!modelJudgeEnabled) { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", [ "settings.routing.calibration.model_judge.enabled", ]), }); } // Resolve verdicts: prefer --verdicts-path, fall back to --verdicts-json let verdicts: unknown; if (flags["verdicts-path"]) { let raw: string; try { raw = readFileSync(flags["verdicts-path"], "utf8"); } catch { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["verdicts-path.readable"]), }); } try { verdicts = JSON.parse(raw); } catch { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["verdicts-path.json"]), }); } } else if (flags["verdicts-json"]) { try { verdicts = JSON.parse(flags["verdicts-json"]); } catch { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["verdicts-json.json"]), }); } } else { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["verdicts.source"]), }); } const { routingPath, outcomesPath } = resolveRoutingPaths( projectRoot, configRes.data.logging.path, ); let sliceId: string; let sliceLabel: string; let sliceStatus: string; let clearPending: ((sliceId: string) => void) | null = overrides.clearPendingJudgment ?? null; if (overrides.sliceLabelLookup && overrides.sliceStatusLookup) { sliceId = flags.slice; sliceLabel = await overrides.sliceLabelLookup(sliceId); sliceStatus = await overrides.sliceStatusLookup(sliceId); } else { const stores = createClosableStateStoresUnchecked(); const { sliceStore, milestoneStore } = stores; try { const resolvedRes = resolveSliceId(flags.slice, sliceStore); if (!resolvedRes.ok) return JSON.stringify({ ok: false, error: resolvedRes.error }); sliceId = resolvedRes.data; const sliceEntity = sliceStore.getSlice(sliceId); if (!sliceEntity.ok || !sliceEntity.data) { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["slice.exists"]), }); } let milestone: { number: number } | undefined; if (sliceEntity.data.milestoneId) { const milestoneRes = milestoneStore.getMilestone(sliceEntity.data.milestoneId); if (!milestoneRes.ok || !milestoneRes.data) { return JSON.stringify({ ok: false, error: new PreconditionViolationError("Precondition violated", ["milestone.exists"]), }); } milestone = milestoneRes.data; } sliceLabel = sliceLabelFor(sliceEntity.data, milestone); sliceStatus = sliceEntity.data.status; } finally { stores.close(); } // Drain the pending-judgment marker after a successful append by // re-opening a store. Best-effort — recording is already durable. if (!clearPending) { clearPending = (id: string) => { const s = createClosableStateStoresUnchecked(); try { s.pendingJudgmentStore.clearPending(id); } finally { s.close(); } }; } } const decisionReader = new JsonlRoutingDecisionReader(routingPath); const knownDecisions = await decisionReader.readKnownDecisions(); const sliceDecisions = knownDecisions .filter((k) => k.slice_id === sliceLabel) .map((k) => ({ decision_id: k.decision_id, slice_id: k.slice_id, workflow_id: k.workflow_id, })); const outcomesSource = new JsonlRoutingOutcomeReader(outcomesPath); const writer = new JsonlRoutingOutcomeWriter(outcomesPath); const res = await recordJudgedOutcomesUseCase( { slice_id: sliceId, verdicts, evidence_truncated: flags["evidence-truncated"] ?? false, }, { sliceStatus, decisions: sliceDecisions, outcomesSource, writer, modelJudgeEnabled, uuid: () => randomUUID(), now: () => new Date().toISOString(), }, ); if (!isOk(res)) return JSON.stringify({ ok: false, error: res.error }); if (clearPending) { try { clearPending(sliceId); } catch { // Best-effort: outcomes are already persisted. } } return JSON.stringify({ ok: true, data: { ...res.data, slice_label: sliceLabel }, }); };