import fs from "node:fs/promises"; const corpus = JSON.parse( await fs.readFile( process.argv[3] || new URL("../test/fixtures/extraction-corpus.json", import.meta.url), "utf8", ), ); const api = async (path, body) => { const r = await fetch( "http://127.0.0.1:4191/api/" + path, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(65000), } : undefined, ); return { ok: r.ok, status: r.status, data: await r.json() }; }; const status = (await api("status")).data; if (!status.modelConfigured) throw new Error("Connect the Azure model through the UI first."); const report = { runAt: new Date().toISOString(), model: status.model, scope: "Synthetic development corpus. Not independent clinical validation; no real patient data.", cases: [], }; const clean = (s) => s.toLowerCase().replace(/\s+/g, "").replaceAll("²", "2"); const matches = (f, g) => f.kind === g[0] && clean(f.label) === clean(g[1]) && g .slice(2) .every((v) => clean((f.normalizedLine ?? "") + " " + f.detail).includes(clean(v)), ); for (const item of corpus) { const row = { id: item.id, passed: false }; const start = performance.now(); const sources = [ { id: "record-" + item.id, title: "Synthetic clinical note", kind: "Synthetic note", date: "2026-10-23", text: item.text, }, ]; try { const extracted = await api("extract", { sources, syntheticOnly: true, useModel: true, freshModel: true, }); if (!extracted.ok) throw new Error(extracted.data.error); const d = extracted.data; row.mode = d.mode; row.strategy = d.strategy; row.attempts = d.attempts; row.extractionMs = d.durationMs; row.metrics = d.metrics; const usable = d.facts.filter((f) => !f.problem); row.required = item.required.length; row.matched = item.required.filter((g) => usable.some((f) => matches(f, g)), ).length; row.missing = item.required.filter( (g) => !usable.some((f) => matches(f, g)), ); row.unsafe = (item.withheld ?? []).filter((g) => usable.some((f) => matches(f, g)), ); row.unexpected = usable .filter((f) => !item.required.some((g) => matches(f, g))) .map((f) => ({ kind: f.kind, label: f.label, detail: f.detail })); row.quarantined = d.warnings?.length ?? 0; const review = Object.fromEntries( d.facts.map((f) => [f.id, f.problem ? "exclude" : "include"]), ); const assessed = await api("assess", { sources, syntheticOnly: true, draftId: d.id, review, fingerprint: d.fingerprint, acknowledgeUnparsed: true, }); if (!assessed.ok) throw new Error(assessed.data.error); row.ruleIds = assessed.data.contextual.findings.map((f) => f.ruleId); row.ruleCorrect = (!item.rule || row.ruleIds.includes(item.rule)) && (!item.absentRule || !row.ruleIds.includes(item.absentRule)); const exported = await api("reports/" + assessed.data.reportId); row.exportVerified = exported.ok && exported.data.extraction.fingerprint === d.fingerprint && exported.data.assessment.contextual.rulesReviewed === false; row.passed = row.missing.length === 0 && row.unsafe.length === 0 && row.unexpected.length === 0 && row.ruleCorrect && row.exportVerified; } catch (e) { row.error = e.message; } row.totalMs = Math.round(performance.now() - start); report.cases.push(row); console.log(JSON.stringify(row)); } report.passed = report.cases.filter((c) => c.passed).length; report.total = report.cases.length; report.required = corpus.reduce((n, c) => n + c.required.length, 0); report.matched = report.cases.reduce((n, c) => n + (c.matched ?? 0), 0); report.unsafeAccepted = report.cases.reduce( (n, c) => n + (c.unsafe?.length ?? 0), 0, ); report.completed = report.cases.filter((c) => c.exportVerified).length; const latencies = report.cases .filter((c) => c.exportVerified) .map((c) => c.totalMs) .sort((a, b) => a - b); report.medianMs = latencies.length ? latencies[Math.floor(latencies.length / 2)] : null; report.p95Ms = latencies.length ? latencies[Math.ceil(latencies.length * 0.95) - 1] : null; await fs.writeFile( process.argv[2] || "validation/corpus-latest.json", JSON.stringify(report, null, 2) + "\n", ); console.log( `Completed ${report.completed}/${report.total}; matched ${report.matched}/${report.required}; all assertions ${report.passed}/${report.total}`, ); process.exitCode = report.passed === report.total ? 0 : 1;