diff --git a/src/lib/measurements/__tests__/apple-health-mapping.test.ts b/src/lib/measurements/__tests__/apple-health-mapping.test.ts index 66544ea21..d109e739c 100644 --- a/src/lib/measurements/__tests__/apple-health-mapping.test.ts +++ b/src/lib/measurements/__tests__/apple-health-mapping.test.ts @@ -78,15 +78,16 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set([ // 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", diff --git a/src/lib/measurements/apple-health-mapping.ts b/src/lib/measurements/apple-health-mapping.ts index d6be7265a..39db69181 100644 --- a/src/lib/measurements/apple-health-mapping.ts +++ b/src/lib/measurements/apple-health-mapping.ts @@ -220,6 +220,22 @@ export const APPLE_HEALTH_TYPE_MAP: Record = { 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: {