feat(signals): add the signal-registry backbone

Introduce one declarative table describing each health signal across the
surfaces it touches — DB measurement type, detail page, correlation engine,
Coach snapshot, MCP read tool, and FHIR coding — discriminated on kind
(measurement / biomarker / score). Promote the proto-registry in
metric-status-registry.ts to the canonical definition and add thin adapters
that derive the per-surface tables from it.

The registry coexists with the enum-driven code: the DB enum stays the source
of truth for persistence and the registry sits above it. An invariant test
pins every derived table byte-for-byte to the values the hand-maintained
tables carry today, so cross-surface drift fails CI.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 15:12:02 +02:00
1 parent c0a91dbbcd
commit 794d598150
7 files changed
+1654

No files matched your search

@@ -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(),
);
});
});
+21
View File
@@ -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;
}
+29
View File
@@ -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);
}
+44
View File
@@ -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