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;