fix(export): move HealthKit placeholder codes off the LOINC namespace

Six HealthKit metrics without a published LOINC term were coded under
http://loinc.org, which fails FHIR R4 conformance (a non-LOINC code must
not sit in the LOINC namespace). Route any HKQuantityTypeIdentifier code
onto a shared custom CodeSystem (https://healthlog.dev/fhir/CodeSystem/healthkit),
keeping the raw HealthKit identifier as the code so the export stays
byte-aligned with the native client's exporter.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 20:46:49 +02:00
1 parent 0f7ca61e2f
commit b5e98533b8
3 files changed
+62 -5

No files matched your search

@@ -398,6 +398,44 @@ describe("buildFhirDocumentBundle", () => {
expect(flights?.valueQuantity?.code).toBe("{flights}");
});
it("routes HealthKit placeholder codes onto the custom CodeSystem, never LOINC", () => {
const bundle = buildFhirDocumentBundle(
makeData({
measurements: {
WEIGHT: [{ value: 80, measuredAt: "2026-04-30T08:00:00.000Z" }],
FLIGHTS_CLIMBED: [
{ value: 12, measuredAt: "2026-04-30T08:00:00.000Z" },
],
},
}),
{ insuranceNumber: null },
FIXED_NOW,
);
const codings = observationsOf(bundle).flatMap((o) => o.code.coding ?? []);
// A real LOINC metric keeps the LOINC namespace.
const weight = codings.find((c) => c.code === "29463-7");
expect(weight?.system).toBe("http://loinc.org");
// The HealthKit placeholder moves to the shared custom CodeSystem with the
// raw HK identifier as the code (byte-aligned with the iOS exporter).
const flightsCoding = codings.find(
(c) => c.code === "HKQuantityTypeIdentifierFlightsClimbed",
);
expect(flightsCoding?.system).toBe(
"https://healthlog.dev/fhir/CodeSystem/healthkit",
);
// Conformance invariant: no HealthKit identifier is ever emitted under the
// LOINC namespace anywhere in the bundle.
const hkUnderLoinc = codings.filter(
(c) =>
c.system === "http://loinc.org" &&
c.code?.startsWith("HKQuantityTypeIdentifier"),
);
expect(hkUnderLoinc).toHaveLength(0);
});
it("discriminates glucose LOINC by context (random/fasting/afterMeal)", () => {
const bundle = buildFhirDocumentBundle(
makeData({
+9 -1
View File
@@ -21,6 +21,7 @@ import type { DoctorReportData } from "@/lib/doctor-report-data";
import { resolveGlucoseUnit, convertGlucose } from "@/lib/glucose";
import {
LOINC_SYSTEM,
HEALTHKIT_CODESYSTEM,
UCUM_SYSTEM,
MEASUREMENT_LOINC,
BP_PANEL_LOINC,
@@ -66,8 +67,15 @@ function escapeXml(value: string): string {
function codeableFromMapping(m: LoincMapping): FhirCodeableConcept {
if (m.loinc) {
// HealthKit placeholder codes have no published LOINC term; they must not
// sit under the LOINC namespace (a non-LOINC code there is a conformance
// violation). Route them onto the shared custom CodeSystem instead —
// byte-aligned with the iOS exporter.
const system = m.loinc.startsWith("HKQuantityTypeIdentifier")
? HEALTHKIT_CODESYSTEM
: LOINC_SYSTEM;
return {
coding: [{ system: LOINC_SYSTEM, code: m.loinc, display: m.display }],
coding: [{ system, code: m.loinc, display: m.display }],
text: m.display,
};
}
+15 -4
View File
@@ -12,6 +12,15 @@
export const LOINC_SYSTEM = "http://loinc.org";
export const UCUM_SYSTEM = "http://unitsofmeasure.org";
/**
* Shared custom CodeSystem for HealthKit placeholder metrics that have no
* published LOINC term. A non-LOINC code under `http://loinc.org` is a FHIR
* conformance violation, so these route here instead. Byte-aligned with the
* iOS exporter (confirmed 2026-06-01) — both clients emit the identical
* `system` + raw `HKQuantityTypeIdentifier…` `code`.
*/
export const HEALTHKIT_CODESYSTEM =
"https://healthlog.dev/fhir/CodeSystem/healthkit";
export type FhirObservationCategory =
| "vital-signs"
@@ -145,10 +154,12 @@ export const MEASUREMENT_LOINC: Record<string, LoincMapping> = {
unit: "kg",
category: "vital-signs",
},
// HK-placeholder codes — no published LOINC term. iOS emits the HealthKit
// identifier STRING as the `code` (in the LOINC system slot); the server
// must emit the identical placeholder, not invent a LOINC. The `loinc`
// field below carries that placeholder string verbatim.
// HK-placeholder codes — no published LOINC term. Both iOS and the server
// emit the HealthKit identifier STRING as the `code` under the shared
// `HEALTHKIT_CODESYSTEM` (NOT the LOINC namespace — a non-LOINC code under
// http://loinc.org fails FHIR conformance). The `loinc` field below carries
// the placeholder string verbatim; the builder routes any
// `HKQuantityTypeIdentifier…` code onto the custom system.
WALKING_DOUBLE_SUPPORT: {
loinc: "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage",
display: "Walking double support percentage",