Merge branch 'main' into feat/mood-level-a

This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-08-08 03:29:52 +02:00
commit bd787daf8b
86 files changed
+4346 -206

No files matched your search

+5
View File
@@ -717,6 +717,11 @@ const labResultBackupSchema = z
unit: z.string().min(1),
referenceLow: z.number().nullable().optional(),
referenceHigh: z.number().nullable().optional(),
// Optional so a backup written before these columns existed still
// restores; absent reads as "this reading had no source window on file".
sourceReferenceLow: z.number().nullable().optional(),
sourceReferenceHigh: z.number().nullable().optional(),
sourceReferenceText: z.string().nullable().optional(),
takenAt: isoDateTime,
source: z.string().min(1),
biomarkerName: z.string().nullable().optional(),
+18
View File
@@ -167,6 +167,15 @@ export const extractedFactRawSchema = z.object({
unit: nullableText(80),
referenceLow: z.number().finite().nullable().catch(null),
referenceHigh: z.number().finite().nullable().catch(null),
/**
* Observation: the reference range EXACTLY as printed, verbatim. A report
* writes its window in prose ("3,5 - 5,0", "< 5", "bis 5,0", "negativ"), and
* `referenceLow` / `referenceHigh` above can only carry the cases that
* reduce to two numbers. This field is what stops every other case from
* being dropped on the floor — the string is kept whether or not the parser
* gets bounds out of it.
*/
referenceText: nullableText(120),
/** MedicationStatement: dose + stated status (verbatim). */
dose: nullableText(120),
medicationStatus: nullableText(64),
@@ -237,6 +246,14 @@ export interface ObservationFactData {
unit: string | null;
referenceLow: number | null;
referenceHigh: number | null;
/**
* The reference range as printed on the report, verbatim.
*
* Optional because it genuinely is: a fact staged before this field existed
* carries no key for it, and that is an honest absence rather than a shim
* for a caller we do not ship. Read it as `?? null`.
*/
referenceText?: string | null;
effectiveDate: string | null;
}
@@ -379,6 +396,7 @@ const observationEditSchema = z
unit: optText(80),
referenceLow: z.number().finite().nullable().optional(),
referenceHigh: z.number().finite().nullable().optional(),
referenceText: optText(120),
effectiveDate: isoDate,
})
// Numeric XOR qualitative, mirroring the lab-result discipline. A null/absent
+20
View File
@@ -78,6 +78,13 @@ export const extractedRowSchema = z.object({
unit: z.string().trim().max(80).nullable().catch(null),
referenceLow: z.number().finite().nullable().catch(null),
referenceHigh: z.number().finite().nullable().catch(null),
/**
* The reference range EXACTLY as printed, verbatim. The two numeric bounds
* above can only carry a window that reduces to two numbers; a report that
* prints "bis 5,0" or "negativ" states a window all the same, and this is
* what keeps it instead of dropping it.
*/
referenceText: z.string().trim().max(120).nullable().catch(null),
takenAt: z.string().nullable().catch(null),
confidence: extractedConfidenceSchema,
});
@@ -107,6 +114,8 @@ export interface OcrExtractedRowDto {
unit: string | null;
referenceLow: number | null;
referenceHigh: number | null;
/** The reference range as printed on the report, verbatim. */
referenceText: string | null;
takenAt: string | null;
confidence: {
analyte: number;
@@ -150,6 +159,17 @@ export const ocrCommitRowSchema = z
unit: z.string().trim().min(1).max(40).optional(),
referenceLow,
referenceHigh,
/**
* The window the scanned report printed for this row. Confirmed by the
* human on the review screen alongside everything else, and written onto
* the reading rather than only used to seed a new catalog marker.
*/
referenceText: z
.string()
.trim()
.max(120)
.optional()
.or(z.literal("").transform(() => undefined)),
takenAt: takenAtField,
})
// Numeric XOR qualitative: exactly one of value / valueText.
+60
View File
@@ -14,6 +14,7 @@
*/
import { z } from "zod/v4";
import { SOURCE_REFERENCE_TEXT_MAX } from "@/lib/labs/parse-reference-range";
import { validateEntryInstant } from "@/lib/validations/entry-instant";
/**
@@ -63,6 +64,29 @@ const optionalNote = z
const referenceLow = z.number().finite().optional();
const referenceHigh = z.number().finite().optional();
/**
* The reference range the SOURCE REPORT printed for this one reading.
*
* Distinct from `referenceLow` / `referenceHigh` above, which seed (and are
* stamped from) the catalog marker's band. A lab runs its own method on its
* own device, so the window a physician evaluates against is the one on that
* report; these fields carry it, and `resolveEffectiveReferenceRange` lets it
* win for that reading alone.
*
* `sourceReferenceText` is what was printed, verbatim. It is accepted with or
* without derivable bounds: a window stated as "negativ" or "siehe Befund"
* still belongs on the record, and dropping it because no number could be read
* out of it is exactly the loss this field exists to stop.
*/
const sourceReferenceLow = z.number().finite().optional();
const sourceReferenceHigh = z.number().finite().optional();
const sourceReferenceText = z
.string()
.trim()
.max(SOURCE_REFERENCE_TEXT_MAX)
.optional()
.or(z.literal("").transform(() => undefined));
/**
* v1.18.1 — optional link to a user-scoped `Biomarker` (the catalog row).
* When set, the structured-entry path resolves the unit + reference range
@@ -101,6 +125,9 @@ export const createLabResultSchema = z
unit: requiredText(40).optional(),
referenceLow,
referenceHigh,
sourceReferenceLow,
sourceReferenceHigh,
sourceReferenceText,
takenAt: takenAtField,
note: optionalNote,
source,
@@ -142,6 +169,19 @@ export const createLabResultSchema = z
message: "referenceLow must not exceed referenceHigh",
path: ["referenceLow"],
},
)
// Same guard on the source window. A transposed window is a client error
// here for the same reason: nothing downstream can tell it from a real one.
.refine(
(d) =>
d.valueText !== undefined ||
d.sourceReferenceLow === undefined ||
d.sourceReferenceHigh === undefined ||
d.sourceReferenceLow <= d.sourceReferenceHigh,
{
message: "sourceReferenceLow must not exceed sourceReferenceHigh",
path: ["sourceReferenceLow"],
},
);
export type CreateLabResultInput = z.infer<typeof createLabResultSchema>;
@@ -164,6 +204,9 @@ export const updateLabResultSchema = z
unit: requiredText(40).optional(),
referenceLow: referenceLow.or(z.null()),
referenceHigh: referenceHigh.or(z.null()),
sourceReferenceLow: sourceReferenceLow.or(z.null()),
sourceReferenceHigh: sourceReferenceHigh.or(z.null()),
sourceReferenceText: sourceReferenceText.or(z.null()),
takenAt: takenAtField.optional(),
note: optionalNote.or(z.null()),
})
@@ -183,6 +226,18 @@ export const updateLabResultSchema = z
message: "referenceLow must not exceed referenceHigh",
path: ["referenceLow"],
},
)
.refine(
(d) =>
d.sourceReferenceLow === undefined ||
d.sourceReferenceLow === null ||
d.sourceReferenceHigh === undefined ||
d.sourceReferenceHigh === null ||
d.sourceReferenceLow <= d.sourceReferenceHigh,
{
message: "sourceReferenceLow must not exceed sourceReferenceHigh",
path: ["sourceReferenceLow"],
},
);
export type UpdateLabResultInput = z.infer<typeof updateLabResultSchema>;
@@ -246,7 +301,12 @@ export type ListLabResultsInput = z.infer<typeof listLabResultsSchema>;
* implementation. Re-exported here for the existing import sites.
*/
export {
classifyAgainstEffectiveRange,
classifyReferenceRange,
formatReferenceRange,
resolveEffectiveReferenceRange,
type EffectiveReferenceRange,
type ReferenceRangeOrigin,
type ReferenceRangeStatus,
type SourceReferenceRange,
} from "@/lib/labs/reference-range";