merge(reconcile): t1-drcohere audit findings

This commit is contained in:
Marc-André Bombeck committed 2026-06-16 03:02:55 +02:00
commit 98d1e7ae31
13 files changed
+566 -9

No files matched your search

+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Wert",
"labsColReference": "Referenz",
"labsColStatus": "Status",
"labsColDate": "Datum"
"labsColDate": "Datum",
"glucoseClinical": {
"title": "Glukose-Übersicht (klinisch)",
"colMetric": "Kennzahl",
"colValue": "Wert",
"tirInRange": "Zeit im Zielbereich (70–180 mg/dl)",
"tirLow": "Niedrig (< 70 mg/dl)",
"tirVeryLow": "Sehr niedrig (< 54 mg/dl)",
"tirHigh": "Hoch (> 180 mg/dl)",
"tirVeryHigh": "Sehr hoch (> 250 mg/dl)",
"mean": "Durchschnittliche Glukose",
"gmi": "Glukose-Management-Indikator (GMI)",
"eA1c": "Geschätzter HbA1c",
"cv": "Variabilität (VK%)",
"spotCaveat": "Dies sind Schätzwerte aus Einzelmessungen, kein Profil eines kontinuierlichen Messgeräts — als Tendenz und nicht als klinisches Urteil zu lesen."
}
},
"errorBoundary": {
"title": "Da ist etwas schiefgelaufen",
+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Value",
"labsColReference": "Reference",
"labsColStatus": "Status",
"labsColDate": "Date"
"labsColDate": "Date",
"glucoseClinical": {
"title": "Glucose overview (clinical)",
"colMetric": "Metric",
"colValue": "Value",
"tirInRange": "Time in range (70–180 mg/dL)",
"tirLow": "Low (< 70 mg/dL)",
"tirVeryLow": "Very low (< 54 mg/dL)",
"tirHigh": "High (> 180 mg/dL)",
"tirVeryHigh": "Very high (> 250 mg/dL)",
"mean": "Average glucose",
"gmi": "Glucose Management Indicator (GMI)",
"eA1c": "Estimated A1C",
"cv": "Variability (CV%)",
"spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile — read them as a direction of travel rather than a clinical verdict."
}
},
"errorBoundary": {
"title": "Something went wrong",
+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Valor",
"labsColReference": "Referencia",
"labsColStatus": "Estado",
"labsColDate": "Fecha"
"labsColDate": "Fecha",
"glucoseClinical": {
"title": "Resumen de glucosa (clínico)",
"colMetric": "Métrica",
"colValue": "Valor",
"tirInRange": "Tiempo en rango (70–180 mg/dl)",
"tirLow": "Bajo (< 70 mg/dl)",
"tirVeryLow": "Muy bajo (< 54 mg/dl)",
"tirHigh": "Alto (> 180 mg/dl)",
"tirVeryHigh": "Muy alto (> 250 mg/dl)",
"mean": "Glucosa media",
"gmi": "Indicador de gestión de la glucosa (GMI)",
"eA1c": "A1C estimada",
"cv": "Variabilidad (CV%)",
"spotCaveat": "Son estimaciones a partir de mediciones puntuales, no un perfil de monitor continuo — léelas como una tendencia, no como un veredicto clínico."
}
},
"errorBoundary": {
"title": "Algo ha salido mal",
+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Valeur",
"labsColReference": "Référence",
"labsColStatus": "Statut",
"labsColDate": "Date"
"labsColDate": "Date",
"glucoseClinical": {
"title": "Aperçu glycémique (clinique)",
"colMetric": "Indicateur",
"colValue": "Valeur",
"tirInRange": "Temps dans la cible (70–180 mg/dl)",
"tirLow": "Bas (< 70 mg/dl)",
"tirVeryLow": "Très bas (< 54 mg/dl)",
"tirHigh": "Élevé (> 180 mg/dl)",
"tirVeryHigh": "Très élevé (> 250 mg/dl)",
"mean": "Glycémie moyenne",
"gmi": "Indicateur de gestion du glucose (GMI)",
"eA1c": "HbA1c estimée",
"cv": "Variabilité (CV%)",
"spotCaveat": "Ce sont des estimations issues de mesures ponctuelles, et non un profil de capteur continu — à lire comme une tendance, pas comme un verdict clinique."
}
},
"errorBoundary": {
"title": "Une erreur est survenue",
+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Valore",
"labsColReference": "Riferimento",
"labsColStatus": "Stato",
"labsColDate": "Data"
"labsColDate": "Data",
"glucoseClinical": {
"title": "Panoramica glicemia (clinica)",
"colMetric": "Metrica",
"colValue": "Valore",
"tirInRange": "Tempo nell'intervallo (70–180 mg/dl)",
"tirLow": "Basso (< 70 mg/dl)",
"tirVeryLow": "Molto basso (< 54 mg/dl)",
"tirHigh": "Alto (> 180 mg/dl)",
"tirVeryHigh": "Molto alto (> 250 mg/dl)",
"mean": "Glicemia media",
"gmi": "Indicatore di gestione del glucosio (GMI)",
"eA1c": "A1C stimata",
"cv": "Variabilità (CV%)",
"spotCaveat": "Sono stime da misurazioni occasionali, non un profilo da monitor continuo — vanno lette come una tendenza, non come un verdetto clinico."
}
},
"errorBoundary": {
"title": "Qualcosa è andato storto",
+16 -1
View File
@@ -189,7 +189,22 @@
"labsColValue": "Wartość",
"labsColReference": "Norma",
"labsColStatus": "Status",
"labsColDate": "Data"
"labsColDate": "Data",
"glucoseClinical": {
"title": "Przegląd glukozy (kliniczny)",
"colMetric": "Wskaźnik",
"colValue": "Wartość",
"tirInRange": "Czas w zakresie (70–180 mg/dl)",
"tirLow": "Niski (< 70 mg/dl)",
"tirVeryLow": "Bardzo niski (< 54 mg/dl)",
"tirHigh": "Wysoki (> 180 mg/dl)",
"tirVeryHigh": "Bardzo wysoki (> 250 mg/dl)",
"mean": "Średnia glukoza",
"gmi": "Wskaźnik zarządzania glukozą (GMI)",
"eA1c": "Szacowane HbA1c",
"cv": "Zmienność (CV%)",
"spotCaveat": "To szacunki z pojedynczych pomiarów, nie profil z monitora ciągłego — czytaj je jako kierunek zmian, a nie kliniczny werdykt."
}
},
"errorBoundary": {
"title": "Coś poszło nie tak",
Binary file not shown.
@@ -439,3 +439,52 @@ describe("doctor-report-pdf-core type-map coverage", () => {
expect(text).toContain("Oxygen saturation");
});
});
// ── clinical glucose panel ── the server-computed TIR / GMI / eA1C panel must
// render in the PDF whenever glucose readings exist, and stay absent otherwise.
describe("doctor-report clinical glucose panel", () => {
function denseReadings(): { measuredAt: Date; mgdl: number }[] {
const out: { measuredAt: Date; mgdl: number }[] = [];
for (let d = 0; d < 60; d += 1) {
for (let h = 0; h < 24; h += 1) {
out.push({
measuredAt: new Date(
FIXED_NOW.getTime() - d * 86_400_000 - h * 3_600_000,
),
mgdl: 110 + ((d + h) % 7) * 8,
});
}
}
return out;
}
it("renders the clinical panel title + TIR/GMI rows when readings exist", async () => {
const data = makeData({
glucoseClinical: computeGlucoseClinicalMetrics(denseReadings(), {
now: FIXED_NOW,
windowDays: 90,
}),
});
expect(data.glucoseClinical.readingCount).toBeGreaterThan(0);
const bytes = renderDoctorReportPdfBytes(data, {
t: getServerTranslator("en").t,
locale: "en",
now: FIXED_NOW,
});
const text = await extractText(bytes);
expect(text).toContain("Glucose overview (clinical)");
expect(text).toContain("Time in range");
expect(text).toContain("Glucose Management Indicator");
});
it("omits the clinical panel when there are no glucose readings", async () => {
const data = makeData(); // empty (zero-reading) glucose panel
const bytes = renderDoctorReportPdfBytes(data, {
t: getServerTranslator("en").t,
locale: "en",
now: FIXED_NOW,
});
const text = await extractText(bytes);
expect(text).not.toContain("Glucose overview (clinical)");
});
});
+87 -3
View File
@@ -25,7 +25,11 @@ import {
import type {
GlucoseContext,
MeasurementSource,
MeasurementType,
} from "@/generated/prisma/client";
import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority";
import { metricKeyForType } from "@/lib/measurements/cumulative-day-sum";
import { userDayKey } from "@/lib/tz/resolver";
import { resolveCanonicalRecovery } from "@/lib/insights/derived/recovery-resolve";
import {
reconstructSleepNights,
@@ -288,6 +292,70 @@ interface RecoveryMeasurementRow {
source: MeasurementSource;
}
/** Minimal Measurement shape the canonical-source collapse consults. */
export interface CanonicalCollapseRow {
type: string;
value: number;
measuredAt: Date;
source: MeasurementSource;
deviceType?: string | null;
}
/**
* Collapse a window's raw Measurement rows to one canonical source per metric
* before the doctor report computes per-type avg/min/max. Without this the PDF
* + FHIR stats blend every source (a WHOOP + Apple-Watch resting-heart-rate
* day would average two readings), while the dashboard / insights aggregator
* already collapse to the ladder-canonical source per day. This routes the rows
* through the SAME picker (`pickCanonicalSourceRows`, keyed by
* `metricKeyForType` + the user's day key + source-priority ladder), so the two
* surfaces report identical numbers.
*
* SLEEP_DURATION is passed through untouched: it carries per-stage rows that
* `reconstructSleepNights` resolves with its own source de-dup downstream, so a
* pre-collapse here would drop stages the night reconstruction needs. A type
* with no ladder, or a day with a single source, passes through unchanged via
* the picker's documented fallback. Exported for unit tests.
*/
export function collapseMeasurementsToCanonical<T extends CanonicalCollapseRow>(
measurements: readonly T[],
timezone: string,
sourcePriorityJson: unknown,
): T[] {
const dayKey = (d: Date) => userDayKey(d, timezone);
const byType = new Map<string, T[]>();
for (const m of measurements) {
const slot = byType.get(m.type);
if (slot) slot.push(m);
else byType.set(m.type, [m]);
}
const out: T[] = [];
for (const [type, rows] of byType) {
const metricKey =
type === "SLEEP_DURATION"
? null
: metricKeyForType(type as MeasurementType);
if (!metricKey) {
out.push(...rows);
continue;
}
const { canonicalRows } = pickCanonicalSourceRows(
rows.map((m) => ({
...m,
type: m.type as MeasurementType,
})),
metricKey,
sourcePriorityJson,
dayKey,
);
// `pickCanonicalSourceRows` returns the spread copies; re-key back to the
// original rows by reference identity is unnecessary because the spread
// preserves every field the caller reads (value / measuredAt / type).
out.push(...(canonicalRows as unknown as T[]));
}
return out;
}
/**
* Summarise RECOVERY_SCORE over the window from the CANONICAL row per day:
* a WHOOP-native row wins over the COMPUTED proxy for the same day, so the
@@ -739,18 +807,35 @@ export async function collectDoctorReportData(
}),
]);
// Group measurements by type.
const reportTz = userProfile?.timezone ?? "Europe/Berlin";
// v1.18.0 — collapse each multi-source metric to its CANONICAL source before
// grouping, so the report's per-type avg/min/max match the dashboard rather
// than blending overlapping sources (e.g. WHOOP + Apple Watch resting-heart-
// rate summed into one inflated mean). See `collapseMeasurementsToCanonical`.
const canonicalMeasurements = collapseMeasurementsToCanonical(
measurements,
reportTz,
userProfile?.sourcePriorityJson ?? null,
);
// Group measurements by type (canonical-source-collapsed).
const byType: Record<
string,
Array<{ value: number; measuredAt: string }>
> = {};
for (const m of measurements) {
for (const m of canonicalMeasurements) {
if (!byType[m.type]) byType[m.type] = [];
byType[m.type].push({
value: m.value,
measuredAt: m.measuredAt.toISOString(),
});
}
// measuredAt-ascending within each type so `latest` (last element) is the
// most recent reading after the canonical collapse reorders by bucket.
for (const entries of Object.values(byType)) {
entries.sort((a, b) => a.measuredAt.localeCompare(b.measuredAt));
}
// v1.17.1 parity — SLEEP_DURATION enters `byType` as RAW per-stage rows (a
// 40-min DEEP block, a 12-min AWAKE block, …). Every other sleep surface
@@ -759,7 +844,6 @@ export async function collectDoctorReportData(
// and the v1.17.1 stamping fixes rewrite exactly those raw stage rows. Route
// the report's sleep value through the same engine — one number, one surface
// — exactly as RECOVERY_SCORE routes through `summariseCanonicalRecovery`.
const reportTz = userProfile?.timezone ?? "Europe/Berlin";
const sleepRows = measurements.filter(
(m) => m.type === "SLEEP_DURATION",
) as unknown as SleepStageRow[];
+102
View File
@@ -746,6 +746,108 @@ export function buildDoctorReportPdfDocument(
.finalY + 8;
}
// v1.18.0 — clinical glucose panel (TIR / GMI / eA1C / mean / CV%). The
// server already computes this (`data.glucoseClinical`) by the one
// literature-locked engine the insights panel + coach consume, and zeroes it
// when the glucose module is off (readingCount === 0). Render it whenever the
// window carries glucose readings so the PDF surfaces the same numbers the
// app shows. A `pct` helper turns the engine's 0–1 fractions into percent.
const clinical = data.glucoseClinical;
if (clinical && clinical.readingCount > 0 && clinical.distribution) {
const pct = (fraction: number) => `${num(fraction * 100)} %`;
const clinicalRows: string[][] = [];
const dist = clinical.distribution;
clinicalRows.push([t("doctorReport.glucoseClinical.tirInRange"), pct(dist.tir)]);
clinicalRows.push([t("doctorReport.glucoseClinical.tirLow"), pct(dist.tbrLevel1)]);
clinicalRows.push([t("doctorReport.glucoseClinical.tirVeryLow"), pct(dist.tbrLevel2)]);
clinicalRows.push([t("doctorReport.glucoseClinical.tirHigh"), pct(dist.tarLevel1)]);
clinicalRows.push([
t("doctorReport.glucoseClinical.tirVeryHigh"),
pct(dist.tarLevel2),
]);
if (clinical.meanMgdl !== null) {
clinicalRows.push([
t("doctorReport.glucoseClinical.mean"),
`${num(convertGlucose(clinical.meanMgdl, glucoseUnit))} ${glucoseUnit}`.trim(),
]);
}
if (clinical.gmi !== null) {
clinicalRows.push([t("doctorReport.glucoseClinical.gmi"), `${num(clinical.gmi)} %`]);
}
if (clinical.estimatedA1c !== null) {
clinicalRows.push([
t("doctorReport.glucoseClinical.eA1c"),
`${num(clinical.estimatedA1c)} %`,
]);
}
if (clinical.variability !== null) {
clinicalRows.push([
t("doctorReport.glucoseClinical.cv"),
`${num(clinical.variability.cv)} %`,
]);
}
// Keep the heading with the start of its table.
y = ensureSpace(y, 6 + 14);
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.setTextColor(30, 30, 30);
doc.text(t("doctorReport.glucoseClinical.title"), margin, y);
y += 6;
autoTable(doc, {
startY: y,
head: [
[
t("doctorReport.glucoseClinical.colMetric"),
t("doctorReport.glucoseClinical.colValue"),
],
],
body: clinicalRows,
theme: "grid",
styles: {
fontSize: 9,
cellPadding: 3,
textColor: [30, 30, 30],
lineColor: [200, 200, 200],
lineWidth: 0.3,
},
headStyles: {
fillColor: [245, 245, 245],
textColor: [30, 30, 30],
fontStyle: "bold",
},
alternateRowStyles: { fillColor: [252, 252, 252] },
margin: {
left: margin,
right: margin,
top: margin,
bottom: tableBottomMargin,
},
});
y =
(doc as jsPDF & { lastAutoTable: { finalY: number } }).lastAutoTable
.finalY + 4;
// Spot-reading caveat when the engine flags the readings as sparse, so the
// clinician reads the panel as a direction-of-travel, not a CGM AGP.
if (clinical.isSpotEstimate) {
doc.setFontSize(8);
doc.setFont("helvetica", "italic");
doc.setTextColor(110, 110, 110);
const caveat = doc.splitTextToSize(
t("doctorReport.glucoseClinical.spotCaveat"),
pageWidth - 2 * margin,
);
for (const line of caveat) {
y = ensureSpace(y, 4);
doc.text(line, margin, y);
y += 4;
}
}
y += 4;
}
// v1.7.0 — jsPDF-native trend sparklines per primary vital. Vector
// polylines drawn with `doc.lines()` — zero new dependency, no native
// canvas module, isomorphic. Selection-gated via `includeCharts`.
@@ -0,0 +1,113 @@
import { describe, it, expect } from "vitest";
import { computeGlucoseClinicalMetrics } from "@/lib/analytics/glucose-metrics";
import type { DoctorReportData } from "@/lib/doctor-report-data";
import {
GLUCOSE_TIR_LOINC,
GLUCOSE_GMI_LOINC,
GLUCOSE_MEAN_LOINC,
GLUCOSE_EA1C_LOINC,
} from "@/lib/fhir/loinc-map";
import { buildFhirDocumentBundle } from "../build-bundle";
import type { FhirObservation } from "../types";
const FIXED_NOW = new Date("2026-05-03T12:00:00.000Z");
function makeData(overrides?: Partial<DoctorReportData>): DoctorReportData {
return {
period: {
days: 90,
since: "2026-02-02T00:00:00.000Z",
start: "2026-02-02T00:00:00.000Z",
end: "2026-05-03T12:00:00.000Z",
},
patient: {
username: "sample-user",
dateOfBirth: "1985-06-15T00:00:00.000Z",
gender: "MALE",
heightCm: 182,
},
practiceName: null,
measurements: {},
stats: {},
glucoseStats: {},
glucoseRanges: {},
glucoseClinical: computeGlucoseClinicalMetrics([], { now: FIXED_NOW }),
glucoseUnit: "mg/dL",
bmi: null,
compliance: {},
medications: [],
mood: null,
glp1: null,
...overrides,
};
}
function observationsOf(bundle: ReturnType<typeof buildFhirDocumentBundle>) {
return bundle.entry
.map((e) => e.resource)
.filter((r): r is FhirObservation => r.resourceType === "Observation");
}
function codeOf(obs: FhirObservation): string | undefined {
return obs.code.coding?.[0]?.code;
}
/** Dense daily readings over the window so the panel populates fully. */
function denseReadings(): { measuredAt: Date; mgdl: number }[] {
const out: { measuredAt: Date; mgdl: number }[] = [];
for (let d = 0; d < 60; d += 1) {
for (let h = 0; h < 24; h += 1) {
out.push({
measuredAt: new Date(
FIXED_NOW.getTime() - d * 86_400_000 - h * 3_600_000,
),
// Mostly in-range with a wobble so CV% / TIR are real.
mgdl: 110 + ((d + h) % 7) * 8,
});
}
}
return out;
}
describe("clinical glucose-panel FHIR Observations", () => {
it("emits TIR / GMI / mean / eA1C Observations when glucose readings exist", () => {
const glucoseClinical = computeGlucoseClinicalMetrics(denseReadings(), {
now: FIXED_NOW,
windowDays: 90,
});
expect(glucoseClinical.readingCount).toBeGreaterThan(0);
const bundle = buildFhirDocumentBundle(makeData({ glucoseClinical }), { insuranceNumber: "A123456780" }, FIXED_NOW);
const codes = observationsOf(bundle).map(codeOf);
expect(codes).toContain(GLUCOSE_TIR_LOINC);
expect(codes).toContain(GLUCOSE_GMI_LOINC);
expect(codes).toContain(GLUCOSE_MEAN_LOINC);
expect(codes).toContain(GLUCOSE_EA1C_LOINC);
});
it("emits a percent-valued, UCUM-coded TIR Observation", () => {
const glucoseClinical = computeGlucoseClinicalMetrics(denseReadings(), {
now: FIXED_NOW,
windowDays: 90,
});
const bundle = buildFhirDocumentBundle(makeData({ glucoseClinical }), { insuranceNumber: "A123456780" }, FIXED_NOW);
const tir = observationsOf(bundle).find((o) => codeOf(o) === GLUCOSE_TIR_LOINC);
expect(tir).toBeDefined();
expect(tir!.valueQuantity?.code).toBe("%");
expect(tir!.valueQuantity?.value).toBeGreaterThan(0);
expect(tir!.valueQuantity?.value).toBeLessThanOrEqual(100);
});
it("emits NO clinical glucose Observation when the panel has no readings (module off / no data)", () => {
// Default makeData() has an empty (zero-reading) panel.
const bundle = buildFhirDocumentBundle(makeData(), { insuranceNumber: "A123456780" }, FIXED_NOW);
const codes = observationsOf(bundle).map(codeOf);
expect(codes).not.toContain(GLUCOSE_TIR_LOINC);
expect(codes).not.toContain(GLUCOSE_GMI_LOINC);
expect(codes).not.toContain(GLUCOSE_MEAN_LOINC);
expect(codes).not.toContain(GLUCOSE_EA1C_LOINC);
});
});
+15
View File
@@ -304,6 +304,21 @@ export const GLUCOSE_LOINC: Record<string, { loinc: string; display: string }> =
BEDTIME: { loinc: "2339-0", display: "Glucose [Mass/volume] in Blood" },
};
/* ── Clinical glucose-panel LOINCs (v1.18.0) ───────────────────────────
* The CGM/spot-reading composite metrics the doctor report computes by the
* one literature-locked engine. Each carries the published LOINC term where
* one exists; the variability CV% has no published LOINC and is emitted as a
* survey-category text-only concept by the builder (honest about what we have).
*/
/** Time in range 70–180 mg/dL [Battelino 2019]. */
export const GLUCOSE_TIR_LOINC = "97510-2";
/** Glucose Management Indicator (GMI) [Bergenstal 2018]. */
export const GLUCOSE_GMI_LOINC = "97506-0";
/** Mean glucose over the reporting period. */
export const GLUCOSE_MEAN_LOINC = "97507-8";
/** Estimated A1C (ADAG, derived from mean glucose). */
export const GLUCOSE_EA1C_LOINC = "41995-2";
/** Medication-adherence Observation LOINC. */
export const MEDICATION_ADHERENCE_LOINC = "71799-1";
/** Mood Observation LOINC (opt-in only). */
+104
View File
@@ -29,6 +29,10 @@ import {
BP_DIA_LOINC,
BP_UNIT,
GLUCOSE_LOINC,
GLUCOSE_TIR_LOINC,
GLUCOSE_GMI_LOINC,
GLUCOSE_MEAN_LOINC,
GLUCOSE_EA1C_LOINC,
MEDICATION_ADHERENCE_LOINC,
MOOD_LOINC,
LMP_LOINC,
@@ -511,6 +515,106 @@ export function observationsFromReportData(
});
}
// --- Clinical glucose-panel Observations (v1.18.0) ---------------------
// The server-computed TIR / GMI / estimated-A1C / mean / CV% over the report
// period, by the one literature-locked engine the insights panel + coach +
// PDF consume. Emitted only when readings exist (the aggregator zeroes the
// panel when the glucose module is off, so an absent panel = module off OR no
// data). Each carries the published LOINC where one exists; CV% has none and
// rides a `survey` text-only concept. Percent metrics use UCUM `%`.
const clinical = data.glucoseClinical;
if (clinical && clinical.readingCount > 0) {
const pushClinical = (
opts: {
loinc?: string;
text: string;
value: number;
unit: string;
ucum: string;
survey?: boolean;
},
) => {
obsSeq += 1;
push({
resourceType: "Observation",
id: `obs-${obsSeq}`,
status: "final",
category: [categoryConcept(opts.survey ? "survey" : "laboratory")],
code: opts.loinc
? {
coding: [
{ system: LOINC_SYSTEM, code: opts.loinc, display: opts.text },
],
text: opts.text,
}
: { text: opts.text },
subject: patientRef,
effectiveDateTime: data.period.end,
valueQuantity: {
value: opts.value,
unit: opts.unit,
system: UCUM_SYSTEM,
code: opts.ucum,
},
...(clinical.isSpotEstimate
? {
note: [
{
text: "Spot-reading estimate from individual measurements, not a continuous-monitor profile.",
},
],
}
: {}),
});
};
if (clinical.distribution) {
pushClinical({
loinc: GLUCOSE_TIR_LOINC,
text: "Glucose time in range (70–180 mg/dL)",
value: Math.round(clinical.distribution.tir * 1000) / 10,
unit: "%",
ucum: "%",
});
}
if (clinical.meanMgdl !== null) {
pushClinical({
loinc: GLUCOSE_MEAN_LOINC,
text: "Mean glucose",
value: convertGlucose(clinical.meanMgdl, glucoseUnit),
unit: glucoseUnit,
ucum: glucoseUnit === "mmol/L" ? "mmol/L" : "mg/dL",
});
}
if (clinical.gmi !== null) {
pushClinical({
loinc: GLUCOSE_GMI_LOINC,
text: "Glucose Management Indicator (GMI)",
value: Math.round(clinical.gmi * 10) / 10,
unit: "%",
ucum: "%",
});
}
if (clinical.estimatedA1c !== null) {
pushClinical({
loinc: GLUCOSE_EA1C_LOINC,
text: "Estimated A1C",
value: Math.round(clinical.estimatedA1c * 10) / 10,
unit: "%",
ucum: "%",
});
}
if (clinical.variability !== null) {
pushClinical({
text: "Glucose variability (coefficient of variation)",
value: Math.round(clinical.variability.cv * 10) / 10,
unit: "%",
ucum: "%",
survey: true,
});
}
}
// --- Lab-result Observations (v1.17.1) ---------------------------------
// One `laboratory` Observation per recorded analyte. `analyte` + `unit`
// are user-recorded free text (no closed LOINC enum), so the code is a