mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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:
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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user