feat(documents): review and extract lab values on the document itself

The server chain existed end to end (extract stages PENDING facts, per-fact
edit clears the low-confidence gate, confirm is the only write into the
structured stores) but the document detail offered no control over any of
it: it counted observation facts and linked to Labs. A user whose automatic
staging run was skipped had to re-upload the same PDF through Labs > Scan a
report.

- The detail sheet gains a staged-facts block with honest states: pending
  facts open an inline review (approve/reject per fact; a low-confidence
  fact is checked and saved first via the existing per-fact edit); a
  lab-filed, already-read document with nothing staged offers "Extract lab
  values" over its stored text; otherwise nothing renders.
- The extract route gains { mode: "stored" }: the same text-structuring
  pass the automatic worker runs, over the document's own content index.
  No re-upload, no second read of the original; full gauntlet including
  the shared document-AI bucket with charge-at-dispatch honesty; 422
  notIndexed when no stored text exists.
- A manual "Read with AI" / "Read again" now continues into the same
  lab auto-staging the background worker performs; the response reports
  labFactsStaged and the toast names the values waiting for review.
- Unchecked values are rejected and the copy says so; an unverified
  low-confidence fact receives no decision and stays pending rather than
  being silently discarded. Confirmation remains the only write into
  Labs, conditions, and medications; approved facts carry the
  committedRecordId link back to the source document (existing model, no
  schema change).
- The "lab values from this document" link now counts APPROVED facts
  only — it claims values that actually exist in Labs.
- Copy in all six locales; OpenAPI regenerated for the new extract mode
  and the index response field.
This commit is contained in:
Marc-André Bombeck committed 2026-08-13 00:33:14 +02:00
1 parent 8e16df6894
commit fd104dd0ac
20 files changed
+1681 -64

No files matched your search

+18 -8
View File
@@ -720,7 +720,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Documents"],
summary: "Extract facts from a stored document",
description:
'Optional AI enhancement on an already-stored document. Runs the dedicated OCR/vision provider over the stored original and stages STRUCTURED FACTS for review. AI-consent / rate / budget gated. With no provider configured this returns 422 (`documents.inbound.providerUnsupported`) — the stored document is untouched, only the enhancement fails. Two modes by content-type: VISION (empty body) decrypts and scans the stored original (PDF needs an Anthropic vision provider); TEXT (`application/json`, opt-in local OCR) `{ mode: "text", text }` structures browser-OCR\'d text. Extraction reproduces what the document states — it never interprets. Nothing reaches the structured stores here; the confirm route is the only write path.',
'Optional AI enhancement on an already-stored document. Runs the dedicated OCR/vision provider over the stored original and stages STRUCTURED FACTS for review. AI-consent / rate / budget gated. With no provider configured this returns 422 (`documents.inbound.providerUnsupported`) — the stored document is untouched, only the enhancement fails. Three modes: VISION (empty body) decrypts and scans the stored original (PDF needs an Anthropic vision provider); TEXT (`application/json`, opt-in local OCR) `{ mode: "text", text }` structures browser-OCR\'d text; STORED (`application/json`) `{ mode: "stored" }` structures the document\'s own stored extracted text (its content index) — the manual recovery for a skipped/failed automatic staging run, 422 `documents.inbound.notIndexed` when no stored text exists. Extraction reproduces what the document states — it never interprets. Nothing reaches the structured stores here; the confirm route is the only write path.',
parameters: [
{
name: "id",
@@ -734,12 +734,17 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: z
.object({
mode: z.literal("text"),
text: z.string(),
kind: kindEnum.optional(),
})
.meta({ id: "InboundTextExtractRequest" }),
.union([
z.object({
mode: z.literal("text"),
text: z.string(),
kind: kindEnum.optional(),
}),
z.object({
mode: z.literal("stored"),
}),
])
.meta({ id: "InboundExtractRequest" }),
},
},
},
@@ -888,7 +893,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Documents"],
summary: "Build / refresh the content-search index",
description:
"Populates or refreshes one document's content-search index so search matches INSIDE its body. VISION (empty body) decrypts the stored original and runs one provider transcription (AI-consent / rate / budget gated); TEXT (`application/json` `{ mode: \"text\", text }`, opt-in local OCR) indexes browser-OCR'd text with no provider egress. Persists ONLY AES-256-GCM ciphertext of the text plus opaque HMAC token hashes — no plaintext body, no plaintext token. Gated on the existing AI consent (no separate per-user toggle). Idempotent; re-indexing overwrites in place.",
"Populates or refreshes one document's content-search index so search matches INSIDE its body. VISION (empty body) decrypts the stored original and runs one provider transcription (AI-consent / rate / budget gated); TEXT (`application/json` `{ mode: \"text\", text }`, opt-in local OCR) indexes browser-OCR'd text with no provider egress. Persists ONLY AES-256-GCM ciphertext of the text plus opaque HMAC token hashes — no plaintext body, no plaintext token. Gated on the existing AI consent (no separate per-user toggle). Idempotent; re-indexing overwrites in place. A successful index continues into the same lab auto-staging the background worker performs (still-STORED lab-looking document with no facts, both modules on, provider + consent eligible); `labFactsStaged` reports how many facts were staged PENDING for review — confirmation stays the only write into Labs.",
parameters: [
{ name: "id", in: "path", required: true, schema: { type: "string" } },
],
@@ -915,6 +920,11 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
documentId: z.string(),
indexed: z.boolean(),
tokenCount: z.number(),
labFactsStaged: z
.number()
.describe(
"How many lab facts the index run auto-staged for review (0 when the document is not a lab report, staging was not eligible, or facts already exist). Staged facts are PENDING — the confirm route remains the only write into Labs.",
),
})
.meta({ id: "DocumentIndexResponse" }),
"DocumentIndexEnvelope",