mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(documents): OpenAPI contract and translations for inbound documents
Document the /api/documents/inbound/* surface in the OpenAPI registry and regenerate the spec. Add the inbound-documents copy across all six locales, including the non-diagnostic disclaimer and the module label/description.
This commit is contained in:
1 parent
422778f679
commit
33bf952d9d
10 files changed
+1442
-6
No files matched your search
@@ -42,6 +42,7 @@ tags:
|
||||
- name: MeasurementReminders
|
||||
- name: Labs
|
||||
- name: Illness
|
||||
- name: Documents
|
||||
- name: Records
|
||||
- name: Admin
|
||||
- name: Meta
|
||||
@@ -8567,6 +8568,331 @@ paths:
|
||||
"404": *a13
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/documents/inbound:
|
||||
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.
|
||||
responses:
|
||||
"200":
|
||||
description: The document list.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundDocumentListEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
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.'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
description: The clinical-document image or PDF. Validated by magic-byte MIME sniff, not the wire Content-Type.
|
||||
kind:
|
||||
description: Optional document-type label.
|
||||
type: string
|
||||
enum:
|
||||
- DOCTOR_REPORT
|
||||
- DISCHARGE_LETTER
|
||||
- OTHER
|
||||
required:
|
||||
- file
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundTextUploadRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: The created document + staging summary.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundDocumentEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/documents/inbound/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Documents
|
||||
summary: Get a document + its staged facts
|
||||
description: The document plus every staged fact for the review screen. Owner-scoped.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Document detail.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundDocumentDetailEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
delete:
|
||||
tags:
|
||||
- Documents
|
||||
summary: Discard an inbound document
|
||||
description: Soft-deletes the document + its staging. Facts already approved into the structured stores are independent
|
||||
rows and are NOT affected.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Discarded.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundDiscardEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/documents/inbound/{id}/facts/{factId}:
|
||||
patch:
|
||||
tags:
|
||||
- Documents
|
||||
summary: Edit a staged fact before approval
|
||||
description: Corrects OCR / units / dates / codes on a pending fact. Clears `needsReview` (the values become
|
||||
user-asserted). The fact's resource type cannot change.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: factId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
factType:
|
||||
type: string
|
||||
const: CONDITION
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 300
|
||||
code:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 64
|
||||
- type: "null"
|
||||
codeSystem:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- SNOMED
|
||||
- ICD10
|
||||
- LOINC
|
||||
- RXNORM
|
||||
- ATC
|
||||
- type: "null"
|
||||
clinicalStatus:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 64
|
||||
- type: "null"
|
||||
verificationStatus:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 64
|
||||
- type: "null"
|
||||
onsetDate:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: ^\d{4}-\d{2}-\d{2}$
|
||||
- type: "null"
|
||||
required:
|
||||
- factType
|
||||
- label
|
||||
- type: object
|
||||
properties:
|
||||
factType:
|
||||
type: string
|
||||
const: OBSERVATION
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 300
|
||||
code:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 64
|
||||
- type: "null"
|
||||
codeSystem:
|
||||
anyOf:
|
||||
- type: string
|
||||
enum:
|
||||
- SNOMED
|
||||
- ICD10
|
||||
- LOINC
|
||||
- RXNORM
|
||||
- ATC
|
||||
- type: "null"
|
||||
value:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
valueText:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 300
|
||||
- type: "null"
|
||||
unit:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 80
|
||||
- type: "null"
|
||||
referenceLow:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
referenceHigh:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
effectiveDate:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: ^\d{4}-\d{2}-\d{2}$
|
||||
- type: "null"
|
||||
required:
|
||||
- factType
|
||||
- label
|
||||
- type: object
|
||||
properties:
|
||||
factType:
|
||||
type: string
|
||||
const: MEDICATION_STATEMENT
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 200
|
||||
dose:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 120
|
||||
- type: "null"
|
||||
rxNormCode:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 20
|
||||
- type: "null"
|
||||
atcCode:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 16
|
||||
- type: "null"
|
||||
statusStated:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 64
|
||||
- type: "null"
|
||||
effectiveDate:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: ^\d{4}-\d{2}-\d{2}$
|
||||
- type: "null"
|
||||
required:
|
||||
- factType
|
||||
- name
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
description: The updated fact.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExtractedFactEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/documents/inbound/{id}/confirm:
|
||||
post:
|
||||
tags:
|
||||
- Documents
|
||||
summary: Confirm the review decisions
|
||||
description: The ONLY write path out of staging. Commits the user's approvals into the structured stores (labs /
|
||||
conditions / medications) and discards rejections. A low-confidence fact that was never edited cannot be
|
||||
approved (fail-closed). Idempotent (Idempotency-Key). Audits as `documents.inbound.confirm`.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
decisions:
|
||||
minItems: 1
|
||||
maxItems: 120
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
factId:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 40
|
||||
action:
|
||||
type: string
|
||||
enum:
|
||||
- approve
|
||||
- reject
|
||||
required:
|
||||
- factId
|
||||
- action
|
||||
required:
|
||||
- decisions
|
||||
responses:
|
||||
"200":
|
||||
description: Per-decision outcome.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InboundConfirmEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/onboarding/disclaimer:
|
||||
post:
|
||||
tags:
|
||||
@@ -9739,6 +10065,8 @@ components:
|
||||
type: boolean
|
||||
mcp:
|
||||
type: boolean
|
||||
inboundDocuments:
|
||||
type: boolean
|
||||
additionalProperties: false
|
||||
description: Partial module enable/disable update. Each key is an optional boolean; an omitted key is left untouched,
|
||||
`false` disables the module, `true` (re-)enables it. Only the toggleable "secondary domains" are accepted — a
|
||||
@@ -10268,6 +10596,23 @@ components:
|
||||
required:
|
||||
- mode
|
||||
- text
|
||||
InboundTextUploadRequest:
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
const: text
|
||||
text:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
- DOCTOR_REPORT
|
||||
- DISCHARGE_LETTER
|
||||
- OTHER
|
||||
required:
|
||||
- mode
|
||||
- text
|
||||
DisclaimerAckRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -16778,6 +17123,9 @@ components:
|
||||
mcp:
|
||||
type: boolean
|
||||
description: Whether the "mcp" module is enabled for this account.
|
||||
inboundDocuments:
|
||||
type: boolean
|
||||
description: Whether the "inboundDocuments" module is enabled for this account.
|
||||
required:
|
||||
- cycle
|
||||
- mood
|
||||
@@ -16793,6 +17141,7 @@ components:
|
||||
- medications
|
||||
- doctorReport
|
||||
- mcp
|
||||
- inboundDocuments
|
||||
additionalProperties: false
|
||||
description: Fully-resolved per-user module enable/disable map. Every toggleable module key is present. `false` means
|
||||
the surface should disappear end-to-end (nav, dashboard, insights, …). `cycle` mirrors `cycleTrackingEnabled`;
|
||||
@@ -24493,6 +24842,372 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
InboundDocumentListEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/InboundDocumentList"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
InboundDocumentList:
|
||||
type: object
|
||||
properties:
|
||||
documents:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InboundDocument"
|
||||
required:
|
||||
- documents
|
||||
additionalProperties: false
|
||||
InboundDocument:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
- DOCTOR_REPORT
|
||||
- DISCHARGE_LETTER
|
||||
- OTHER
|
||||
filename:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
mimeType:
|
||||
type: string
|
||||
byteSize:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- EXTRACTING
|
||||
- EXTRACTED
|
||||
- FAILED
|
||||
- CONFIRMED
|
||||
- DISCARDED
|
||||
providerType:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
reportDate:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
errorReason:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
factCount:
|
||||
type: number
|
||||
pendingCount:
|
||||
type: number
|
||||
createdAt:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- kind
|
||||
- filename
|
||||
- mimeType
|
||||
- byteSize
|
||||
- status
|
||||
- providerType
|
||||
- reportDate
|
||||
- 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.
|
||||
InboundDocumentEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/InboundDocument"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
InboundDocumentDetailEnvelope:
|
||||
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
|
||||
InboundDocumentDetail:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
- DOCTOR_REPORT
|
||||
- DISCHARGE_LETTER
|
||||
- OTHER
|
||||
filename:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
mimeType:
|
||||
type: string
|
||||
byteSize:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- EXTRACTING
|
||||
- EXTRACTED
|
||||
- FAILED
|
||||
- CONFIRMED
|
||||
- DISCARDED
|
||||
providerType:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
reportDate:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
errorReason:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
factCount:
|
||||
type: number
|
||||
pendingCount:
|
||||
type: number
|
||||
createdAt:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
facts:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ExtractedFact"
|
||||
required:
|
||||
- id
|
||||
- kind
|
||||
- filename
|
||||
- mimeType
|
||||
- byteSize
|
||||
- status
|
||||
- providerType
|
||||
- reportDate
|
||||
- errorReason
|
||||
- factCount
|
||||
- pendingCount
|
||||
- createdAt
|
||||
- updatedAt
|
||||
- facts
|
||||
additionalProperties: false
|
||||
ExtractedFact:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
factType:
|
||||
type: string
|
||||
enum:
|
||||
- CONDITION
|
||||
- OBSERVATION
|
||||
- MEDICATION_STATEMENT
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- PENDING
|
||||
- APPROVED
|
||||
- REJECTED
|
||||
confidence:
|
||||
type: number
|
||||
needsReview:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
propertyNames:
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
provenance:
|
||||
type: object
|
||||
properties:
|
||||
sourceText:
|
||||
type: string
|
||||
page:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
confidence:
|
||||
type: number
|
||||
required:
|
||||
- sourceText
|
||||
- page
|
||||
- confidence
|
||||
additionalProperties: false
|
||||
committedRecordId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
committedRecordType:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
required:
|
||||
- id
|
||||
- factType
|
||||
- status
|
||||
- confidence
|
||||
- needsReview
|
||||
- data
|
||||
- provenance
|
||||
- committedRecordId
|
||||
- committedRecordType
|
||||
additionalProperties: false
|
||||
description: One STATED fact transcribed from the document, FHIR-staged (Condition / Observation / MedicationStatement)
|
||||
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).
|
||||
InboundDiscardEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
discarded:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- discarded
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ExtractedFactEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/ExtractedFact"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
InboundConfirmEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/InboundConfirmResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
InboundConfirmResponse:
|
||||
type: object
|
||||
properties:
|
||||
approved:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
factId:
|
||||
type: string
|
||||
recordType:
|
||||
type: string
|
||||
recordId:
|
||||
type: string
|
||||
required:
|
||||
- factId
|
||||
- recordType
|
||||
- recordId
|
||||
additionalProperties: false
|
||||
rejected:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
needsReview:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
failed:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
factId:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
required:
|
||||
- factId
|
||||
- reason
|
||||
additionalProperties: false
|
||||
required:
|
||||
- approved
|
||||
- rejected
|
||||
- needsReview
|
||||
- failed
|
||||
additionalProperties: false
|
||||
description: "The outcome per decision: `approved` (committed, with the new record ref), `rejected` (discarded),
|
||||
`needsReview` (refused — edit first, fail-closed), `failed` (a per-fact commit miss, e.g. a numeric observation
|
||||
with no unit)."
|
||||
DisclaimerAckEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Zyklus",
|
||||
"labs": "Laborwerte",
|
||||
"vorsorge": "Vorsorge",
|
||||
"illness": "Krankheit"
|
||||
"illness": "Krankheit",
|
||||
"documents": "Dokumente"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Anmelden",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "Externer Assistentenzugriff (MCP)",
|
||||
"description": "Einem MCP-fähigen Assistenten Lesezugriff auf deine Gesundheitsdaten über ein begrenztes Token erlauben. Standardmäßig aus."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Eingehende Dokumente",
|
||||
"description": "Lade einen Arztbericht oder Entlassbrief hoch; prüfe die erkannten Angaben, bevor sie gespeichert werden. Standardmäßig aus."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "Der Eintrag konnte nicht gespeichert werden."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Eingehende Dokumente",
|
||||
"subtitle": "Lade ein klinisches Dokument hoch und prüfe die Angaben, die wir daraus gelesen haben.",
|
||||
"disclaimer": {
|
||||
"title": "Wir lesen, wir deuten nicht",
|
||||
"body": "Wir übernehmen nur das, was dein Dokument angibt — Werte, Codes und Daten — und stellen keine Diagnose, deuten nichts und bewerten nichts. Nichts wird gespeichert, bevor du es geprüft und bestätigt hast. Nur deine Ärztin oder dein Arzt kann diese Ergebnisse beurteilen."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Dokument hochladen",
|
||||
"kindLabel": "Dokumenttyp",
|
||||
"button": "Hochladen",
|
||||
"uploading": "Wird gelesen …"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Arztbericht",
|
||||
"DISCHARGE_LETTER": "Entlassbrief",
|
||||
"OTHER": "Sonstiges"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Noch keine Dokumente."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Wird gelesen",
|
||||
"EXTRACTED": "Bereit zur Prüfung",
|
||||
"FAILED": "Konnte nicht gelesen werden",
|
||||
"CONFIRMED": "Geprüft",
|
||||
"DISCARDED": "Verworfen"
|
||||
},
|
||||
"review": {
|
||||
"title": "Erkannte Angaben prüfen",
|
||||
"discard": "Verwerfen",
|
||||
"confirm": "Bestätigte Angaben speichern",
|
||||
"confirming": "Wird gespeichert …",
|
||||
"needsReview": "Prüfung nötig",
|
||||
"approve": "Übernehmen",
|
||||
"reject": "Ablehnen",
|
||||
"edit": "Bearbeiten",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Nichts mehr zu prüfen."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnose",
|
||||
"OBSERVATION": "Wert",
|
||||
"MEDICATION_STATEMENT": "Medikament"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Name",
|
||||
"value": "Wert",
|
||||
"unit": "Einheit",
|
||||
"date": "Datum",
|
||||
"dose": "Dosis"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Dokument gelesen. Prüfe die Angaben unten.",
|
||||
"uploadFailed": "Dokument konnte nicht gelesen werden.",
|
||||
"confirmed": "Bestätigte Angaben gespeichert.",
|
||||
"confirmFailed": "Deine Entscheidungen konnten nicht gespeichert werden.",
|
||||
"saved": "Angabe aktualisiert.",
|
||||
"discarded": "Dokument verworfen."
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Cycle",
|
||||
"labs": "Labs",
|
||||
"vorsorge": "Checkups",
|
||||
"illness": "Illness"
|
||||
"illness": "Illness",
|
||||
"documents": "Documents"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Sign in",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "External assistant access (MCP)",
|
||||
"description": "Let an MCP-capable assistant read your health data over a scoped token. Off by default."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Inbound documents",
|
||||
"description": "Upload a doctor report or discharge letter; review the extracted facts before they're saved. Off by default."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "Couldn't save the entry."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Inbound documents",
|
||||
"subtitle": "Upload a clinical document and review the facts we read from it.",
|
||||
"disclaimer": {
|
||||
"title": "We read, we don't interpret",
|
||||
"body": "We only transcribe what your document states — values, codes, and dates — and never diagnose, interpret, or flag anything. Nothing is saved until you review and approve it. Only your clinician can interpret these results."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Upload a document",
|
||||
"kindLabel": "Document type",
|
||||
"button": "Upload",
|
||||
"uploading": "Reading…"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Doctor report",
|
||||
"DISCHARGE_LETTER": "Discharge letter",
|
||||
"OTHER": "Other"
|
||||
},
|
||||
"list": {
|
||||
"empty": "No documents yet."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Reading",
|
||||
"EXTRACTED": "Ready to review",
|
||||
"FAILED": "Couldn't read",
|
||||
"CONFIRMED": "Reviewed",
|
||||
"DISCARDED": "Discarded"
|
||||
},
|
||||
"review": {
|
||||
"title": "Review extracted facts",
|
||||
"discard": "Discard",
|
||||
"confirm": "Save approved facts",
|
||||
"confirming": "Saving…",
|
||||
"needsReview": "Needs review",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"edit": "Edit",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Nothing left to review."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnosis",
|
||||
"OBSERVATION": "Value",
|
||||
"MEDICATION_STATEMENT": "Medication"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Name",
|
||||
"value": "Value",
|
||||
"unit": "Unit",
|
||||
"date": "Date",
|
||||
"dose": "Dose"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Document read. Review the facts below.",
|
||||
"uploadFailed": "Couldn't read the document.",
|
||||
"confirmed": "Approved facts saved.",
|
||||
"confirmFailed": "Couldn't save your decisions.",
|
||||
"saved": "Fact updated.",
|
||||
"discarded": "Document discarded."
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Ciclo",
|
||||
"labs": "Análisis",
|
||||
"vorsorge": "Revisiones",
|
||||
"illness": "Enfermedad"
|
||||
"illness": "Enfermedad",
|
||||
"documents": "Documentos"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Iniciar sesión",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "Acceso de asistente externo (MCP)",
|
||||
"description": "Permite que un asistente compatible con MCP lea tus datos de salud mediante un token con alcance limitado. Desactivado de forma predeterminada."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Documentos entrantes",
|
||||
"description": "Sube un informe médico o un alta; revisa los datos extraídos antes de guardarlos. Desactivado de forma predeterminada."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "No se ha podido guardar la entrada."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documentos entrantes",
|
||||
"subtitle": "Sube un documento clínico y revisa los datos que hemos leído.",
|
||||
"disclaimer": {
|
||||
"title": "Leemos, no interpretamos",
|
||||
"body": "Solo transcribimos lo que indica tu documento — valores, códigos y fechas — y nunca diagnosticamos, interpretamos ni marcamos nada. No se guarda nada hasta que lo revises y lo apruebes. Solo tu profesional sanitario puede interpretar estos resultados."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Subir un documento",
|
||||
"kindLabel": "Tipo de documento",
|
||||
"button": "Subir",
|
||||
"uploading": "Leyendo…"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Informe médico",
|
||||
"DISCHARGE_LETTER": "Alta hospitalaria",
|
||||
"OTHER": "Otro"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Aún no hay documentos."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Leyendo",
|
||||
"EXTRACTED": "Listo para revisar",
|
||||
"FAILED": "No se pudo leer",
|
||||
"CONFIRMED": "Revisado",
|
||||
"DISCARDED": "Descartado"
|
||||
},
|
||||
"review": {
|
||||
"title": "Revisar datos extraídos",
|
||||
"discard": "Descartar",
|
||||
"confirm": "Guardar datos aprobados",
|
||||
"confirming": "Guardando…",
|
||||
"needsReview": "Requiere revisión",
|
||||
"approve": "Aprobar",
|
||||
"reject": "Rechazar",
|
||||
"edit": "Editar",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Nada más que revisar."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnóstico",
|
||||
"OBSERVATION": "Valor",
|
||||
"MEDICATION_STATEMENT": "Medicación"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Nombre",
|
||||
"value": "Valor",
|
||||
"unit": "Unidad",
|
||||
"date": "Fecha",
|
||||
"dose": "Dosis"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Documento leído. Revisa los datos a continuación.",
|
||||
"uploadFailed": "No se pudo leer el documento.",
|
||||
"confirmed": "Datos aprobados guardados.",
|
||||
"confirmFailed": "No se pudieron guardar tus decisiones.",
|
||||
"saved": "Dato actualizado.",
|
||||
"discarded": "Documento descartado."
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Cycle",
|
||||
"labs": "Analyses",
|
||||
"vorsorge": "Examens",
|
||||
"illness": "Maladie"
|
||||
"illness": "Maladie",
|
||||
"documents": "Documents"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Se connecter",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "Accès assistant externe (MCP)",
|
||||
"description": "Autorise un assistant compatible MCP à lire vos données de santé via un jeton à portée limitée. Désactivé par défaut."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Documents entrants",
|
||||
"description": "Importez un compte rendu médical ou une lettre de sortie ; vérifiez les données extraites avant de les enregistrer. Désactivé par défaut."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "Impossible d'enregistrer l'entrée."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documents entrants",
|
||||
"subtitle": "Importez un document clinique et vérifiez les données que nous y avons lues.",
|
||||
"disclaimer": {
|
||||
"title": "Nous lisons, nous n'interprétons pas",
|
||||
"body": "Nous transcrivons uniquement ce qu'indique votre document — valeurs, codes et dates — et nous ne posons aucun diagnostic, n'interprétons et ne signalons rien. Rien n'est enregistré tant que vous ne l'avez pas vérifié et approuvé. Seul votre professionnel de santé peut interpréter ces résultats."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Importer un document",
|
||||
"kindLabel": "Type de document",
|
||||
"button": "Importer",
|
||||
"uploading": "Lecture…"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Compte rendu médical",
|
||||
"DISCHARGE_LETTER": "Lettre de sortie",
|
||||
"OTHER": "Autre"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Aucun document pour l'instant."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Lecture",
|
||||
"EXTRACTED": "Prêt à vérifier",
|
||||
"FAILED": "Lecture impossible",
|
||||
"CONFIRMED": "Vérifié",
|
||||
"DISCARDED": "Écarté"
|
||||
},
|
||||
"review": {
|
||||
"title": "Vérifier les données extraites",
|
||||
"discard": "Écarter",
|
||||
"confirm": "Enregistrer les données approuvées",
|
||||
"confirming": "Enregistrement…",
|
||||
"needsReview": "À vérifier",
|
||||
"approve": "Approuver",
|
||||
"reject": "Rejeter",
|
||||
"edit": "Modifier",
|
||||
"cancel": "Annuler",
|
||||
"save": "Enregistrer"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Plus rien à vérifier."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnostic",
|
||||
"OBSERVATION": "Valeur",
|
||||
"MEDICATION_STATEMENT": "Médicament"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Nom",
|
||||
"value": "Valeur",
|
||||
"unit": "Unité",
|
||||
"date": "Date",
|
||||
"dose": "Dose"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Document lu. Vérifiez les données ci-dessous.",
|
||||
"uploadFailed": "Lecture du document impossible.",
|
||||
"confirmed": "Données approuvées enregistrées.",
|
||||
"confirmFailed": "Impossible d'enregistrer vos décisions.",
|
||||
"saved": "Donnée mise à jour.",
|
||||
"discarded": "Document écarté."
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Ciclo",
|
||||
"labs": "Esami",
|
||||
"vorsorge": "Controlli",
|
||||
"illness": "Malattia"
|
||||
"illness": "Malattia",
|
||||
"documents": "Documenti"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Accedi",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "Accesso assistente esterno (MCP)",
|
||||
"description": "Consente a un assistente compatibile con MCP di leggere i tuoi dati sanitari tramite un token con ambito limitato. Disattivato per impostazione predefinita."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Documenti in entrata",
|
||||
"description": "Carica un referto medico o una lettera di dimissione; rivedi i dati estratti prima di salvarli. Disattivato per impostazione predefinita."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "Impossibile salvare la voce."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documenti in entrata",
|
||||
"subtitle": "Carica un documento clinico e rivedi i dati che abbiamo letto.",
|
||||
"disclaimer": {
|
||||
"title": "Leggiamo, non interpretiamo",
|
||||
"body": "Trascriviamo solo ciò che il documento indica — valori, codici e date — e non formuliamo diagnosi, non interpretiamo e non segnaliamo nulla. Niente viene salvato finché non lo verifichi e lo approvi. Solo il tuo medico può interpretare questi risultati."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Carica un documento",
|
||||
"kindLabel": "Tipo di documento",
|
||||
"button": "Carica",
|
||||
"uploading": "Lettura…"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Referto medico",
|
||||
"DISCHARGE_LETTER": "Lettera di dimissione",
|
||||
"OTHER": "Altro"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Ancora nessun documento."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Lettura",
|
||||
"EXTRACTED": "Pronto per la revisione",
|
||||
"FAILED": "Impossibile leggere",
|
||||
"CONFIRMED": "Rivisto",
|
||||
"DISCARDED": "Scartato"
|
||||
},
|
||||
"review": {
|
||||
"title": "Rivedi i dati estratti",
|
||||
"discard": "Scarta",
|
||||
"confirm": "Salva i dati approvati",
|
||||
"confirming": "Salvataggio…",
|
||||
"needsReview": "Da rivedere",
|
||||
"approve": "Approva",
|
||||
"reject": "Rifiuta",
|
||||
"edit": "Modifica",
|
||||
"cancel": "Annulla",
|
||||
"save": "Salva"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Niente altro da rivedere."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnosi",
|
||||
"OBSERVATION": "Valore",
|
||||
"MEDICATION_STATEMENT": "Farmaco"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Nome",
|
||||
"value": "Valore",
|
||||
"unit": "Unità",
|
||||
"date": "Data",
|
||||
"dose": "Dose"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Documento letto. Rivedi i dati qui sotto.",
|
||||
"uploadFailed": "Impossibile leggere il documento.",
|
||||
"confirmed": "Dati approvati salvati.",
|
||||
"confirmFailed": "Impossibile salvare le tue scelte.",
|
||||
"saved": "Dato aggiornato.",
|
||||
"discarded": "Documento scartato."
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -239,7 +239,8 @@
|
||||
"cycle": "Cykl",
|
||||
"labs": "Wyniki badań",
|
||||
"vorsorge": "Badania",
|
||||
"illness": "Choroba"
|
||||
"illness": "Choroba",
|
||||
"documents": "Dokumenty"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Zaloguj się",
|
||||
@@ -7220,6 +7221,10 @@
|
||||
"mcp": {
|
||||
"label": "Dostęp asystenta zewnętrznego (MCP)",
|
||||
"description": "Pozwól asystentowi obsługującemu MCP czytać Twoje dane zdrowotne za pomocą tokena o ograniczonym zakresie. Domyślnie wyłączone."
|
||||
},
|
||||
"inboundDocuments": {
|
||||
"label": "Dokumenty przychodzące",
|
||||
"description": "Prześlij raport lekarski lub wypis; sprawdź wyodrębnione dane przed zapisaniem. Domyślnie wyłączone."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
@@ -7671,5 +7676,69 @@
|
||||
"saveError": "Nie udało się zapisać wpisu."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Dokumenty przychodzące",
|
||||
"subtitle": "Prześlij dokument kliniczny i sprawdź dane, które z niego odczytaliśmy.",
|
||||
"disclaimer": {
|
||||
"title": "Odczytujemy, nie interpretujemy",
|
||||
"body": "Przepisujemy tylko to, co podaje dokument — wartości, kody i daty — i nigdy nie stawiamy diagnozy, nie interpretujemy ani niczego nie oznaczamy. Nic nie zostaje zapisane, dopóki tego nie sprawdzisz i nie zatwierdzisz. Tylko lekarz może zinterpretować te wyniki."
|
||||
},
|
||||
"upload": {
|
||||
"label": "Prześlij dokument",
|
||||
"kindLabel": "Typ dokumentu",
|
||||
"button": "Prześlij",
|
||||
"uploading": "Odczytywanie…"
|
||||
},
|
||||
"kind": {
|
||||
"DOCTOR_REPORT": "Raport lekarski",
|
||||
"DISCHARGE_LETTER": "Wypis",
|
||||
"OTHER": "Inny"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Brak dokumentów."
|
||||
},
|
||||
"status": {
|
||||
"EXTRACTING": "Odczytywanie",
|
||||
"EXTRACTED": "Gotowe do przeglądu",
|
||||
"FAILED": "Nie udało się odczytać",
|
||||
"CONFIRMED": "Sprawdzone",
|
||||
"DISCARDED": "Odrzucone"
|
||||
},
|
||||
"review": {
|
||||
"title": "Przejrzyj wyodrębnione dane",
|
||||
"discard": "Odrzuć",
|
||||
"confirm": "Zapisz zatwierdzone dane",
|
||||
"confirming": "Zapisywanie…",
|
||||
"needsReview": "Wymaga przeglądu",
|
||||
"approve": "Zatwierdź",
|
||||
"reject": "Odrzuć",
|
||||
"edit": "Edytuj",
|
||||
"cancel": "Anuluj",
|
||||
"save": "Zapisz"
|
||||
},
|
||||
"empty": {
|
||||
"noFacts": "Nic więcej do przejrzenia."
|
||||
},
|
||||
"factType": {
|
||||
"CONDITION": "Diagnoza",
|
||||
"OBSERVATION": "Wartość",
|
||||
"MEDICATION_STATEMENT": "Lek"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Nazwa",
|
||||
"value": "Wartość",
|
||||
"unit": "Jednostka",
|
||||
"date": "Data",
|
||||
"dose": "Dawka"
|
||||
},
|
||||
"toast": {
|
||||
"uploaded": "Dokument odczytany. Sprawdź dane poniżej.",
|
||||
"uploadFailed": "Nie udało się odczytać dokumentu.",
|
||||
"confirmed": "Zatwierdzone dane zapisane.",
|
||||
"confirmFailed": "Nie udało się zapisać Twoich decyzji.",
|
||||
"saved": "Dane zaktualizowane.",
|
||||
"discarded": "Dokument odrzucony."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ const openApiBase: Pick<
|
||||
{ name: "MeasurementReminders" },
|
||||
{ name: "Labs" },
|
||||
{ name: "Illness" },
|
||||
{ name: "Documents" },
|
||||
{ name: "Records" },
|
||||
{ name: "Admin" },
|
||||
{ name: "Meta" },
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* OpenAPI route table for inbound clinical documents (`/api/documents/inbound/*`).
|
||||
*
|
||||
* Part of the OpenAPI route table; aggregated in `./index.ts`. The confirm +
|
||||
* edit request bodies reuse the runtime schemas from
|
||||
* `@/lib/validations/inbound-documents` so the wire contract stays
|
||||
* single-source. The upload is a `multipart/form-data` binary body (or a JSON
|
||||
* text-mode body); the response shapes are declared here.
|
||||
*
|
||||
* Safety contract surfaced in the descriptions: extraction reproduces what the
|
||||
* document states (stated codes/status only) into a staging area — it never
|
||||
* interprets or diagnoses. Nothing reaches the structured stores until the
|
||||
* confirm route commits the user's approvals; a low-confidence fact fails
|
||||
* closed.
|
||||
*/
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
inboundConfirmSchema,
|
||||
inboundFactEditSchema,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
|
||||
import { dataEnvelope, stdResponses } from "./shared";
|
||||
|
||||
inboundConfirmSchema.meta({
|
||||
id: "InboundConfirmRequest",
|
||||
description:
|
||||
"The approve/reject decisions a human made on the review screen. Each names a staged fact by id. Approved facts are committed to the structured stores (labs / conditions / medications) through their normal create paths; rejected facts are discarded. A fact still flagged `needsReview` (below the confidence floor, never edited) cannot be approved — it is reported back as `needsReview`. No `userId` field — it is narrowed from the session; every fact id is re-scoped to the document + caller.",
|
||||
});
|
||||
|
||||
inboundFactEditSchema.meta({
|
||||
id: "InboundFactEditRequest",
|
||||
description:
|
||||
"A correction to a staged fact before approval (fixes OCR / units / dates / codes). Discriminated by `factType` so the edit cannot change the fact's resource type. A successful edit clears `needsReview` — the values become user-asserted.",
|
||||
});
|
||||
|
||||
const factProvenance = z.object({
|
||||
sourceText: z.string(),
|
||||
page: z.number().nullable(),
|
||||
confidence: z.number(),
|
||||
});
|
||||
|
||||
const extractedFact = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
factType: z.enum(["CONDITION", "OBSERVATION", "MEDICATION_STATEMENT"]),
|
||||
status: z.enum(["PENDING", "APPROVED", "REJECTED"]),
|
||||
confidence: z.number(),
|
||||
needsReview: z.boolean(),
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
provenance: factProvenance,
|
||||
committedRecordId: z.string().nullable(),
|
||||
committedRecordType: z.string().nullable(),
|
||||
})
|
||||
.meta({
|
||||
id: "ExtractedFact",
|
||||
description:
|
||||
"One STATED fact transcribed from the document, FHIR-staged (Condition / Observation / MedicationStatement) 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).",
|
||||
});
|
||||
|
||||
const inboundDocument = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(["DOCTOR_REPORT", "DISCHARGE_LETTER", "OTHER"]),
|
||||
filename: z.string().nullable(),
|
||||
mimeType: z.string(),
|
||||
byteSize: z.number(),
|
||||
status: z.enum([
|
||||
"EXTRACTING",
|
||||
"EXTRACTED",
|
||||
"FAILED",
|
||||
"CONFIRMED",
|
||||
"DISCARDED",
|
||||
]),
|
||||
providerType: z.string().nullable(),
|
||||
reportDate: z.string().nullable(),
|
||||
errorReason: z.string().nullable(),
|
||||
factCount: z.number(),
|
||||
pendingCount: z.number(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.meta({
|
||||
id: "InboundDocument",
|
||||
description:
|
||||
"An uploaded clinical document. The raw bytes are stored encrypted at rest and never returned; this is the metadata + staging summary.",
|
||||
});
|
||||
|
||||
const inboundDocumentDetail = inboundDocument
|
||||
.extend({ facts: z.array(extractedFact) })
|
||||
.meta({ id: "InboundDocumentDetail" });
|
||||
|
||||
const listResponse = z
|
||||
.object({ documents: z.array(inboundDocument) })
|
||||
.meta({ id: "InboundDocumentList" });
|
||||
|
||||
const confirmResponse = z
|
||||
.object({
|
||||
approved: z.array(
|
||||
z.object({
|
||||
factId: z.string(),
|
||||
recordType: z.string(),
|
||||
recordId: z.string(),
|
||||
}),
|
||||
),
|
||||
rejected: z.array(z.string()),
|
||||
needsReview: z.array(z.string()),
|
||||
failed: z.array(z.object({ factId: z.string(), reason: z.string() })),
|
||||
})
|
||||
.meta({
|
||||
id: "InboundConfirmResponse",
|
||||
description:
|
||||
"The outcome per decision: `approved` (committed, with the new record ref), `rejected` (discarded), `needsReview` (refused — edit first, fail-closed), `failed` (a per-fact commit miss, e.g. a numeric observation with no unit).",
|
||||
});
|
||||
|
||||
export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"/api/documents/inbound": {
|
||||
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.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The document list.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(listResponse, "InboundDocumentListEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
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.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: z.object({
|
||||
file: z.string().meta({
|
||||
format: "binary",
|
||||
description:
|
||||
"The clinical-document image or PDF. Validated by magic-byte MIME sniff, not the wire Content-Type.",
|
||||
}),
|
||||
kind: z
|
||||
.enum(["DOCTOR_REPORT", "DISCHARGE_LETTER", "OTHER"])
|
||||
.optional()
|
||||
.meta({ description: "Optional document-type label." }),
|
||||
}),
|
||||
},
|
||||
"application/json": {
|
||||
schema: z
|
||||
.object({
|
||||
mode: z.literal("text"),
|
||||
text: z.string(),
|
||||
kind: z
|
||||
.enum(["DOCTOR_REPORT", "DISCHARGE_LETTER", "OTHER"])
|
||||
.optional(),
|
||||
})
|
||||
.meta({ id: "InboundTextUploadRequest" }),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"201": {
|
||||
description: "The created document + staging summary.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(inboundDocument, "InboundDocumentEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/inbound/{id}": {
|
||||
get: {
|
||||
tags: ["Documents"],
|
||||
summary: "Get a document + its staged facts",
|
||||
description:
|
||||
"The document plus every staged fact for the review screen. Owner-scoped.",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Document detail.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
inboundDocumentDetail,
|
||||
"InboundDocumentDetailEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ["Documents"],
|
||||
summary: "Discard an inbound document",
|
||||
description:
|
||||
"Soft-deletes the document + its staging. Facts already approved into the structured stores are independent rows and are NOT affected.",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Discarded.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
z.object({ id: z.string(), discarded: z.boolean() }),
|
||||
"InboundDiscardEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/inbound/{id}/facts/{factId}": {
|
||||
patch: {
|
||||
tags: ["Documents"],
|
||||
summary: "Edit a staged fact before approval",
|
||||
description:
|
||||
"Corrects OCR / units / dates / codes on a pending fact. Clears `needsReview` (the values become user-asserted). The fact's resource type cannot change.",
|
||||
parameters: [
|
||||
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
||||
{
|
||||
name: "factId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: inboundFactEditSchema } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The updated fact.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(extractedFact, "ExtractedFactEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/inbound/{id}/confirm": {
|
||||
post: {
|
||||
tags: ["Documents"],
|
||||
summary: "Confirm the review decisions",
|
||||
description:
|
||||
"The ONLY write path out of staging. Commits the user's approvals into the structured stores (labs / conditions / medications) and discards rejections. A low-confidence fact that was never edited cannot be approved (fail-closed). Idempotent (Idempotency-Key). Audits as `documents.inbound.confirm`.",
|
||||
parameters: [
|
||||
{
|
||||
name: "id",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: inboundConfirmSchema } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Per-decision outcome.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(confirmResponse, "InboundConfirmEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { consentPaths } from "./consent";
|
||||
import { cyclePaths } from "./cycle";
|
||||
import { biomarkerPaths } from "./biomarkers";
|
||||
import { inboundDocumentPaths } from "./documents";
|
||||
import { devicePaths } from "./devices";
|
||||
import { exportPaths } from "./export";
|
||||
import { familyHistoryPaths } from "./family-history";
|
||||
@@ -89,6 +90,7 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
...mentalHealthPaths,
|
||||
...allergyPaths,
|
||||
...familyHistoryPaths,
|
||||
...inboundDocumentPaths,
|
||||
...onboardingPaths,
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user