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:
Marc-André Bombeck committed 2026-06-28 16:57:24 +02:00
1 parent 422778f679
commit 33bf952d9d
10 files changed
+1442 -6

No files matched your search

+715
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."
}
}
}
+1
View File
@@ -75,6 +75,7 @@ const openApiBase: Pick<
{ name: "MeasurementReminders" },
{ name: "Labs" },
{ name: "Illness" },
{ name: "Documents" },
{ name: "Records" },
{ name: "Admin" },
{ name: "Meta" },
+304
View File
@@ -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,
},
},
},
};
+2
View File
@@ -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,
};