feat(measurements): map HealthKit waist circumference to WAIST_CIRCUMFERENCE

HealthKit emits HKQuantityTypeIdentifierWaistCircumference as a length
quantity in metres, but the mapping table had no entry for it, so a batch
upload of that identifier was silently dropped (mapAppleHealthEntry returned
null). Add the bidirectional mapping with metre-to-cm scaling so the round
trip lands on the canonical WAIST_CIRCUMFERENCE measurement (cm), matching
getUnitForType and the WHO display bands. Extend the mapping test.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 18:59:11 +02:00
1 parent 8eaa8a714e
commit e93f74381e
2 files changed
+42 -5

No files matched your search

@@ -78,15 +78,16 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
// resilience metric. No HK mapping by design.
"RESILIENCE",
// v1.25 — clinical-signals wave. The mental-health screener totals are
// derived from an in-app questionnaire, and grip strength / pain NRS / waist
// circumference + waist-to-height are manual / instrument readings — none
// ships a first-class HealthKit quantity we ingest, so they have no HK
// mapping by design.
// derived from an in-app questionnaire, and grip strength / pain NRS are
// manual / instrument readings — none ships a first-class HealthKit quantity
// we ingest, so they have no HK mapping by design. Waist-to-height is a
// server-derived ratio (no HK quantity). `WAIST_CIRCUMFERENCE` DOES round-
// trip via `HKQuantityTypeIdentifierWaistCircumference`, so it is NOT listed
// here.
"PHQ9_SCORE",
"GAD7_SCORE",
"GRIP_STRENGTH",
"PAIN_NRS",
"WAIST_CIRCUMFERENCE",
"WAIST_TO_HEIGHT",
]);
@@ -288,6 +289,26 @@ describe("HK_QUANTITY_TYPE_DEFERRED", () => {
expect(mapping.convertToDbUnit(0.245)).toBeCloseTo(24.5);
});
it("round-trips waist circumference from HK metres to canonical cm", () => {
const mapping =
APPLE_HEALTH_TYPE_MAP.HKQuantityTypeIdentifierWaistCircumference;
expect(mapping.measurementType).toBe("WAIST_CIRCUMFERENCE");
expect(mapping.dbUnit).toBe("cm");
// 0.84 m → 84 cm.
expect(mapping.convertToDbUnit(0.84)).toBeCloseTo(84);
const out = mapAppleHealthEntry({
hkIdentifier: "HKQuantityTypeIdentifierWaistCircumference",
value: 0.92,
unit: "m",
startDate: "2026-06-28T08:00:00.000Z",
endDate: "2026-06-28T08:00:00.000Z",
});
expect(out).not.toBeNull();
expect(out!.type).toBe("WAIST_CIRCUMFERENCE");
expect(out!.value).toBeCloseTo(92);
expect(out!.unit).toBe("cm");
});
it("uses identity conversion for the SI-aligned identifiers", () => {
for (const id of [
"HKQuantityTypeIdentifierBodyMass",
@@ -220,6 +220,22 @@ export const APPLE_HEALTH_TYPE_MAP: Record<string, AppleHealthMapping> = {
convertToDbUnit: (v) => v,
aggregation: "latest",
},
// Waist circumference — HealthKit ships this as a length quantity in the SI
// base unit (metres), the same wire convention the other length identifiers
// follow (`DistanceWalkingRunning`, `WalkingStepLength`). It is the first
// length metric whose canonical DB unit (cm) differs from the metre wire
// unit, so it scales ×100 server-side — the length sibling of the percent
// metrics' ×100 path. WHO risk bands are evaluated in cm at display
// (men > 94 / women > 80; see `norms.ts`).
HKQuantityTypeIdentifierWaistCircumference: {
hkIdentifier: "HKQuantityTypeIdentifierWaistCircumference",
measurementType: "WAIST_CIRCUMFERENCE",
hkUnit: "m",
dbUnit: "cm",
// Apple ships length in metres; HealthLog stores waist in cm.
convertToDbUnit: (v) => v * 100,
aggregation: "latest",
},
// ── Cardiovascular ──────────────────────────────────────────
HKQuantityTypeIdentifierBloodPressureSystolic: {