mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
Merge branch 'feat/v125-signal-registry' into release/v1.25.0
This commit is contained in:
commit
40689862b2
9 files changed
+1663
-241
No files matched your search
+3
-232
@@ -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<string, LoincMapping> = {
|
||||
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<string, LoincMapping> =
|
||||
deriveMeasurementLoinc();
|
||||
|
||||
/** BP panel + component LOINC codes. */
|
||||
export const BP_PANEL_LOINC = "85354-9";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
"WEIGHT",
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
"BLOOD_PRESSURE_DIA",
|
||||
"PULSE",
|
||||
"BODY_FAT",
|
||||
"SLEEP_DURATION",
|
||||
"ACTIVITY_STEPS"
|
||||
]
|
||||
@@ -0,0 +1,218 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Registry invariants — the drift guard that pins the SIGNAL REGISTRY to the
|
||||
* values the per-surface tables carry today. Any edit that changes a derived
|
||||
* table (FHIR coding, correlation eligibility, metric-status metadata) without
|
||||
* intentionally updating the golden master fails here, so a registry flip stays
|
||||
* a byte-for-byte no-op and accidental cross-surface drift cannot land.
|
||||
*
|
||||
* Golden masters under `./fixtures/*` were extracted verbatim from the
|
||||
* hand-maintained tables at the v1.25 branch base; update them deliberately
|
||||
* (and in lock-step with the registry) when a value genuinely changes.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
METRIC_STATUS_IDS,
|
||||
getMetricStatusMeta,
|
||||
} from "@/lib/insights/metric-status-registry";
|
||||
import { MEASUREMENT_LOINC } from "@/lib/fhir/loinc-map";
|
||||
import {
|
||||
SIGNALS,
|
||||
allSignals,
|
||||
getSignal,
|
||||
signalForMeasurementType,
|
||||
} from "@/lib/signals/registry";
|
||||
import { deriveMetricStatusRegistry } from "@/lib/signals/adapters/metric-status";
|
||||
import { deriveMeasurementLoinc } from "@/lib/signals/adapters/fhir";
|
||||
import { deriveBucketedTypes } from "@/lib/signals/adapters/correlation";
|
||||
|
||||
import expectedMeasurementLoinc from "./fixtures/expected-measurement-loinc.json";
|
||||
import expectedBucketedTypes from "./fixtures/expected-bucketed-types.json";
|
||||
|
||||
describe("signal registry — structural sanity", () => {
|
||||
it("every entry's key matches its record key", () => {
|
||||
for (const [key, signal] of Object.entries(SIGNALS)) {
|
||||
expect(signal.key).toBe(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("every signal carries a well-formed source for its kind", () => {
|
||||
for (const signal of allSignals()) {
|
||||
if (signal.kind === "biomarker") {
|
||||
expect(signal.source.biomarkerKey.length).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect(signal.source.measurementType.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("mcp exposure follows Coach-snapshot inclusion", () => {
|
||||
// The MCP get_metric_series enum is the Coach scope vocabulary, so a signal
|
||||
// is MCP-readable iff it carries a Coach scope (and vice-versa).
|
||||
for (const signal of allSignals()) {
|
||||
const hasScope = signal.surfaces.coachSnapshot !== false;
|
||||
expect(signal.surfaces.mcp).toBe(hasScope);
|
||||
}
|
||||
});
|
||||
|
||||
it("signalForMeasurementType round-trips every measurement/score signal", () => {
|
||||
for (const signal of allSignals()) {
|
||||
if (signal.kind === "biomarker") continue;
|
||||
expect(signalForMeasurementType(signal.source.measurementType)).toBe(
|
||||
signal,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("signal registry — metric-status projection equals the live table", () => {
|
||||
it("registers every generic metric-status id", () => {
|
||||
for (const id of METRIC_STATUS_IDS) {
|
||||
expect(getSignal(id)).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("derives each MetricStatusMeta byte-for-byte from the registry", () => {
|
||||
const derived = deriveMetricStatusRegistry();
|
||||
for (const id of METRIC_STATUS_IDS) {
|
||||
expect(derived[id]).toEqual(getMetricStatusMeta(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("signal registry — FHIR table is byte-identical", () => {
|
||||
it("derives MEASUREMENT_LOINC equal to the golden master", () => {
|
||||
expect(deriveMeasurementLoinc()).toEqual(expectedMeasurementLoinc);
|
||||
});
|
||||
|
||||
it("the live (flipped) MEASUREMENT_LOINC export equals the golden master", () => {
|
||||
expect(MEASUREMENT_LOINC).toEqual(expectedMeasurementLoinc);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signal registry — correlation table is byte-identical", () => {
|
||||
it("derives BUCKETED_TYPES equal to the golden master (set semantics)", () => {
|
||||
expect([...deriveBucketedTypes()].sort()).toEqual(
|
||||
[...(expectedBucketedTypes as string[])].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Adapter: derive the correlation engine's bucketed-type list from the
|
||||
* registry. Replaces the hand-maintained `BUCKETED_TYPES` literal in
|
||||
* `features.ts` — every signal flagged `surfaces.correlationEligible` projects
|
||||
* to its DB `MeasurementType`. The list is consumed as a membership/iteration
|
||||
* set (each type is read independently), so order is not semantically
|
||||
* significant; the registry-invariant test compares it as a set.
|
||||
*/
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { allSignals } from "@/lib/signals/registry";
|
||||
|
||||
/** The registry-derived correlation-eligible `MeasurementType` list. */
|
||||
export function deriveBucketedTypes(): MeasurementType[] {
|
||||
const types: MeasurementType[] = [];
|
||||
for (const signal of allSignals()) {
|
||||
if (signal.kind === "biomarker") continue;
|
||||
if (!signal.surfaces.correlationEligible) continue;
|
||||
types.push(signal.source.measurementType);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Adapter: derive the single-value `MeasurementType → FHIR coding` table from
|
||||
* the registry. Replaces the hand-maintained `MEASUREMENT_LOINC` literal in
|
||||
* `loinc-map.ts` — every signal that carries a `fhir` facet projects to one
|
||||
* entry keyed by its DB `MeasurementType`. Signals without a `fhir` facet are
|
||||
* absent (their value is surfaced as a local text concept elsewhere, or handled
|
||||
* by a dedicated builder, e.g. BP panel / glucose context).
|
||||
*/
|
||||
import type { LoincMapping } from "@/lib/fhir/loinc-map";
|
||||
import { allSignals } from "@/lib/signals/registry";
|
||||
|
||||
/** The registry-derived `MEASUREMENT_LOINC`, keyed by `MeasurementType` string. */
|
||||
export function deriveMeasurementLoinc(): Record<string, LoincMapping> {
|
||||
const entries: Array<[string, LoincMapping]> = [];
|
||||
for (const signal of allSignals()) {
|
||||
if (signal.kind === "biomarker") continue;
|
||||
if (!signal.fhir) continue;
|
||||
entries.push([
|
||||
signal.source.measurementType,
|
||||
{
|
||||
loinc: signal.fhir.loinc,
|
||||
display: signal.fhir.display,
|
||||
unit: signal.fhir.unit,
|
||||
category: signal.fhir.category,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Adapter: project the registry back to the `MetricStatusMeta` shape the
|
||||
* generic assessment path consumes today. The hand-written `REGISTRY` in
|
||||
* `metric-status-registry.ts` stays the live source for the assessment card
|
||||
* (untouched in this change); this projection exists so the registry-invariant
|
||||
* test can assert the two agree field-for-field and flag any future drift.
|
||||
*/
|
||||
import {
|
||||
METRIC_STATUS_IDS,
|
||||
type MetricStatusMeta,
|
||||
type MetricStatusMetricId,
|
||||
} from "@/lib/insights/metric-status-registry";
|
||||
import { getSignal } from "@/lib/signals/registry";
|
||||
|
||||
/** Build the `MetricStatusMeta` for one registered metric-status id. */
|
||||
function metaFor(id: MetricStatusMetricId): MetricStatusMeta {
|
||||
const signal = getSignal(id);
|
||||
if (!signal || signal.kind === "biomarker") {
|
||||
throw new Error(`signal registry: no measurement signal for "${id}"`);
|
||||
}
|
||||
// Build field-by-field, omitting the optional keys when absent so the
|
||||
// projection matches the hand-written object shape exactly (deepEqual).
|
||||
const meta: MetricStatusMeta = {
|
||||
id,
|
||||
measurementType: signal.source.measurementType,
|
||||
displayName: signal.displayName,
|
||||
unit: signal.unit,
|
||||
direction: signal.direction,
|
||||
archetype: signal.archetype,
|
||||
};
|
||||
if (signal.normalRange !== undefined) meta.normalRange = signal.normalRange;
|
||||
if (signal.feverBandC !== undefined) meta.feverBandC = signal.feverBandC;
|
||||
return meta;
|
||||
}
|
||||
|
||||
/** The full registry-derived metric-status table, keyed by metric-status id. */
|
||||
export function deriveMetricStatusRegistry(): Record<
|
||||
MetricStatusMetricId,
|
||||
MetricStatusMeta
|
||||
> {
|
||||
return Object.fromEntries(
|
||||
METRIC_STATUS_IDS.map((id) => [id, metaFor(id)]),
|
||||
) as Record<MetricStatusMetricId, MetricStatusMeta>;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user