mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(labs): accept optional OCR provenance on lab create
The structured lab-create path (POST /api/labs) now accepts an optional
`source` marker ("MANUAL" | "OCR"). The on-device-OCR ingestion path posts
"OCR" so a reading captured from a phone-camera lab photo records its
provenance; the raw image never touches the server on this path. Omitting
the field reads as "MANUAL", so the existing hand-entry contract is
unchanged. The value is narrowed by a closed Zod enum and assigned
field-by-field, never spread from the body. Mirrors the "OCR" source the
server-side /api/labs/ocr/commit path already stamps.
Regenerate the OpenAPI spec for the widened request body.
This commit is contained in:
1 parent
2563690e4b
commit
74986116cd
4 files changed
+30
-5
No files matched your search
@@ -7056,8 +7056,10 @@ paths:
|
||||
tags:
|
||||
- Labs
|
||||
summary: Record a lab result
|
||||
description: Creates a single lab result for the caller. `source` is hardcoded MANUAL on this path. The optional note is
|
||||
AES-256-GCM encrypted before write. Audits as `labResult.create`.
|
||||
description: Creates a single lab result for the caller. The optional `source` marks provenance (`"MANUAL"` | `"OCR"`);
|
||||
it defaults to `"MANUAL"` when omitted, so the legacy hand-entry contract is unchanged. The on-device-OCR client
|
||||
posts `"OCR"` after the human confirms the parsed values — the raw image never touches the server on this path.
|
||||
The optional note is AES-256-GCM encrypted before write. Audits as `labResult.create`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -7104,6 +7106,11 @@ paths:
|
||||
maxLength: 2000
|
||||
- type: string
|
||||
const: ""
|
||||
source:
|
||||
type: string
|
||||
enum:
|
||||
- MANUAL
|
||||
- OCR
|
||||
required:
|
||||
- takenAt
|
||||
responses:
|
||||
|
||||
@@ -179,6 +179,7 @@ async function postLabResult(request: NextRequest) {
|
||||
referenceHigh,
|
||||
takenAt,
|
||||
note,
|
||||
source,
|
||||
} = parsed.data;
|
||||
|
||||
// v1.18.9 — a qualitative reading carries `valueText` ("negativ" / …) instead
|
||||
@@ -239,7 +240,11 @@ async function postLabResult(request: NextRequest) {
|
||||
referenceLow: biomarker.lowerBound,
|
||||
referenceHigh: biomarker.upperBound,
|
||||
takenAt,
|
||||
source: "MANUAL",
|
||||
// v1.25 (iOS #36) — provenance: the on-device-OCR path posts
|
||||
// `source: "OCR"`; an omitted field stays "MANUAL" so the legacy
|
||||
// hand-entry contract is unchanged. Narrowed to the closed enum by Zod,
|
||||
// never spread from the body.
|
||||
source: source ?? "MANUAL",
|
||||
noteEncrypted: note ? encryptNoteToBytes(note) : null,
|
||||
},
|
||||
});
|
||||
@@ -258,6 +263,8 @@ async function postLabResult(request: NextRequest) {
|
||||
// Whether the client picked a catalog marker (structured) vs the
|
||||
// free-text path that resolved-or-minted one server-side.
|
||||
structured: biomarkerId !== undefined,
|
||||
// v1.25 (iOS #36) — provenance of the reading ("MANUAL" | "OCR").
|
||||
source: source ?? "MANUAL",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
createLabResultSchema.meta({
|
||||
id: "CreateLabResultRequest",
|
||||
description:
|
||||
'Record a single biomarker reading (HbA1c, LDL, ferritin, TSH, …). A reading is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`, e.g. "negativ" / "positiv") — exactly one, never both, never neither. `analyte` + `unit` are free-form (a lab prints its own naming); a qualitative reading needs no unit / bounds. When both numeric bounds are present `referenceLow` must not exceed `referenceHigh`. `takenAt` is a backdatable ISO instant (no future, ≤ 50 years past). The optional `note` is encrypted at rest.',
|
||||
'Record a single biomarker reading (HbA1c, LDL, ferritin, TSH, …). A reading is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`, e.g. "negativ" / "positiv") — exactly one, never both, never neither. `analyte` + `unit` are free-form (a lab prints its own naming); a qualitative reading needs no unit / bounds. When both numeric bounds are present `referenceLow` must not exceed `referenceHigh`. `takenAt` is a backdatable ISO instant (no future, ≤ 50 years past). The optional `note` is encrypted at rest. The optional `source` records provenance: `"OCR"` for an on-device-OCR capture (the raw image never reaches the server on this path), defaulting to `"MANUAL"` when omitted.',
|
||||
});
|
||||
|
||||
updateLabResultSchema.meta({
|
||||
@@ -119,7 +119,7 @@ export const labsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Labs"],
|
||||
summary: "Record a lab result",
|
||||
description:
|
||||
"Creates a single lab result for the caller. `source` is hardcoded MANUAL on this path. The optional note is AES-256-GCM encrypted before write. Audits as `labResult.create`.",
|
||||
'Creates a single lab result for the caller. The optional `source` marks provenance (`"MANUAL"` | `"OCR"`); it defaults to `"MANUAL"` when omitted, so the legacy hand-entry contract is unchanged. The on-device-OCR client posts `"OCR"` after the human confirms the parsed values — the raw image never touches the server on this path. The optional note is AES-256-GCM encrypted before write. Audits as `labResult.create`.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: createLabResultSchema } },
|
||||
|
||||
@@ -79,6 +79,16 @@ const biomarkerId = z.string().trim().min(1).max(64).optional();
|
||||
*/
|
||||
const valueText = requiredText(120).optional();
|
||||
|
||||
/**
|
||||
* v1.25 (iOS #36) — optional provenance marker. The on-device-OCR ingestion
|
||||
* path posts `source: "OCR"` so a reading captured from a phone-camera lab
|
||||
* photo records its provenance; the raw image never touches the server on this
|
||||
* path. Omitting the field reads as `"MANUAL"`, preserving the legacy
|
||||
* hand-entry contract exactly. Mirrors the `source: "OCR"` the server-side
|
||||
* `/api/labs/ocr/commit` path already stamps.
|
||||
*/
|
||||
const source = z.enum(["MANUAL", "OCR"]).optional();
|
||||
|
||||
export const createLabResultSchema = z
|
||||
.object({
|
||||
biomarkerId,
|
||||
@@ -93,6 +103,7 @@ export const createLabResultSchema = z
|
||||
referenceHigh,
|
||||
takenAt: takenAtField,
|
||||
note: optionalNote,
|
||||
source,
|
||||
})
|
||||
// Either a catalog link OR a free-text analyte must be present.
|
||||
.refine((d) => d.biomarkerId !== undefined || d.analyte !== undefined, {
|
||||
|
||||
Reference in new issue
Block a user