fix(documents): read a long document through the lab scan's read core

Extracting lab values from a stored document still sent the whole text
in one call. A multi-page report produced more JSON than one call's
output ceiling, the answer was cut mid-fact, the parse failed twice, and
the read came back as 422 "Couldn't read the document. Try a clearer
copy." The lab scan had already learned to read the same report a page
or a part at a time, but each surface carried its own copy of the read,
so the document path never got it. It also ran under the shared 60 s
timeout, dropped every fact past 120 (or every fact when one did not
match the schema), and charged a flat reservation.

The read now lives once, in lib/documents/ocr-read.ts and ocr-plan.ts:
the per-read limits, the corrective retry, the salvage of a cut
answer's complete rows, the wire re-check and budget reservation before
each read, and the planning of a PDF a page at a time and text in parts
at line boundaries. The lab scan and the document extraction (the
extract route, its background run and the lab auto-staging) both read
through it, and the background run plans its reads on the same deadline
as a lab scan. The per-read budget moves to a shared metered budget.

The document path validates the answer fact by fact, keeps the lab
scan's row cap (400), and says why nothing came back: 422
documents.inbound.outputLimit when the reads stopped at the output
limit, 429 when the budget refused them, extractFailed only for answers
that were not the schema. A run whose reads were cut or not reached
reports `incomplete` on its result.

A guard holds the class: the output ceiling is set only in the core,
both surfaces read through runOcrReads, both runs share the deadline,
and a lab read and a document read go out under the same limits.

Refs #1257
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 10:34:31 +02:00
1 parent 3d47c1e6b4
commit 5a7bd95c7b
31 files changed
+2333 -1034

No files matched your search

@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 354,
"total": 114410,
"count": 352,
"total": 113991,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
@@ -95,7 +95,7 @@
"components/documents/document-bulk-bar.tsx :: DocumentBulkBar": 204,
"components/documents/document-card.tsx :: DocumentCard": 354,
"components/documents/document-detail-sheet.tsx :: DocumentDetailSheet": 325,
"components/documents/document-facts-review.tsx :: DocumentFactsSection": 252,
"components/documents/document-facts-review.tsx :: DocumentFactsSection": 246,
"components/documents/document-facts-review.tsx :: FactRow": 262,
"components/documents/document-filter-bar.tsx :: DocumentFilterBar": 348,
"components/documents/document-record-links.tsx :: DocumentRecordLinks": 202,
@@ -323,8 +323,6 @@
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 611,
"lib/jobs/workout-insight-generate.ts :: runWorkoutInsightGenerate": 324,
"lib/labs/biomarker-merge.ts :: runMerge": 254,
"lib/labs/ocr-extract.ts :: runOcrExtraction": 207,
"lib/labs/ocr-run.ts :: executeOcrExtraction": 206,
"lib/measurements/consolidate-daily-mean.ts :: consolidateDailyMean": 278,
"lib/measurements/consolidate-legacy-steps.ts :: consolidateLegacySteps": 263,
"lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention": 537,