diff --git a/src/__tests__/share-view-leaf-render-guard.test.ts b/src/__tests__/share-view-leaf-render-guard.test.ts index 50b591b21..841f63ffa 100644 --- a/src/__tests__/share-view-leaf-render-guard.test.ts +++ b/src/__tests__/share-view-leaf-render-guard.test.ts @@ -208,6 +208,7 @@ const DISPOSITIONS: Record = { value: 42, valueText: null, unit: "ng/mL", + sourceValue: null, referenceLow: 30, referenceHigh: 400, catalogReferenceLow: 30, diff --git a/src/components/clinician/__tests__/clinician-view.test.tsx b/src/components/clinician/__tests__/clinician-view.test.tsx index 22fb9d4e0..d109b52b0 100644 --- a/src/components/clinician/__tests__/clinician-view.test.tsx +++ b/src/components/clinician/__tests__/clinician-view.test.tsx @@ -176,6 +176,7 @@ describe("", () => { value: 5.2, valueText: null, unit: "mmol/L", + sourceValue: null, referenceLow: 3.9, referenceHigh: 5.4, catalogReferenceLow: 3.5, @@ -194,6 +195,55 @@ describe("", () => { expect(html).toContain("saved range 3.5–5"); }); + it("prints a lab value and its range with every digit it was recorded with", () => { + // Two decimals at most read a PSA of 0.034 as "0.03" and a TSH window of + // 0.275 as "0.28": the share page dropped what the lab reported. + const html = render( + makeReport({ + labResults: [ + { + panel: null, + analyte: "PSA", + value: 0.034, + valueText: null, + unit: "ng/mL", + sourceValue: null, + referenceLow: 0.275, + referenceHigh: 4.2, + catalogReferenceLow: 0.275, + catalogReferenceHigh: 4.2, + sourceReferenceText: null, + referenceOrigin: "catalog", + referenceDivergesFromCatalog: false, + takenAt: "2026-01-20T09:00:00.000Z", + count: 1, + }, + ], + }), + ); + expect(html).toContain("0.034 ng/mL"); + expect(html).toContain("Reference 0.275–4.2"); + }); + + it("keeps the digits of a measurement read below one", () => { + const html = render( + makeReport({ + stats: { + WAIST_TO_HEIGHT: { + avg: 0.5, + min: 0.495, + max: 0.505, + count: 2, + latest: 0.505, + }, + }, + }), + { selection: selectionFromLeaves(["WAIST_TO_HEIGHT"]) }, + ); + expect(html).toContain("0.505"); + expect(html).toContain("0.495"); + }); + it("renders the fenced wellness card with the descriptive disclaimer", () => { const html = render(makeReport()); expect(html).toContain("Wellness scores"); @@ -241,8 +291,9 @@ describe("", () => { }, }), ); - expect(html).toContain("latest 79 kg (avg 80 kg, range 78–82 kg)"); - expect(html).toContain("latest 7.5 h (avg 7.5 h, range 7–8 h)"); + // One precision per row, the one the PDF prints the row with. + expect(html).toContain("latest 79.0 kg (avg 80.0 kg, range 78.0–82.0 kg)"); + expect(html).toContain("latest 7.5 h (avg 7.5 h, range 7.0–8.0 h)"); }); it("shows glucose in the record owner's unit, never raw mg/dL", () => { @@ -259,10 +310,10 @@ describe("", () => { ); // 99 mg/dL = 5.5 mmol/L, 108 = 6.0, 90 = 5.0, 126 = 7.0, 95 = 5.3. expect(html).toContain( - "latest 5.5 mmol/L (avg 6 mmol/L, range 5–7 mmol/L)", + "latest 5.5 mmol/L (avg 6.0 mmol/L, range 5.0–7.0 mmol/L)", ); expect(html).toContain( - "latest 5.3 mmol/L (avg 5.5 mmol/L, range 5–6 mmol/L)", + "latest 5.3 mmol/L (avg 5.5 mmol/L, range 5.0–6.0 mmol/L)", ); expect(html).not.toContain("mg/dL"); expect(html).not.toContain("latest 99"); diff --git a/src/components/clinician/clinician-view.tsx b/src/components/clinician/clinician-view.tsx index 52cf315ef..d7ab42b1f 100644 --- a/src/components/clinician/clinician-view.tsx +++ b/src/components/clinician/clinician-view.tsx @@ -30,6 +30,7 @@ * document list in `./documents-list`, the downloads in `./download-actions`. */ import type { DoctorReportData } from "@/lib/doctor-report-data"; +import { formatLabValue, type LabNumberFormat } from "@/lib/labs/format-value"; import { makeFormatters, type DateFormatPreference, @@ -152,6 +153,10 @@ export function ClinicianView({ // Two decimals at most, in the owner's locale: a German reader gets // "80,5", not the "80.5" a bare number prints. const fmtNum = (n: number) => fmt.number(Math.round(n * 100) / 100); + // A lab figure or a dose is what was recorded, not a statistic: it keeps + // every digit it holds, so a PSA of 0.034 does not read as 0.03. + const fmtRecorded: LabNumberFormat = (n, printed) => + formatLabValue(n, fmt.number, printed); const scope = makeLeafScope(selection, unavailableLeaves); return ( @@ -198,7 +203,7 @@ export function ClinicianView({ t={t} report={report} scope={scope} - fmtNum={fmtNum} + fmtDigits={fmt.number} /> {report.bmi !== null && report.bmi !== undefined && @@ -219,14 +224,14 @@ export function ClinicianView({ t={t} report={report} scope={scope} - fmtNum={fmtNum} + fmtDigits={fmt.number} /> {/* ── medications ──────────────────────────────────────── */} @@ -242,13 +247,14 @@ export function ClinicianView({ scope={scope} fmtDate={fmtDate} fmtNum={fmtNum} + fmtRecorded={fmtRecorded} /> {/* ── history ──────────────────────────────────────────── */} diff --git a/src/components/clinician/history-sections.tsx b/src/components/clinician/history-sections.tsx index e49ff28ab..308f74ea4 100644 --- a/src/components/clinician/history-sections.tsx +++ b/src/components/clinician/history-sections.tsx @@ -17,6 +17,7 @@ import { encounterKindLabelKey, lateralityLabelKey, } from "@/lib/encounters/kind-label"; +import type { LabNumberFormat } from "@/lib/labs/format-value"; import { formatReferenceRange } from "@/lib/labs/reference-range"; import type { DoctorReportData } from "@/lib/doctor-report-data"; import { @@ -47,13 +48,14 @@ export function LabResultsSection({ report, scope, fmtDate, - fmtNum, + fmtLab, }: { t: Translate; report: DoctorReportData; scope: LeafScope; fmtDate: (iso: string) => string; - fmtNum: (n: number) => string; + /** Lab figures keep the precision they were recorded at (`formatLabValue`). */ + fmtLab: LabNumberFormat; }) { const results = report.labResults ?? []; @@ -71,9 +73,13 @@ export function LabResultsSection({ const qualitative = lab.value === null; const reading = qualitative ? lab.valueText - : `${fmtNum(lab.value as number)} ${lab.unit}`.trim(); - const range = (low: number | null, high: number | null) => - formatReferenceRange(low, high, fmtNum, { + : `${fmtLab(lab.value as number, lab.sourceValue)} ${lab.unit}`.trim(); + const range = ( + low: number | null, + high: number | null, + printed?: number | null, + ) => + formatReferenceRange(low, high, (n) => fmtLab(n, printed), { emptyText: "", }); // The window the reading was judged against, printed as the source @@ -83,7 +89,11 @@ export function LabResultsSection({ ? null : lab.referenceOrigin === "source" && lab.sourceReferenceText ? lab.sourceReferenceText - : range(lab.referenceLow, lab.referenceHigh); + : range( + lab.referenceLow, + lab.referenceHigh, + lab.referenceOrigin === "source" ? lab.sourceValue : null, + ); // When the report's own window and the saved band disagree about the // same number, both are named. Showing one of two disagreeing windows // is a partial answer, and the PDF's footnote says so; here it fits on diff --git a/src/components/clinician/report-sections.tsx b/src/components/clinician/report-sections.tsx index 919013988..2868ba6d7 100644 --- a/src/components/clinician/report-sections.tsx +++ b/src/components/clinician/report-sections.tsx @@ -20,9 +20,11 @@ import { dateOnlyAtNoonUtc } from "@/lib/tz/date-only"; import type { MeasurementType } from "@/generated/prisma/client"; import type { DoctorReportData } from "@/lib/doctor-report-data"; import { + reportStatFractionDigits, reportStatUnit, reportStatValue, } from "@/lib/doctor-report/stat-display"; +import type { FractionDigitsFormat } from "@/lib/format/recorded-number"; import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/lib/measurements/type-label-keys"; import { @@ -203,12 +205,13 @@ export function MeasurementGroups({ t, report, scope, - fmtNum, + fmtDigits, }: { t: Translate; report: DoctorReportData; scope: LeafScope; - fmtNum: (n: number) => string; + /** A locale number with exactly the given decimals. */ + fmtDigits: FractionDigitsFormat; }) { const groups = REPORT_GROUPS.map((group) => ({ labelKey: group.labelKey, @@ -225,8 +228,9 @@ export function MeasurementGroups({ // mass, length and temperature, the owner's unit for glucose, hours for // sleep. A clinician must never read a figure without knowing its unit. const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null); - const show = (type: string, value: number) => - fmtNum(reportStatValue(type, value, glucoseUnit)); + // ...and in the same precision: one per row, the one its readings carry. + const show = (type: string, value: number, digits: number) => + fmtDigits(reportStatValue(type, value, glucoseUnit), digits); return ( <> @@ -236,18 +240,21 @@ export function MeasurementGroups({ title={t(group.labelKey)} leaves={group.rows.map((row) => row.type)} > - {group.rows.map(({ type, stat }) => ( - - ))} + {group.rows.map(({ type, stat }) => { + const digits = reportStatFractionDigits(type, stat!, glucoseUnit); + return ( + + ); + })} ))} @@ -278,19 +285,22 @@ export function GlucoseSection({ t, report, scope, - fmtNum, + fmtDigits, }: { t: Translate; report: DoctorReportData; scope: LeafScope; - fmtNum: (n: number) => string; + /** A locale number with exactly the given decimals. */ + fmtDigits: FractionDigitsFormat; }) { const entries = Object.entries(report.glucoseStats).filter( ([, s]) => s.count > 0, ); // Stored in mg/dL; shown in the record owner's unit, as the PDF shows it. const glucoseUnit = resolveGlucoseUnit(report.glucoseUnit ?? null); - const show = (value: number) => fmtNum(convertGlucose(value, glucoseUnit)); + // The unit's own precision, the one the PDF's glucose rows print with. + const show = (value: number, digits: number) => + fmtDigits(convertGlucose(value, glucoseUnit), digits); return ( - {entries.map(([ctx, s]) => ( - - ))} + {entries.map(([ctx, s]) => { + const digits = reportStatFractionDigits( + "BLOOD_GLUCOSE", + s, + glucoseUnit, + ); + return ( + + ); + })} ); } diff --git a/src/components/clinician/therapy-sections.tsx b/src/components/clinician/therapy-sections.tsx index 0c250a44d..d467562c6 100644 --- a/src/components/clinician/therapy-sections.tsx +++ b/src/components/clinician/therapy-sections.tsx @@ -42,12 +42,15 @@ export function Glp1Section({ scope, fmtDate, fmtNum, + fmtRecorded, }: { t: Translate; report: DoctorReportData; scope: LeafScope; fmtDate: (iso: string) => string; fmtNum: (n: number) => string; + /** A dose keeps every digit it was recorded with (0.125 mg, not 0.13). */ + fmtRecorded: (n: number) => string; }) { const glp1 = report.glp1 ?? null; const medications = glp1?.medications ?? []; @@ -89,7 +92,7 @@ export function Glp1Section({ value={ med.currentDose ? t("doctorReport.glp1CurrentDose", { - value: fmtNum(med.currentDose.value), + value: fmtRecorded(med.currentDose.value), unit: med.currentDose.unit, since: fmtDate(med.currentDose.since), }) @@ -112,7 +115,7 @@ export function Glp1Section({ key={`${change.effectiveFrom}-${index}`} label={fmtDate(change.effectiveFrom)} value={ - `${fmtNum(change.value)} ${change.unit}` + + `${fmtRecorded(change.value)} ${change.unit}` + (change.note ? ` · ${change.note}` : "") } /> @@ -167,13 +170,14 @@ export function DoseLogSection({ report, scope, fmtDateTime, - fmtNum, + fmtRecorded, }: { t: Translate; report: DoctorReportData; scope: LeafScope; fmtDateTime: (iso: string) => string; - fmtNum: (n: number) => string; + /** A dose keeps every digit it was recorded with (0.125 mg, not 0.13). */ + fmtRecorded: (n: number) => string; }) { const administrations = report.medicationAdministrations ?? []; // Newest first: a clinician reads "when did they last take it", not "when @@ -212,7 +216,7 @@ export function DoseLogSection({ ? t("doctorReport.colTaken") : t("doctorReport.colSkipped")) + (dose.dose - ? ` · ${fmtNum(dose.dose.value)} ${dose.dose.unit}` + ? ` · ${fmtRecorded(dose.dose.value)} ${dose.dose.unit}` : dose.doseText ? ` · ${dose.doseText}` : "") + diff --git a/src/components/labs/__tests__/lab-converted-note.test.tsx b/src/components/labs/__tests__/lab-converted-note.test.tsx index 2f0c79c49..af70a67d7 100644 --- a/src/components/labs/__tests__/lab-converted-note.test.tsx +++ b/src/components/labs/__tests__/lab-converted-note.test.tsx @@ -33,10 +33,10 @@ describe("", () => { ).toContain("umgerechnet aus 5,6 mmol/L"); }); - it("trims a long fraction the way the reading itself is trimmed", () => { + it("prints the figure the report printed, with every digit", () => { expect( render({ converted: true, sourceValue: 5.6123456, sourceUnit: "mmol/L" }), - ).toContain("converted from 5.61 mmol/L"); + ).toContain("converted from 5.6123456 mmol/L"); }); it.each([ diff --git a/src/components/labs/__tests__/lab-reference-range-bar.test.tsx b/src/components/labs/__tests__/lab-reference-range-bar.test.tsx index 6b53a51f8..cc5ef1f97 100644 --- a/src/components/labs/__tests__/lab-reference-range-bar.test.tsx +++ b/src/components/labs/__tests__/lab-reference-range-bar.test.tsx @@ -53,8 +53,8 @@ describe("", () => { "de", ); expect(html).toContain('aria-label="0,4–4,05 mIU/L"'); - // Lab precision: at most two decimals, then the locale's separator. - expect(html).toContain("1,83 mIU/L"); + // Lab precision: every recorded digit, in the locale's separator. + expect(html).toContain("1,8333 mIU/L"); expect(html).not.toMatch(/\d\.\d+ mIU\/L/); }); }); diff --git a/src/components/labs/__tests__/ocr-row-editor.test.tsx b/src/components/labs/__tests__/ocr-row-editor.test.tsx index 12f012634..971170ffb 100644 --- a/src/components/labs/__tests__/ocr-row-editor.test.tsx +++ b/src/components/labs/__tests__/ocr-row-editor.test.tsx @@ -175,7 +175,8 @@ describe(" unit differs but converts", () => { it("says what Save will store and what the report stated, not that it is blocked", () => { const html = render(undefined, "en", converts); expect(html).toContain('data-slot="ocr-row-converted-badge"'); - expect(html).toContain("Saved as 100.9 mg/dL, converted from 5.6 mmol/L"); + // The converted value keeps the precision of the printed 5.6. + expect(html).toContain("Saved as 101 mg/dL, converted from 5.6 mmol/L"); expect(html).not.toContain("Tracked in"); expect(html).not.toContain('aria-invalid="true"'); }); diff --git a/src/components/labs/lab-biomarker-chart.tsx b/src/components/labs/lab-biomarker-chart.tsx index 7a8828340..01cca67b8 100644 --- a/src/components/labs/lab-biomarker-chart.tsx +++ b/src/components/labs/lab-biomarker-chart.tsx @@ -75,6 +75,8 @@ interface ChartPoint { referenceHigh: number | null; referenceOrigin: LabResultDto["referenceOrigin"]; sourceReferenceText: string | null; + /** What the report printed, for a converted reading; sets its precision. */ + sourceValue: number | null; } /** The source window drawn as the second band, resolved from the readings in view. */ @@ -153,6 +155,7 @@ export function LabBiomarkerChart({ referenceHigh: r.referenceHigh, referenceOrigin: r.referenceOrigin, sourceReferenceText: r.sourceReferenceText, + sourceValue: r.sourceValue, })); }, [readings, range, nowMs]); @@ -221,7 +224,7 @@ export function LabBiomarkerChart({ const before = at > 0 ? ordered[at - 1] : undefined; return { label: readings[0]?.analyte ?? "", - value: labNumber(point.value), + value: labNumber(point.value, point.sourceValue), unit, compare: before && before.value !== null @@ -229,7 +232,8 @@ export function LabBiomarkerChart({ label: t("day.previousReading", { date: labShortDate(before.takenAt), }), - value: `${labNumber(before.value)} ${unit}`.trim(), + value: + `${labNumber(before.value, before.sourceValue)} ${unit}`.trim(), } : undefined, }; @@ -381,7 +385,7 @@ export function LabBiomarkerChart({ const rows: RichTooltipRow[] = [ { name: t("labs.chart.valueLabel"), - value: `${labNumber(point.value)} ${unit}`, + value: `${labNumber(point.value, point.sourceValue)} ${unit}`, color: primary, }, ]; diff --git a/src/components/labs/lab-converted-note.tsx b/src/components/labs/lab-converted-note.tsx index dfbdc9e85..a66041585 100644 --- a/src/components/labs/lab-converted-note.tsx +++ b/src/components/labs/lab-converted-note.tsx @@ -1,7 +1,6 @@ "use client"; import { useTranslations } from "@/lib/i18n/context"; -import { formatLabValue } from "@/lib/labs/format-value"; import { useLabNumber } from "./use-lab-format"; @@ -36,7 +35,7 @@ export function LabConvertedNote({ className={className ?? "text-muted-foreground text-xs"} > {t("labs.convertedFrom", { - value: formatLabValue(reading.sourceValue, labNumber), + value: labNumber(reading.sourceValue), unit: reading.sourceUnit, })} diff --git a/src/components/labs/lab-history-list.tsx b/src/components/labs/lab-history-list.tsx index 2d8f16c2e..f555f1154 100644 --- a/src/components/labs/lab-history-list.tsx +++ b/src/components/labs/lab-history-list.tsx @@ -102,7 +102,7 @@ export function LabHistoryList({ readings }: { readings: LabResultDto[] }) { value: reading.value === null ? (reading.valueText ?? "") - : labNumber(reading.value), + : labNumber(reading.value, reading.sourceValue), unit: reading.value === null ? undefined : reading.unit, compare: before ? { diff --git a/src/components/labs/ocr-row-editor.tsx b/src/components/labs/ocr-row-editor.tsx index 8219eb601..d140e861d 100644 --- a/src/components/labs/ocr-row-editor.tsx +++ b/src/components/labs/ocr-row-editor.tsx @@ -206,7 +206,10 @@ export function OcrRowEditor({ {t("labs.ocr.unitConvertedBadge", { - value: labNumber(conversion.value), + value: labNumber( + conversion.value, + conversion.statedValue, + ), markerUnit: conversion.unit, statedValue: labNumber(conversion.statedValue), unit: conversion.statedUnit, diff --git a/src/components/labs/use-lab-format.ts b/src/components/labs/use-lab-format.ts index ef7820fc2..78edcaa0e 100644 --- a/src/components/labs/use-lab-format.ts +++ b/src/components/labs/use-lab-format.ts @@ -8,13 +8,15 @@ import { formatLabValue, type LabNumberFormat } from "@/lib/labs/format-value"; import { isNoonUtcAnchor } from "@/lib/tz/date-only"; /** - * A lab value in the reader's number format, trimmed the way every lab - * surface trims it (whole numbers bare, at most two decimals). + * A lab value in the reader's number format, with every digit the reading + * holds (see `formatLabValue`). Pass the reading's `sourceValue` as `printed` + * for a converted reading so it keeps the precision its report printed. */ export function useLabNumber(): LabNumberFormat { const fmt = useFormatters(); return useCallback( - (value: number) => formatLabValue(value, (n) => fmt.number(n)), + (value: number, printed?: number | null) => + formatLabValue(value, fmt.number, printed), [fmt], ); } diff --git a/src/lib/__tests__/doctor-report-pdf-core.test.ts b/src/lib/__tests__/doctor-report-pdf-core.test.ts index 031ed6de3..d8c9c2f8d 100644 --- a/src/lib/__tests__/doctor-report-pdf-core.test.ts +++ b/src/lib/__tests__/doctor-report-pdf-core.test.ts @@ -635,6 +635,106 @@ describe("doctor-report clinical glucose panel", () => { // v1.18.1 P4 — illness / condition episodes section. Retrospective + factual: // label, type, course, onset, resolved. Present only when the aggregator // populated `illnessEpisodes` (module on + episode in window). +describe("doctor-report recorded precision", () => { + function lab( + over: Partial[number]>, + ): NonNullable[number] { + return { + panel: null, + analyte: "PSA", + value: 0.034, + valueText: null, + unit: "ng/mL", + sourceValue: null, + referenceLow: null, + referenceHigh: 4, + catalogReferenceLow: null, + catalogReferenceHigh: 4, + sourceReferenceText: null, + referenceOrigin: "catalog", + referenceDivergesFromCatalog: false, + takenAt: "2026-04-20T08:00:00.000Z", + count: 1, + ...over, + }; + } + + async function render(data: DoctorReportData): Promise { + const bytes = renderDoctorReportPdfBytes(data, { + timeFormat: "AUTO", + dateFormat: "AUTO", + t: getServerTranslator("en").t, + locale: "en", + now: FIXED_NOW, + }); + return extractText(bytes); + } + + it("prints a lab value and its range with the digits they were recorded with", async () => { + const text = await render( + makeData({ + labResults: [ + lab({}), + lab({ + analyte: "TSH", + value: 2.345, + unit: "mIU/L", + referenceLow: 0.27, + referenceHigh: 4.2, + catalogReferenceLow: 0.27, + catalogReferenceHigh: 4.2, + }), + ], + }), + ); + // A one-decimal report printed these as "0.0 ng/mL" and "0.3–4.2". + expect(text).toContain("0.034 ng/mL"); + expect(text).toContain("2.345 mIU/L"); + expect(text).toContain("0.27–4.2"); + }); + + it("prints a converted lab value with the precision its report printed", async () => { + const text = await render( + makeData({ + labResults: [ + lab({ + analyte: "Cholesterol", + value: 5.2 * 38.67, + unit: "mg/dL", + sourceValue: 5.2, + referenceLow: null, + referenceHigh: 5 * 38.67, + catalogReferenceHigh: null, + referenceOrigin: "source", + }), + ], + }), + ); + expect(text).toContain("201 mg/dL"); + // The PDF spells "≤" as "<=" (WinAnsi). + expect(text).toContain("<= 193"); + }); + + it("keeps the digits of a measurement read below one", async () => { + const text = await render( + makeData({ + stats: { + WAIST_TO_HEIGHT: { + avg: 0.525, + min: 0.52, + max: 0.53, + count: 2, + latest: 0.53, + }, + }, + }), + ); + // The fixed single decimal printed every one of these as "0.5". + expect(text).toContain("0.53"); + expect(text).toContain("0.52"); + }); +}); + describe("doctor-report illness section", () => { it("prints the conditions table with label, type, and dates", async () => { const data = makeData({ diff --git a/src/lib/day/records.ts b/src/lib/day/records.ts index e065d9d97..4386287ee 100644 --- a/src/lib/day/records.ts +++ b/src/lib/day/records.ts @@ -29,6 +29,7 @@ import type { DaySectionKey, } from "@/lib/day/contract"; import { prisma } from "@/lib/db"; +import { formatRecordedNumber } from "@/lib/format/recorded-number"; import { SCREENER_INSTRUMENTS } from "@/lib/validations/mental-health"; import { listTargetsBySource } from "@/lib/links/link-service"; import { getEvent } from "@/lib/logging/context"; @@ -686,6 +687,7 @@ async function labsPart(frame: DayFrame): Promise { value: true, valueText: true, unit: true, + sourceValue: true, takenAt: true, biomarkerId: true, noteEncrypted: true, @@ -703,7 +705,9 @@ async function labsPart(frame: DayFrame): Promise { title: row.analyte, meta: row.value !== null - ? `${row.value} ${row.unit}`.trim() + ? // Every recorded digit, without the float noise a converted + // reading carries (the labs page prints the same figure). + `${formatRecordedNumber(row.value, (v, digits) => v.toFixed(digits), row.sourceValue)} ${row.unit}`.trim() : (row.valueText ?? null), note: openText(row.noteEncrypted, "lab note"), href: row.biomarkerId ? `/labs/${row.biomarkerId}` : "/labs", diff --git a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts index eb8488a10..39ad2f552 100644 --- a/src/lib/doctor-report-pdf/clinical-records-notes-section.ts +++ b/src/lib/doctor-report-pdf/clinical-records-notes-section.ts @@ -10,6 +10,7 @@ import { classifyReferenceRange, formatReferenceRange, } from "../labs/reference-range"; +import { formatLabValue } from "../labs/format-value"; import { pdfCursorState, type DoctorReportPdfCursorState, @@ -46,8 +47,19 @@ export function buildClinicalRecordsNotesSection( doc.text(t("doctorReport.labsTitle"), margin, y); y += 6; - const rangeText = (low: number | null, high: number | null): string => - formatReferenceRange(low, high, num, { emptyText: "—" }); + // Lab figures print with the precision they were recorded at, never the + // report's one-decimal default: a PSA of 0.034 is not "0.0". A window + // converted with its reading keeps the precision of what was printed. + const labNum = (value: number, printed?: number | null): string => + formatLabValue(value, num, printed); + const rangeText = ( + low: number | null, + high: number | null, + printed?: number | null, + ): string => + formatReferenceRange(low, high, (n) => labNum(n, printed), { + emptyText: "—", + }); // Quiet, non-colour status glyph. In-range reads as a neutral dash so // the table is not a field of warning marks; out-of-range reads as a // direction arrow the clinician can scan, with no alarm tint. @@ -77,7 +89,11 @@ export function buildClinicalRecordsNotesSection( const printed = lr.referenceOrigin === "source" && lr.sourceReferenceText ? lr.sourceReferenceText - : rangeText(lr.referenceLow, lr.referenceHigh); + : rangeText( + lr.referenceLow, + lr.referenceHigh, + lr.referenceOrigin === "source" ? lr.sourceValue : null, + ); return lr.referenceDivergesFromCatalog ? `${printed} *` : printed; }; @@ -93,7 +109,7 @@ export function buildClinicalRecordsNotesSection( lr.panel ? `${lr.analyte} (${lr.panel})` : lr.analyte, isQualitative ? (lr.valueText ?? "") - : `${num(lr.value as number)} ${lr.unit}`.trim(), + : `${labNum(lr.value as number, lr.sourceValue)} ${lr.unit}`.trim(), isQualitative ? "—" : referenceCell(lr), isQualitative ? "" diff --git a/src/lib/doctor-report-pdf/measurements-charts-section.ts b/src/lib/doctor-report-pdf/measurements-charts-section.ts index 797c56150..672857029 100644 --- a/src/lib/doctor-report-pdf/measurements-charts-section.ts +++ b/src/lib/doctor-report-pdf/measurements-charts-section.ts @@ -6,7 +6,10 @@ import { glucoseBandParams, resolveGlucoseUnit, } from "../glucose"; -import { reportStatValue } from "../doctor-report/stat-display"; +import { + reportStatFractionDigits, + reportStatValue, +} from "../doctor-report/stat-display"; import type { MeasurementType } from "@/generated/prisma/client"; import { MEASUREMENT_TYPE_LABEL_KEYS } from "../measurements/type-label-keys"; import { @@ -223,13 +226,14 @@ export function buildMeasurementsChartsSection( for (const ctx of GLUCOSE_CONTEXTS) { const s = data.glucoseStats?.[ctx]; if (!s) continue; - const conv = (v: number) => convertGlucose(v, glucoseUnit); + const digits = reportStatFractionDigits("BLOOD_GLUCOSE", s, glucoseUnit); + const show = (v: number) => num(convertGlucose(v, glucoseUnit), digits); glucoseContextRows.push([ t(GLUCOSE_LABEL_KEYS[ctx]), - `${num(conv(s.latest))} ${glucoseUnit}`.trim(), - `${num(conv(s.avg))} ${glucoseUnit}`.trim(), - num(conv(s.min)), - num(conv(s.max)), + `${show(s.latest)} ${glucoseUnit}`.trim(), + `${show(s.avg)} ${glucoseUnit}`.trim(), + show(s.min), + show(s.max), String(s.count), ]); } @@ -242,13 +246,17 @@ export function buildMeasurementsChartsSection( const unit = unitFor(type); // Sleep minutes print as hours and glucose in the owner's unit; the // clinician view reads the same helper, so the two cannot drift. - const conv = (v: number) => reportStatValue(type, v, glucoseUnit); + // One precision per row, the one the readings carry (see + // `reportStatFractionDigits`); the share page prints the same. + const digits = reportStatFractionDigits(type, s, glucoseUnit); + const show = (v: number) => + num(reportStatValue(type, v, glucoseUnit), digits); groupRows.push([ t(MEASUREMENT_TYPE_LABEL_KEYS[type as MeasurementType] ?? ""), - `${num(conv(s.latest))} ${unit}`.trim(), - `${num(conv(s.avg))} ${unit}`.trim(), - num(conv(s.min)), - num(conv(s.max)), + `${show(s.latest)} ${unit}`.trim(), + `${show(s.avg)} ${unit}`.trim(), + show(s.min), + show(s.max), String(s.count), ]); } diff --git a/src/lib/doctor-report-pdf/medication-mood-wellness-section.ts b/src/lib/doctor-report-pdf/medication-mood-wellness-section.ts index b3e53dc5f..2aa8715f0 100644 --- a/src/lib/doctor-report-pdf/medication-mood-wellness-section.ts +++ b/src/lib/doctor-report-pdf/medication-mood-wellness-section.ts @@ -2,6 +2,7 @@ import type { jsPDF } from "jspdf"; import { dateOnlyAtNoonUtc } from "@/lib/tz/date-only"; import autoTable from "jspdf-autotable"; import { adherenceRatePercent } from "../doctor-report-data"; +import { formatRecordedNumber } from "../format/recorded-number"; import { pdfCursorState, type DoctorReportPdfCursorState, @@ -184,7 +185,7 @@ export function buildMedicationMoodWellnessSection( if (med.currentDose) { doc.text( t("doctorReport.glp1CurrentDose", { - value: num(med.currentDose.value, 2), + value: formatRecordedNumber(med.currentDose.value, num), unit: med.currentDose.unit, since: fmtDate(med.currentDose.since), }), @@ -212,7 +213,7 @@ export function buildMedicationMoodWellnessSection( if (med.doseHistory.length > 0) { const historyRows = med.doseHistory.map((dc) => [ fmtDate(dc.effectiveFrom), - `${num(dc.value, 2)} ${dc.unit}`, + `${formatRecordedNumber(dc.value, num)} ${dc.unit}`, dc.note ?? "", ]); // A titration history reads as one block — break before it if the diff --git a/src/lib/doctor-report-types.ts b/src/lib/doctor-report-types.ts index f718d9c55..de8a01b22 100644 --- a/src/lib/doctor-report-types.ts +++ b/src/lib/doctor-report-types.ts @@ -283,6 +283,12 @@ export interface DoctorReportData { */ valueText: string | null; unit: string; + /** + * The figure the source report printed when `value` was converted into + * the marker's unit; null for a reading stored as stated. It sets the + * precision the converted value is printed with (`formatLabValue`). + */ + sourceValue: number | null; /** * The reference window this reading is judged against: the range printed * on its own report when it carries one, the catalog band otherwise. diff --git a/src/lib/doctor-report/clinical-records.ts b/src/lib/doctor-report/clinical-records.ts index 05f408e7d..e212f2225 100644 --- a/src/lib/doctor-report/clinical-records.ts +++ b/src/lib/doctor-report/clinical-records.ts @@ -60,6 +60,7 @@ export async function loadLabResults( sourceReferenceHigh: true, sourceReferenceText: true, sourceUnit: true, + sourceValue: true, takenAt: true, biomarkerId: true, biomarker: { select: { name: true, panel: true } }, @@ -93,6 +94,7 @@ export async function loadLabResults( value: r.value, valueText: r.valueText, unit: r.unit, + sourceValue: r.sourceValue, referenceLow: range.low, referenceHigh: range.high, catalogReferenceLow: range.catalogLow, diff --git a/src/lib/doctor-report/stat-display.ts b/src/lib/doctor-report/stat-display.ts index a2ede71f9..10b4762b9 100644 --- a/src/lib/doctor-report/stat-display.ts +++ b/src/lib/doctor-report/stat-display.ts @@ -18,6 +18,8 @@ * it. */ import { convertGlucose, type GlucoseUnit } from "@/lib/glucose"; +import { recordedPrecision } from "@/lib/format/recorded-number"; +import { getReadingTransform } from "@/lib/measurements/display-transform"; import { getUnitForType } from "@/lib/validations/measurement"; /** @@ -77,3 +79,49 @@ export function reportStatValue( if (type === "BLOOD_GLUCOSE") return convertGlucose(value, glucoseUnit); return value; } + +/** Significant digits a statistic keeps before it is cut to decimals. */ +const STAT_SIGNIFICANT_DIGITS = 3; + +/** Decimals a statistic row never goes below, and never above. */ +const STAT_MIN_FRACTION_DIGITS = 1; +const STAT_MAX_FRACTION_DIGITS = 3; + +/** + * Decimals for one statistic row of `type` (latest, average and range alike, + * so a row reads in one precision), for the number {@link reportStatValue} + * returns. + * + * A fixed single decimal printed a waist-to-height ratio of 0.52 as "0.5" and + * a temperature deviation of 0.25 as "0.3": a measurement below or near 1 + * lost the digits that carry it. Each reading now keeps up to + * {@link STAT_SIGNIFICANT_DIGITS} significant digits, but never more decimals + * than it was recorded with, so a weight of 79.5 stays "79.5" and a device + * figure of 45.123456 ms reads "45.1" rather than six decimals of sensor + * noise. Glucose follows its unit's own precision (whole mg/dL, one decimal + * mmol/L) and sleep, a quotient of minutes, keeps one decimal of hours. + */ +export function reportStatFractionDigits( + type: string, + stat: { latest: number; min: number; max: number }, + glucoseUnit: GlucoseUnit, +): number { + if (type === "BLOOD_GLUCOSE") { + // The report stays SI for everything but glucose, so only its unit counts. + return getReadingTransform(type, { system: "metric", glucoseUnit }) + .decimals; + } + if (type === "SLEEP_DURATION") return 1; + const needed = [stat.latest, stat.min, stat.max].map((raw) => { + const value = reportStatValue(type, raw, glucoseUnit); + if (value === 0 || !Number.isFinite(value)) return 0; + const recorded = recordedPrecision(value).fractionDigits; + const magnitude = Math.floor(Math.log10(Math.abs(value))); + const significant = Math.max(0, STAT_SIGNIFICANT_DIGITS - 1 - magnitude); + return Math.min(recorded, significant); + }); + return Math.min( + STAT_MAX_FRACTION_DIGITS, + Math.max(STAT_MIN_FRACTION_DIGITS, ...needed), + ); +} diff --git a/src/lib/fhir/__tests__/lab-observations.test.ts b/src/lib/fhir/__tests__/lab-observations.test.ts index 049958c06..274f0798b 100644 --- a/src/lib/fhir/__tests__/lab-observations.test.ts +++ b/src/lib/fhir/__tests__/lab-observations.test.ts @@ -52,6 +52,7 @@ describe("lab-result FHIR Observations", () => { value: 110, valueText: null, unit: "mg/dL", + sourceValue: null, referenceLow: null, referenceHigh: 116, catalogReferenceLow: null, @@ -68,6 +69,7 @@ describe("lab-result FHIR Observations", () => { value: 5.4, valueText: null, unit: "%", + sourceValue: null, referenceLow: 4, referenceHigh: 5.6, catalogReferenceLow: null, @@ -145,6 +147,7 @@ describe("lab-result FHIR Observations", () => { value: 2.1, valueText: null, unit: "mg/L", + sourceValue: null, referenceLow: null, referenceHigh: null, catalogReferenceLow: null, @@ -178,6 +181,7 @@ describe("lab-result FHIR Observations", () => { value: 100.9, valueText: null, unit: "mg/dL", + sourceValue: null, referenceLow: 70.27, referenceHigh: 99.1, catalogReferenceLow: 70, @@ -237,6 +241,7 @@ describe("lab-result LOINC + canonical UCUM coding (v1.18.8)", () => { value, valueText: null, unit, + sourceValue: null, referenceLow: bounds?.low ?? null, referenceHigh: bounds?.high ?? null, catalogReferenceLow: null, @@ -345,6 +350,7 @@ describe("qualitative lab-result FHIR Observations (v1.18.9)", () => { value: null, valueText, unit: "", + sourceValue: null, referenceLow: null, referenceHigh: null, catalogReferenceLow: null, diff --git a/src/lib/format/recorded-number.ts b/src/lib/format/recorded-number.ts new file mode 100644 index 000000000..6c4d10ef8 --- /dev/null +++ b/src/lib/format/recorded-number.ts @@ -0,0 +1,101 @@ +/** + * How many digits a number somebody recorded is printed with. + * + * A lab value, a dose or a reference bound is a figure a person typed or a + * report printed, and its digits are the precision it was measured at. A + * PSA of 0.034 ng/mL is not "0.03", and a dose of 0.125 mg is not "0.13". + * Every surface that prints such a figure used to pick its own fixed number + * of decimals (one on the PDF, two on the share page, "two above 1, three + * significant below" on the labs page), so the same reading read differently + * in each place and some of them dropped digits that carried meaning. + * + * The rule here keeps every digit the number holds: + * + * - A stored value is printed exactly as stored. Nothing records how many + * trailing zeros were typed, so "4.20" prints as "4.2"; every digit that is + * there survives. The value is first read back at + * {@link STORED_SIGNIFICANT_DIGITS} significant digits, which removes + * binary floating-point noise ("0.30000000000000004") and nothing a person + * could have entered. + * - A value converted from another unit carries no precision of its own, its + * digits are the product of a conversion factor. When the figure the report + * printed is known (`printed`), the converted value keeps as many + * significant digits as that figure had, never fewer than + * {@link CONVERTED_MIN_SIGNIFICANT_DIGITS} and never fewer than its own + * integer digits, so 5.2 mmol/L reads as 201 mg/dL rather than 201.0844. + * + * The caller formats the result in its own locale with exactly + * `fractionDigits` decimals, so the separator follows the reader. + */ + +/** Significant digits a stored value is read back at. */ +const STORED_SIGNIFICANT_DIGITS = 12; + +/** Floor for a converted value's significant digits. */ +const CONVERTED_MIN_SIGNIFICANT_DIGITS = 3; + +/** `Intl.NumberFormat` accepts at most this many fraction digits everywhere. */ +const MAX_FRACTION_DIGITS = 20; + +/** A locale number formatter that prints exactly `fractionDigits` decimals. */ +export type FractionDigitsFormat = ( + value: number, + fractionDigits: number, +) => string; + +/** Significant digits and decimal exponent of a finite, nonzero number. */ +function shape(n: number): { digits: number; exponent: number } { + // Without an argument `toExponential` prints as many digits as it takes to + // identify the number uniquely: the shortest round-trip spelling. + const [mantissa, exponent] = Math.abs(n).toExponential().split("e"); + return { + digits: mantissa.replace(".", "").length, + exponent: Number(exponent), + }; +} + +/** + * The value to print and how many decimals it carries. `printed` is the + * figure the source printed when `value` was converted from it, else absent. + */ +export function recordedPrecision( + value: number, + printed?: number | null, +): { value: number; fractionDigits: number } { + if (!Number.isFinite(value) || value === 0) { + return { value, fractionDigits: 0 }; + } + let significant = STORED_SIGNIFICANT_DIGITS; + if ( + printed !== null && + printed !== undefined && + Number.isFinite(printed) && + printed !== 0 + ) { + const integerDigits = Math.max(1, shape(value).exponent + 1); + significant = Math.max( + shape(printed).digits, + CONVERTED_MIN_SIGNIFICANT_DIGITS, + integerDigits, + ); + } + const rounded = Number(value.toPrecision(Math.min(significant, 100))); + const { digits, exponent } = shape(rounded); + return { + value: rounded, + fractionDigits: Math.min( + MAX_FRACTION_DIGITS, + Math.max(0, digits - 1 - exponent), + ), + }; +} + +/** A recorded number in the caller's locale, with every digit it holds. */ +export function formatRecordedNumber( + value: number, + formatNumber: FractionDigitsFormat, + printed?: number | null, +): string { + const { value: rounded, fractionDigits } = recordedPrecision(value, printed); + return formatNumber(rounded, fractionDigits); +} diff --git a/src/lib/labs/__tests__/format-value.test.ts b/src/lib/labs/__tests__/format-value.test.ts index 85ad43216..5ae63a988 100644 --- a/src/lib/labs/__tests__/format-value.test.ts +++ b/src/lib/labs/__tests__/format-value.test.ts @@ -3,30 +3,65 @@ import { describe, expect, it } from "vitest"; import { makeFormatters } from "@/lib/format-locale"; import { formatLabReading, formatLabValue } from "../format-value"; -const de = (n: number) => makeFormatters("de", "UTC", "AUTO", "AUTO").number(n); -const en = (n: number) => makeFormatters("en", "UTC", "AUTO", "AUTO").number(n); +const de = makeFormatters("de", "UTC", "AUTO", "AUTO").number; +const en = makeFormatters("en", "UTC", "AUTO", "AUTO").number; +const labNumber = (value: number, printed?: number | null) => + formatLabValue(value, de, printed); describe("lab value formatting", () => { - it("trims to two decimals and prints the locale's separator", () => { + // These readings used to be trimmed to two decimals above 1 and three + // significant digits below, so 12.3456 read "12.35" and 0.123456 "0.123": + // digits the lab reported were dropped. A reading now keeps them all. + it("keeps every recorded digit and prints the locale's separator", () => { expect(formatLabValue(1.8, de)).toBe("1,8"); expect(formatLabValue(1.8, en)).toBe("1.8"); - expect(formatLabValue(0.123456, de)).toBe("0,123"); - expect(formatLabValue(12.3456, de)).toBe("12,35"); + expect(formatLabValue(0.123456, de)).toBe("0,123456"); + expect(formatLabValue(12.3456, de)).toBe("12,3456"); expect(formatLabValue(5, de)).toBe("5"); }); - it("never shows a small nonzero reading as zero", () => { + it("never shows a small nonzero reading as zero or shortened", () => { expect(formatLabValue(0.004, de)).toBe("0,004"); expect(formatLabValue(0.012, en)).toBe("0.012"); expect(formatLabValue(0.035, de)).toBe("0,035"); + expect(formatLabValue(0.034, en)).toBe("0.034"); + expect(formatLabValue(0.0012, en)).toBe("0.0012"); + }); + + it("drops binary floating-point noise, never a recorded digit", () => { + expect(formatLabValue(0.1 + 0.2, en)).toBe("0.3"); + expect(formatLabValue(1234.5678, en)).toBe("1,234.5678"); + }); + + it("prints a converted reading with the precision its report printed", () => { + // 5.2 mmol/L of cholesterol converted to mg/dL. + expect(formatLabValue(5.2 * 38.67, en, 5.2)).toBe("201"); + // A four-digit report figure keeps four significant digits. + expect(formatLabValue(5.234 * 38.67, en, 5.234)).toBe("202.4"); + // Never fewer digits than the integer part. + expect(formatLabValue(12.5 * 88.4, en, 12.5)).toBe("1,105"); }); it("formats a numeric reading with its unit and leaves a qualitative one verbatim", () => { expect( - formatLabReading({ value: 61.3, valueText: null, unit: "kg" }, de), + formatLabReading({ value: 61.3, valueText: null, unit: "kg" }, labNumber), ).toBe("61,3 kg"); expect( - formatLabReading({ value: null, valueText: "negativ", unit: "" }, de), + formatLabReading( + { value: null, valueText: "negativ", unit: "" }, + labNumber, + ), ).toBe("negativ"); + expect( + formatLabReading( + { + value: 5.2 * 38.67, + valueText: null, + unit: "mg/dL", + sourceValue: 5.2, + }, + labNumber, + ), + ).toBe("201 mg/dL"); }); }); diff --git a/src/lib/labs/format-value.ts b/src/lib/labs/format-value.ts index 3626d7e4e..cc4baf4ae 100644 --- a/src/lib/labs/format-value.ts +++ b/src/lib/labs/format-value.ts @@ -1,46 +1,57 @@ /** - * v1.18.1 — shared lab-value number formatting. + * Shared lab-value number formatting. * - * The list card, the detail history rows, and the chart tooltip all trim a - * lab reading the same way: whole numbers render bare, fractional values keep - * up to two decimals (no trailing-zero noise), and a value below 1 keeps three - * significant digits so a small reading (TSH 0.004, troponin 0.012) never - * reads as zero. Centralised so the three surfaces stay byte-identical. + * Every surface that prints a lab reading (the list card, the detail history, + * the chart tooltip, the doctor report PDF, the clinician share page) prints + * it through here, so a reading reads the same everywhere. The precision rule + * is the recorded-number rule in `src/lib/format/recorded-number.ts`: a + * reading keeps every digit it was stored with (TSH 0.004, PSA 0.034, a + * troponin of 0.0012 never shortened), and a reading converted from the unit + * its report printed keeps the precision of the printed figure. * - * The digits themselves come from the caller's locale number formatter - * (`useFormatters().number`), so a German reader sees "1,8 mIU/L" where an - * English one sees "1.8 mIU/L". This module only decides the precision. + * The digits themselves come from the caller's locale number formatter, so a + * German reader sees "1,8 mIU/L" where an English one sees "1.8 mIU/L". This + * module only decides the precision. */ -export type LabNumberFormat = (value: number) => string; +import { + formatRecordedNumber, + type FractionDigitsFormat, +} from "@/lib/format/recorded-number"; + +/** + * A lab number in the reader's locale. `printed` is the figure the source + * report printed when the number was converted from it (`sourceValue`). + */ +export type LabNumberFormat = ( + value: number, + printed?: number | null, +) => string; export function formatLabValue( value: number, - formatNumber: LabNumberFormat, + formatNumber: FractionDigitsFormat, + printed?: number | null, ): string { - if (Number.isInteger(value)) return formatNumber(value); - const rounded = - Math.abs(value) < 1 - ? Number(value.toPrecision(3)) - : Number(value.toFixed(2)); - return formatNumber(rounded); + return formatRecordedNumber(value, formatNumber, printed); } /** - * v1.18.9 — render a reading's display value: a qualitative row (numeric - * `value` is null) shows its `valueText` verbatim; a numeric row shows the - * formatted number plus its unit. One helper so the list card, the history - * rows, and the detail header stay consistent. + * Render a reading's display value: a qualitative row (numeric `value` is + * null) shows its `valueText` verbatim; a numeric row shows the formatted + * number plus its unit. One helper so the list card, the history rows, and + * the detail header stay consistent. */ export function formatLabReading( reading: { value: number | null; valueText: string | null; unit: string; + sourceValue?: number | null; }, formatNumber: LabNumberFormat, ): string { if (reading.value === null) { return reading.valueText ?? ""; } - return `${formatLabValue(reading.value, formatNumber)} ${reading.unit}`.trim(); + return `${formatNumber(reading.value, reading.sourceValue)} ${reading.unit}`.trim(); } diff --git a/tests/integration/lab-source-reference-range.test.ts b/tests/integration/lab-source-reference-range.test.ts index 923e72588..7cb112060 100644 --- a/tests/integration/lab-source-reference-range.test.ts +++ b/tests/integration/lab-source-reference-range.test.ts @@ -281,6 +281,15 @@ describe("a printed reference range, end to end", () => { ); expect(labs?.[0].sourceReferenceText).toBeNull(); expect(labs?.[0].referenceLow).toBeCloseTo(3.9 * 18.0182, 6); + // ...but it carries the printed figure, so the converted value prints + // with the precision the report gave it rather than a float's tail. + expect(labs?.[0].sourceValue).toBe(5.6); + const { formatLabValue } = await import("@/lib/labs/format-value"); + const { makeFormatters } = await import("@/lib/format-locale"); + const en = makeFormatters("en", "UTC", "AUTO", "AUTO").number; + expect( + formatLabValue(labs![0].value as number, en, labs![0].sourceValue), + ).toBe("101"); // FHIR: the text, when present, must agree with low and high. Here it is // left out, and the bounds carry the marker's unit like the value. @@ -361,6 +370,8 @@ describe("a printed reference range, end to end", () => { new Date("2026-12-31T23:59:59.000Z"), ); expect(labs?.[0].sourceReferenceText).toBeNull(); + // An edited value is no longer a conversion, and prints as typed. + expect(labs?.[0].sourceValue).toBeNull(); const { labObservations } = await import("@/lib/fhir/resources/labs"); let seq = 0;