feat(apple-health): extend HK identifier mapping with iOS-17 + iOS-18 long-tail

Adds 32 HKQuantity / HKCategory identifiers to HK_QUANTITY_TYPE_DEFERRED
so the batch ingest endpoint logs a "deferred, not unknown" signal when
the iOS app passes a sample type HealthLog hasn't surfaced yet. None
currently have a HealthLog MeasurementType counterpart; mapping them
through requires the enum extension to land first (separate wave).

Buckets covered, with iOS version they shipped in:
  - Cardiovascular clinical (iOS 16): AFib burden, peripheral perfusion
  - Mobility (iOS 14/15): walking steadiness, falls, steadiness event
  - Pulmonary clinical (iOS 17): FEV1, FVC, peak expiratory flow,
    inhaler usage
  - Other quantity types: insulin delivery, UV exposure, electrodermal
    activity, blood-alcohol content, legacy Nike fuel
  - Heart-rhythm event flags (iOS 9+/17 refresh): low/high heart-rate
    events, irregular rhythm, low cardio fitness event
  - Audio-exposure event flags + environmental sound reduction
  - Behavioural: handwashing, toothbrushing
  - Reproductive / fertility / pregnancy: contraceptive, lactation,
    pregnancy, pregnancy/progesterone test, sexual activity, sleep
    changes, persistent intermenstrual bleeding, prolonged / irregular
    / infrequent menstrual cycles

Tests gain explicit coverage that every long-tail identifier sits in
the deferred set and that no identifier is duplicated.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 17:43:37 +02:00
1 parent 41147bd76a
commit b4df3ae8cd
2 files changed
+116

No files matched your search

@@ -119,6 +119,69 @@ describe("HK_QUANTITY_TYPE_DEFERRED", () => {
}
});
// v1.4.25 W16a — explicit coverage gate for the iOS-17 + iOS-18
// long-tail. The brief flagged these as "the iOS app may emit them
// when the user has the relevant HealthKit category enabled"; the
// deferred set means the batch endpoint treats them as
// skipped-but-known rather than unknown-and-dropped.
it("covers the iOS-17 + iOS-18 long-tail identifiers", () => {
const expectedLongTail = [
// Cardiovascular / clinical
"HKQuantityTypeIdentifierAtrialFibrillationBurden",
"HKQuantityTypeIdentifierPeripheralPerfusionIndex",
// Mobility
"HKQuantityTypeIdentifierAppleWalkingSteadiness",
"HKQuantityTypeIdentifierNumberOfTimesFallen",
"HKCategoryTypeIdentifierAppleWalkingSteadinessEvent",
// Respiratory / pulmonary
"HKQuantityTypeIdentifierForcedExpiratoryVolume1",
"HKQuantityTypeIdentifierForcedVitalCapacity",
"HKQuantityTypeIdentifierPeakExpiratoryFlowRate",
"HKQuantityTypeIdentifierInhalerUsage",
// Other quantity identifiers
"HKQuantityTypeIdentifierInsulinDelivery",
"HKQuantityTypeIdentifierUVExposure",
"HKQuantityTypeIdentifierElectrodermalActivity",
"HKQuantityTypeIdentifierBloodAlcoholContent",
"HKQuantityTypeIdentifierNikeFuel",
// Heart-rhythm event flags
"HKCategoryTypeIdentifierLowHeartRateEvent",
"HKCategoryTypeIdentifierHighHeartRateEvent",
"HKCategoryTypeIdentifierIrregularHeartRhythmEvent",
"HKCategoryTypeIdentifierLowCardioFitnessEvent",
// Audio-exposure events
"HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent",
"HKCategoryTypeIdentifierHeadphoneAudioExposureEvent",
"HKCategoryTypeIdentifierEnvironmentalSoundReduction",
// Behavioural / habit
"HKCategoryTypeIdentifierHandwashingEvent",
"HKCategoryTypeIdentifierToothbrushingEvent",
// Reproductive / fertility / pregnancy
"HKCategoryTypeIdentifierContraceptive",
"HKCategoryTypeIdentifierLactation",
"HKCategoryTypeIdentifierPregnancy",
"HKCategoryTypeIdentifierPregnancyTestResult",
"HKCategoryTypeIdentifierProgesteroneTestResult",
"HKCategoryTypeIdentifierSexualActivity",
"HKCategoryTypeIdentifierSleepChanges",
"HKCategoryTypeIdentifierPersistentIntermenstrualBleeding",
"HKCategoryTypeIdentifierProlongedMenstrualPeriods",
"HKCategoryTypeIdentifierIrregularMenstrualCycles",
"HKCategoryTypeIdentifierInfrequentMenstrualCycles",
];
for (const id of expectedLongTail) {
expect(
HK_QUANTITY_TYPE_DEFERRED.has(id),
`${id} should be in the deferred set`,
).toBe(true);
}
});
it("does not duplicate any deferred identifier", () => {
const ids = Array.from(HK_QUANTITY_TYPE_DEFERRED);
expect(new Set(ids).size).toBe(ids.length);
});
it("converts oxygen saturation from 0..1 fraction to 0..100 percent", () => {
const mapping =
APPLE_HEALTH_TYPE_MAP.HKQuantityTypeIdentifierOxygenSaturation;
@@ -382,6 +382,59 @@ export const HK_QUANTITY_TYPE_DEFERRED = new Set<string>([
// Scored assessments (iOS 18) — v1.5 (PHQ-9 / GAD-7 mapping TBD)
"HKScoredAssessmentTypeIdentifierGAD7",
"HKScoredAssessmentTypeIdentifierPHQ9",
// ── v1.4.25 W16a — iOS-17 + iOS-18 long-tail closure ──
// Cardiovascular / clinical (iOS 16+) — no HealthLog counterpart yet;
// clinical decision-support territory, defer behind a stricter user
// opt-in than the existing Health-share prompt.
"HKQuantityTypeIdentifierAtrialFibrillationBurden",
"HKQuantityTypeIdentifierPeripheralPerfusionIndex",
// Mobility (iOS 15+) — surface as a wellness signal in v1.5 once the
// Insights cardio sub-page has room for a steadiness gauge.
"HKQuantityTypeIdentifierAppleWalkingSteadiness",
"HKQuantityTypeIdentifierNumberOfTimesFallen",
"HKCategoryTypeIdentifierAppleWalkingSteadinessEvent",
// Respiratory / pulmonary clinical (iOS 17) — pair with FHIR clinical
// bucket; no Measurement enum mapping today.
"HKQuantityTypeIdentifierForcedExpiratoryVolume1",
"HKQuantityTypeIdentifierForcedVitalCapacity",
"HKQuantityTypeIdentifierPeakExpiratoryFlowRate",
"HKQuantityTypeIdentifierInhalerUsage",
// Other quantity identifiers — explicit hold (privacy / not in scope).
"HKQuantityTypeIdentifierInsulinDelivery",
"HKQuantityTypeIdentifierUVExposure",
"HKQuantityTypeIdentifierElectrodermalActivity",
"HKQuantityTypeIdentifierBloodAlcoholContent",
"HKQuantityTypeIdentifierNikeFuel", // legacy Nike+iPod fitness points
// Heart-rhythm event flags (iOS 9+ but watch-detected; iOS 18
// refreshed surfaces) — would land as Measurement annotations rather
// than rows; defer until the annotation column lands.
"HKCategoryTypeIdentifierLowHeartRateEvent",
"HKCategoryTypeIdentifierHighHeartRateEvent",
"HKCategoryTypeIdentifierIrregularHeartRhythmEvent",
"HKCategoryTypeIdentifierLowCardioFitnessEvent",
// Audio-exposure events (iOS 13+) — the continuous AUDIO_EXPOSURE_*
// quantity identifiers ARE mapped above; these are the
// "loud-event-fired" flags that pair with them. Defer until we
// surface event chips in the Insights audio sub-page.
"HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent",
"HKCategoryTypeIdentifierHeadphoneAudioExposureEvent",
"HKCategoryTypeIdentifierEnvironmentalSoundReduction",
// Behavioural / habit category-types — not in HealthLog scope yet.
"HKCategoryTypeIdentifierHandwashingEvent",
"HKCategoryTypeIdentifierToothbrushingEvent",
// Reproductive / fertility / pregnancy — explicit privacy hold
// (matches the menstrual-flow stance already encoded above).
"HKCategoryTypeIdentifierContraceptive",
"HKCategoryTypeIdentifierLactation",
"HKCategoryTypeIdentifierPregnancy",
"HKCategoryTypeIdentifierPregnancyTestResult",
"HKCategoryTypeIdentifierProgesteroneTestResult",
"HKCategoryTypeIdentifierSexualActivity",
"HKCategoryTypeIdentifierSleepChanges",
"HKCategoryTypeIdentifierPersistentIntermenstrualBleeding",
"HKCategoryTypeIdentifierProlongedMenstrualPeriods",
"HKCategoryTypeIdentifierIrregularMenstrualCycles",
"HKCategoryTypeIdentifierInfrequentMenstrualCycles",
] as const);
/** Input to `mapAppleHealthEntry()`. */