diff --git a/src/lib/fhir/loinc-map.ts b/src/lib/fhir/loinc-map.ts index 9e1fd4cc0..31c9324cb 100644 --- a/src/lib/fhir/loinc-map.ts +++ b/src/lib/fhir/loinc-map.ts @@ -9,6 +9,7 @@ * components 8480-6 / 8462-4), so the two BP component types are NOT in * this single-value map. */ +import { deriveMeasurementLoinc } from "@/lib/signals/adapters/fhir"; export const LOINC_SYSTEM = "http://loinc.org"; export const UCUM_SYSTEM = "http://unitsofmeasure.org"; @@ -39,238 +40,8 @@ export interface LoincMapping { * panel). Glucose is handled per-context by the builder using * `GLUCOSE_LOINC`. */ -export const MEASUREMENT_LOINC: Record = { - WEIGHT: { - loinc: "29463-7", - display: "Body weight", - unit: "kg", - category: "vital-signs", - }, - BODY_MASS_INDEX: { - loinc: "39156-5", - display: "Body mass index (BMI) [Ratio]", - unit: "kg/m2", - category: "vital-signs", - }, - PULSE: { - loinc: "8867-4", - display: "Heart rate", - unit: "/min", - category: "vital-signs", - }, - RESTING_HEART_RATE: { - loinc: "40443-4", - display: "Heart rate --resting", - unit: "/min", - category: "vital-signs", - }, - RESPIRATORY_RATE: { - loinc: "9279-1", - display: "Respiratory rate", - unit: "/min", - category: "vital-signs", - }, - BODY_TEMPERATURE: { - loinc: "8310-5", - display: "Body temperature", - unit: "Cel", - category: "vital-signs", - }, - OXYGEN_SATURATION: { - loinc: "59408-5", - display: "Oxygen saturation in Arterial blood by Pulse oximetry", - unit: "%", - category: "vital-signs", - }, - BODY_FAT: { - loinc: "41982-0", - display: "Percentage of body fat Measured", - unit: "%", - category: "vital-signs", - }, - VO2_MAX: { - loinc: "96402-2", - display: "Oxygen consumption maximum during exercise", - unit: "mL/min/kg", - category: "vital-signs", - }, - HEART_RATE_VARIABILITY: { - loinc: "80404-7", - display: "R-R interval.standard deviation (Heart rate variability)", - unit: "ms", - category: "vital-signs", - }, - ACTIVITY_STEPS: { - loinc: "41950-7", - display: "Number of steps in 24 hour Measured", - unit: "{steps}", - category: "activity", - }, - // Stored canonically in MINUTES; the builder converts the emitted value to - // hours so it matches the UCUM `h` unit (and the iOS table). PDF unaffected. - SLEEP_DURATION: { - loinc: "93832-4", - display: "Sleep duration", - unit: "h", - category: "activity", - }, - ACTIVE_ENERGY_BURNED: { - loinc: "41981-2", - display: "Calories burned", - unit: "kcal", - category: "activity", - }, - WALKING_SPEED: { - loinc: "41957-2", - display: "Gait speed [Velocity] Measured", - unit: "m/s", - category: "vital-signs", - }, - WALKING_ASYMMETRY: { - loinc: "91557-1", - display: "Walking asymmetry percentage", - unit: "%", - category: "vital-signs", - }, - WALKING_STEP_LENGTH: { - loinc: "41955-6", - display: "Step length Measured", - unit: "m", - category: "vital-signs", - }, - // Body-composition family with iOS-locked LOINC codes. - TOTAL_BODY_WATER: { - loinc: "73704-9", - display: "Body water by Bioelectrical impedance analysis", - unit: "kg", - category: "vital-signs", - }, - BONE_MASS: { - loinc: "73708-0", - display: "Bone mineral content by DXA", - unit: "kg", - category: "vital-signs", - }, - // 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", - unit: "%", - category: "vital-signs", - }, - AUDIO_EXPOSURE_ENV: { - loinc: "HKQuantityTypeIdentifierEnvironmentalAudioExposure", - display: "Environmental audio exposure", - unit: "dB[A]", - category: "vital-signs", - }, - AUDIO_EXPOSURE_HEADPHONE: { - loinc: "HKQuantityTypeIdentifierHeadphoneAudioExposure", - display: "Headphone audio exposure", - unit: "dB[A]", - category: "vital-signs", - }, - FLIGHTS_CLIMBED: { - loinc: "HKQuantityTypeIdentifierFlightsClimbed", - display: "Flights climbed", - unit: "{flights}", - category: "activity", - }, - WALKING_RUNNING_DISTANCE: { - loinc: "HKQuantityTypeIdentifierDistanceWalkingRunning", - display: "Distance walking/running", - unit: "m", - category: "activity", - }, - TIME_IN_DAYLIGHT: { - loinc: "HKQuantityTypeIdentifierTimeInDaylight", - display: "Time in daylight", - unit: "min", - category: "activity", - }, - MUSCLE_MASS: { - loinc: null, - display: "Muscle mass", - unit: "kg", - category: "vital-signs", - }, - FAT_MASS: { - loinc: null, - display: "Fat mass", - unit: "kg", - category: "vital-signs", - }, - FAT_FREE_MASS: { - loinc: null, - display: "Fat-free mass", - unit: "kg", - category: "vital-signs", - }, - LEAN_BODY_MASS: { - loinc: null, - display: "Lean body mass", - unit: "kg", - category: "vital-signs", - }, - VISCERAL_FAT: { - loinc: null, - display: "Visceral fat", - unit: "1", - category: "vital-signs", - }, - // ── v1.10.0 — additive HealthKit signals (WX-A) ── - // No stable LOINC term for these Apple-specific quantities, so each - // routes through the shared HEALTHKIT_CODESYSTEM with the raw - // HKQuantityTypeIdentifier… string as the code (a non-LOINC code under - // the LOINC namespace would fail FHIR conformance). - CARDIO_RECOVERY: { - loinc: "HKQuantityTypeIdentifierHeartRateRecoveryOneMinute", - display: "Cardio recovery (1-minute heart-rate recovery)", - unit: "/min", - category: "vital-signs", - }, - WRIST_TEMPERATURE: { - loinc: "HKQuantityTypeIdentifierAppleSleepingWristTemperature", - display: "Sleeping wrist temperature", - unit: "Cel", - category: "vital-signs", - }, - FALL_COUNT: { - loinc: "HKQuantityTypeIdentifierNumberOfTimesFallen", - display: "Number of times fallen", - unit: "{falls}", - category: "activity", - }, - SIX_MINUTE_WALK_DISTANCE: { - loinc: "HKQuantityTypeIdentifierSixMinuteWalkTestDistance", - display: "Six-minute walk test distance", - unit: "m", - category: "activity", - }, - STAIR_ASCENT_SPEED: { - loinc: "HKQuantityTypeIdentifierStairAscentSpeed", - display: "Stair ascent speed", - unit: "m/s", - category: "vital-signs", - }, - STAIR_DESCENT_SPEED: { - loinc: "HKQuantityTypeIdentifierStairDescentSpeed", - display: "Stair descent speed", - unit: "m/s", - category: "vital-signs", - }, - BREATHING_DISTURBANCES: { - loinc: "HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances", - display: "Sleeping breathing disturbances", - unit: "{count}", - category: "vital-signs", - }, -}; +export const MEASUREMENT_LOINC: Record = + deriveMeasurementLoinc(); /** BP panel + component LOINC codes. */ export const BP_PANEL_LOINC = "85354-9"; diff --git a/src/lib/insights/features.ts b/src/lib/insights/features.ts index b8de9b84a..76d3e0afe 100644 --- a/src/lib/insights/features.ts +++ b/src/lib/insights/features.ts @@ -29,6 +29,7 @@ import { getMedicationCategories } from "@/lib/medication-category"; import { classifyReferenceRange } from "@/lib/labs/reference-range"; import { resolveLabFields } from "@/lib/labs/serialise"; import { readRollupBuckets } from "@/lib/rollups/measurement-rollups"; +import { deriveBucketedTypes } from "@/lib/signals/adapters/correlation"; import { ensureUserMoodRollupsFresh, readMoodDayRollups, @@ -464,15 +465,11 @@ const BUCKET_WINDOWS: Array<{ * block was suppressed. New `MeasurementType` enum values flow in by * adding one row; the rollup populator already covers every type. */ -const BUCKETED_TYPES: MeasurementType[] = [ - "WEIGHT", - "BLOOD_PRESSURE_SYS", - "BLOOD_PRESSURE_DIA", - "PULSE", - "BODY_FAT", - "SLEEP_DURATION", - "ACTIVITY_STEPS", -]; +// Derived from the signal registry: every signal flagged +// `surfaces.correlationEligible` projects to its DB `MeasurementType`. The list +// is a membership/iteration set (each type is read independently), so order is +// not significant; the registry-invariant test pins the set byte-for-byte. +const BUCKETED_TYPES: MeasurementType[] = deriveBucketedTypes(); function stdDev(values: number[]): number | null { if (values.length < 2) return null;