Promote the reproductive HealthKit category types out of the Apple-Health-import privacy-hold list and route them into cycle day-logs instead of measurements, and surface cycle data on the three export surfaces. Import promotion: - A new HealthKit -> cycle mapping module is the single source of truth for how MenstrualFlow, IntermenstrualBleeding, CervicalMucusQuality, OvulationTestResult, SexualActivity, PregnancyTestResult, ProgesteroneTestResult, Contraceptive and the symptom category types fold into CycleDayLog fields / seeded symptom keys. The XML importer accumulates same-day reproductive samples and writes one canonical day-log per day with source APPLE_HEALTH and a synthetic per-day externalId for idempotent re-import. The fold is gated on cycle tracking being enabled, so a non-cycle export never provisions cycle rows. A contraceptive method nudges the profile goal toward avoiding pregnancy only when the goal is still on its default. Basal body temperature stays a measurement and mirrors onto the day-log. Pregnancy/Lactation status and the pregnancy-bleeding types stay deferred; the cycle awareness types remain server-derived. Export: - FHIR emits LMP (LOINC 8665-2), average cycle length (64700-8), period length (64698-4) and the current phase as opt-in reproductive-health observations grouped into their own document section. - The doctor PDF gains an opt-in cycle section (recent cycles, average length and variability, current phase), privacy-default off like mood. - The full-backup export, the weekly backup worker and the off-host DR snapshot include the cycle tables, and the admin restore re-creates them; the day-log note round-trips as ciphertext and is never decrypted onto a report surface. Mapping table documented in docs/api/cycle-healthkit-mapping.md. Adds unit + integration coverage for the mapping, the import routing, the FHIR observations and the backup round-trip. OpenAPI regenerated. Six-locale strings added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.7 KiB
Apple Health → cycle mapping
How HealthLog folds Apple Health reproductive samples into the cycle model.
Both the export.xml importer and the iOS batch contract share one mapping
table: src/lib/cycle/healthkit-mapping.ts. This page is the human-readable
companion — keep the two in sync.
Reproductive samples route into cycle day-logs (CycleDayLog), never into
Measurement. Each Apple Health day collapses into one canonical day-log per
(user, date); the importer writes source = APPLE_HEALTH with a synthetic
per-day externalId (hkcycle:<YYYY-MM-DD>) so a re-import upserts the same
row. The fold only runs when cycle tracking is enabled for the account.
Day-log fields
| HealthKit category type | CycleDayLog field | Values |
|---|---|---|
MenstrualFlow |
flow |
unspecified→LIGHT, light→LIGHT, medium→MEDIUM, heavy→HEAVY, none→NONE |
IntermenstrualBleeding |
intermenstrualBleeding |
presence → true |
CervicalMucusQuality |
cervicalMucus |
dry→DRY, sticky→STICKY, creamy→CREAMY, watery→WATERY, eggWhite→EGG_WHITE |
OvulationTestResult |
ovulationTest |
negative→NEGATIVE, lhSurge→POSITIVE_LH_SURGE, indeterminate→INDETERMINATE, estrogenSurge→ESTROGEN_SURGE |
SexualActivity |
sexualActivity + protectedSex |
presence → true; HKMetadataKeySexualActivityProtectionUsed → protectedSex |
PregnancyTestResult |
pregnancyTest |
negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
ProgesteroneTestResult |
progesteroneTest |
negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
Contraceptive |
contraceptive (+ profile nudge) |
unspecified→UNSPECIFIED, implant→IMPLANT, injection→INJECTION, iud→IUD, ring→INTRAVAGINAL_RING, oral→ORAL, patch→PATCH, emergency→EMERGENCY |
HealthLog's SPOTTING flow level has no HealthKit counterpart; it is only ever
produced by a manual entry. HealthKit unspecified flow maps to LIGHT (a
logged-but-unspecified bleeding day is at least light flow).
Symptom category types → seeded symptom keys
Symptom category samples link to the seeded CycleSymptom catalogue by key. A
sample whose severity is not present (codepoint 1) does NOT create a link.
| HealthKit category type | CycleSymptom key |
|---|---|
AbdominalCramps |
cramps |
Headache |
headache |
Bloating |
bloating |
Acne |
acne |
BreastPain |
breast_tenderness |
Fatigue |
fatigue |
LowerBackPain |
back_pain |
SleepChanges |
insomnia |
MoodChanges |
mood_swings |
AppetiteChanges |
food_cravings |
Nausea |
nausea |
Profile-level signals
A Contraceptive sample asserting an active method also nudges the
CycleProfile.goal to AVOID_PREGNANCY — but only when the goal is still on
its GENERAL_HEALTH default, so an explicit user choice is never overwritten.
Basal body temperature
basalBodyTemperature is not routed here. It stays a
Measurement(BODY_TEMPERATURE) (the system of record for the rollup tier and
the cross-source source-priority ladder) and is mirrored onto
CycleDayLog.basalBodyTempC for the cycle chart. Apple Watch wrist/skin
temperature already flows to WRIST_TEMPERATURE / SKIN_TEMPERATURE and feeds
the temperature-trend ovulation method directly from Measurement.
Deferred
| HealthKit category type | Status |
|---|---|
Pregnancy, Lactation |
Deferred — no CycleProfile column yet (pregnancy-mode is a later release). |
BleedingDuringPregnancy, BleedingAfterPregnancy |
Deferred — pregnancy-mode. |
IrregularMenstrualCycles, InfrequentMenstrualCycles, ProlongedMenstrualPeriods, PersistentIntermenstrualBleeding |
Server-derived from the cycle engine, never ingested. |