Merge v1.25.1: documents library backend — store-without-AI, optional extract, edit, list

# Conflicts:
#	docs/api/openapi.yaml
#	src/lib/documents/store.ts
#	src/lib/openapi/routes/documents.ts
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 23:08:54 +02:00
commit cdd66b6620
12 files changed
+1820 -472

No files matched your search

+262 -22
View File
@@ -8595,12 +8595,82 @@ paths:
get:
tags:
- Documents
summary: List inbound clinical documents
description: The caller's uploaded doctor reports / discharge letters (newest first, live only). Owner-scoped; gated on
the opt-in `inboundDocuments` module.
summary: List stored documents
description: The caller's stored documents with title/filename search, category filter, a `documentDate` range, sort,
and keyset pagination. Owner-scoped; gated on the opt-in `inboundDocuments` module.
parameters:
- name: q
in: query
required: false
schema:
type: string
maxLength: 100
description: Case-insensitive search over title + filename.
- name: kind
in: query
required: false
schema:
type: string
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
description: Category filter.
- name: from
in: query
required: false
schema:
type: string
format: date
description: Inclusive lower bound on documentDate (YYYY-MM-DD).
- name: to
in: query
required: false
schema:
type: string
format: date
description: Inclusive upper bound on documentDate (YYYY-MM-DD).
- name: sort
in: query
required: false
schema:
type: string
enum:
- documentDate
- createdAt
- title
default: documentDate
- name: order
in: query
required: false
schema:
type: string
enum:
- asc
- desc
default: desc
- name: cursor
in: query
required: false
schema:
type: string
description: Keyset cursor (the `nextCursor` from a prior page).
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 50
responses:
"200":
description: The document list.
description: A page of documents.
content:
application/json:
schema:
@@ -8611,14 +8681,12 @@ paths:
post:
tags:
- Documents
summary: Upload + extract a clinical document
description: 'Ingests a doctor report / discharge letter through the dedicated OCR/vision provider, stores the raw
document ENCRYPTED at rest, and stages the extracted STRUCTURED FACTS for review. Two modes by content-type.
VISION (`multipart/form-data`): a `file` (JPEG/PNG/WebP, or PDF on an Anthropic vision provider; ≤ 12 MiB) plus
an optional `kind`. TEXT (`application/json`, opt-in local OCR): `{ mode: "text", text, kind? }` — only the
extracted text reaches the server. Both modes are AI-consent / rate / budget gated. Extraction reproduces what
the document states (stated codes/status only) — it never interprets. Nothing reaches the structured stores
here; the confirm route is the only write path.'
summary: Store a document (no extraction)
description: "STORE-ONLY upload. Stores the raw document ENCRYPTED at rest with `status: STORED` and runs NO extraction
— provider-free, no AI consent / budget / egress. A file is always filable, even with no document-scan provider
configured. `multipart/form-data`: a `file` (JPEG/PNG/WebP/PDF, validated by magic-byte MIME sniff, ≤ 12 MiB)
plus optional `title`, `kind`, and `documentDate` (YYYY-MM-DD) form fields. AI extraction is a separate opt-in
action — see `POST /api/documents/inbound/{id}/extract`."
requestBody:
required: true
content:
@@ -8629,22 +8697,30 @@ paths:
file:
type: string
format: binary
description: The clinical-document image or PDF. Validated by magic-byte MIME sniff, not the wire Content-Type.
description: The document image or PDF. Validated by magic-byte MIME sniff, not the wire Content-Type.
title:
description: Optional user label.
type: string
kind:
description: Optional document-type label.
description: Optional category.
type: string
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
documentDate:
description: Optional user filing date (YYYY-MM-DD).
type: string
required:
- file
application/json:
schema:
$ref: "#/components/schemas/InboundTextUploadRequest"
responses:
"201":
description: The created document + staging summary.
description: The stored document.
content:
application/json:
schema:
@@ -8674,10 +8750,60 @@ paths:
"401": *a1
"422": *a3
"429": *a4
patch:
tags:
- Documents
summary: Edit a document's metadata
description: Rename / recategorise / set the user filing date on a stored document. Owner-scoped; no mass assignment.
Returns the updated document + its staged facts.
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
anyOf:
- type: string
maxLength: 200
- type: "null"
kind:
type: string
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
documentDate:
anyOf:
- type: string
pattern: ^\d{4}-\d{2}-\d{2}$
- type: "null"
responses:
"200":
description: The updated document.
content:
application/json:
schema:
$ref: "#/components/schemas/InboundDocumentUpdateEnvelope"
"401": *a1
"422": *a3
"429": *a4
delete:
tags:
- Documents
summary: Discard an inbound document
summary: Discard a document
description: Soft-deletes the document + its staging. Facts already approved into the structured stores are independent
rows and are NOT affected.
parameters:
@@ -8730,6 +8856,40 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/api/documents/inbound/{id}/extract:
post:
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."
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/InboundTextExtractRequest"
responses:
"200":
description: The document with its newly staged facts.
content:
application/json:
schema:
$ref: "#/components/schemas/InboundExtractEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/documents/inbound/{id}/facts/{factId}:
patch:
tags:
@@ -10663,7 +10823,7 @@ components:
required:
- mode
- text
InboundTextUploadRequest:
InboundTextExtractRequest:
type: object
properties:
mode:
@@ -10676,6 +10836,11 @@ components:
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
required:
- mode
@@ -24941,9 +25106,16 @@ components:
type: array
items:
$ref: "#/components/schemas/InboundDocument"
nextCursor:
anyOf:
- type: string
- type: "null"
required:
- documents
- nextCursor
additionalProperties: false
description: A page of documents plus an opaque `nextCursor` (the id to pass back as `cursor` for the next page; null
when the list is exhausted).
InboundDocument:
type: object
properties:
@@ -24954,7 +25126,16 @@ components:
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
title:
anyOf:
- type: string
- type: "null"
filename:
anyOf:
- type: string
@@ -24966,6 +25147,7 @@ components:
status:
type: string
enum:
- STORED
- EXTRACTING
- EXTRACTED
- FAILED
@@ -24979,6 +25161,10 @@ components:
anyOf:
- type: string
- type: "null"
documentDate:
anyOf:
- type: string
- type: "null"
errorReason:
anyOf:
- type: string
@@ -24994,20 +25180,24 @@ components:
required:
- id
- kind
- title
- filename
- mimeType
- byteSize
- status
- providerType
- reportDate
- documentDate
- errorReason
- factCount
- pendingCount
- createdAt
- updatedAt
additionalProperties: false
description: An uploaded clinical document. The raw bytes are stored encrypted at rest and never returned; this is the
metadata + staging summary.
description: A stored document. The raw bytes are stored encrypted at rest and never returned; this is the metadata +
staging summary. `status` is STORED for a freshly uploaded file (no extraction run). `title` is the user label
(plaintext); `documentDate` is the user filing date; `reportDate` is the model-transcribed date (null until
extraction runs).
InboundDocumentEnvelope:
type: object
properties:
@@ -25052,7 +25242,16 @@ components:
enum:
- DOCTOR_REPORT
- DISCHARGE_LETTER
- LAB_RESULT
- IMAGING
- PRESCRIPTION
- INSURANCE
- VACCINATION
- OTHER
title:
anyOf:
- type: string
- type: "null"
filename:
anyOf:
- type: string
@@ -25064,6 +25263,7 @@ components:
status:
type: string
enum:
- STORED
- EXTRACTING
- EXTRACTED
- FAILED
@@ -25077,6 +25277,10 @@ components:
anyOf:
- type: string
- type: "null"
documentDate:
anyOf:
- type: string
- type: "null"
errorReason:
anyOf:
- type: string
@@ -25096,12 +25300,14 @@ components:
required:
- id
- kind
- title
- filename
- mimeType
- byteSize
- status
- providerType
- reportDate
- documentDate
- errorReason
- factCount
- pendingCount
@@ -25174,6 +25380,23 @@ components:
with per-field provenance + a confidence gate. STATED status only — codes (SNOMED/ICD-10/LOINC/RxNorm/ATC) are
present only when the document wrote them; no range-flag, no inferred links. `needsReview` is true when the fact
scored below the confidence floor (it cannot be approved until edited).
InboundDocumentUpdateEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InboundDocumentDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InboundDiscardEnvelope:
type: object
properties:
@@ -25200,6 +25423,23 @@ components:
- data
- error
additionalProperties: false
InboundExtractEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InboundDocumentDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ExtractedFactEnvelope:
type: object
properties: