From da4056ce0592a9cf7963706bc0a9261b86746edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 02:03:27 +0100 Subject: [PATCH] fix(documents): key an undated fact on its report's date in the merge The extraction merged facts across reads on a key whose only date was the fact's own effective date. Facts without one collapsed across reads of different reports, so the same value on two days staged once. An undated fact is now keyed on its read's report date, falling back to the document's, as the lab scan keys a row. Refs #1257 --- .../documents/__tests__/extract-reads.test.ts | 18 ++++++++++++++++++ src/lib/documents/extract.ts | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/lib/documents/__tests__/extract-reads.test.ts b/src/lib/documents/__tests__/extract-reads.test.ts index 37792100e..57fa54549 100644 --- a/src/lib/documents/__tests__/extract-reads.test.ts +++ b/src/lib/documents/__tests__/extract-reads.test.ts @@ -103,6 +103,24 @@ describe("runInboundExtraction over several reads", () => { expect(result.incomplete).toBe(false); }); + it("keeps a value two reads list for different report dates, and merges it on the same date", async () => { + const page = (reportDate: string | null) => ({ + content: JSON.stringify({ reportDate, facts: [observation("Hb", 14)] }), + }); + // Two reports of different days, one value each with no date of its own. + const twoDays = await run( + scripted([page("2026-09-01"), page("2026-09-30")]), + reads(2), + ); + expect(twoDays.facts).toHaveLength(2); + // A page without a date reads under the report's, as the lab scan does. + const sameDay = await run( + scripted([page("2026-09-01"), page(null)]), + reads(2), + ); + expect(sameDay.facts).toHaveLength(1); + }); + it("keeps the lab scan's row cap, and says when a report holds more", async () => { expect(INBOUND_MAX_FACTS).toBe(OCR_MAX_ROWS); const reply = (r: number) => ({ diff --git a/src/lib/documents/extract.ts b/src/lib/documents/extract.ts index cd821fd76..d48803484 100644 --- a/src/lib/documents/extract.ts +++ b/src/lib/documents/extract.ts @@ -293,8 +293,13 @@ function factPage(raw: ExtractedFactRaw, chunk: OcrChunk): number | null { : null; } -/** The identity of a fact for the merge: the same fact in two reads is one. */ -function factKey(raw: ExtractedFactRaw): string { +/** + * The identity of a fact for the merge: the same fact in two reads is one. + * A fact without a date of its own is keyed on the date of the report its + * read belongs to, as the lab scan keys a row, so the same value on two + * reports of different days stays two facts. + */ +function factKey(raw: ExtractedFactRaw, readDate: string | null): string { return [ raw.type, raw.label.trim().toLowerCase(), @@ -303,7 +308,7 @@ function factKey(raw: ExtractedFactRaw): string { (raw.unit ?? "").trim().toLowerCase(), (raw.referenceText ?? "").trim().toLowerCase(), (raw.dose ?? "").trim().toLowerCase(), - normaliseDate(raw.effectiveDate) ?? "", + normaliseDate(raw.effectiveDate) ?? readDate ?? "", ].join("|"); } @@ -345,9 +350,12 @@ export async function runInboundExtraction( let droppedFacts = 0; for (const { envelope, chunk } of reads.reads) { droppedFacts += envelope.droppedFacts; + // A read that prints its own date keeps it; the others read under the + // report's. + const readDate = normaliseDate(envelope.reportDate) ?? reportDate; const readKeys: string[] = []; for (const raw of envelope.facts) { - const key = factKey(raw); + const key = factKey(raw, readDate); if (seen.has(key)) { mergedDuplicates += 1; continue;