Files
HealthLog/src/lib/signals/registry.ts
T
Marc-André Bombeck cc8ea3dfbb feat(measurements): accept indoor CO2, temperature and humidity
Room climate from a home automation system gets three types of its own:
INDOOR_CO2 (ppm), INDOOR_TEMPERATURE (celsius, shown in Fahrenheit for
imperial readers) and INDOOR_HUMIDITY (percent). They arrive through the
existing ingest token (measurements:write, source EXTERNAL), sit in the
environment cluster beside daylight, and the daily rollup carries their
mean, low and high like any spot reading. They never mix with the body
temperatures or with the outdoor environment channels.

The Home Assistant page documents the three types, a rest_command that
sends a room as one request, a single-room automation, and an
importable blueprint (docs/integrations/blueprints) that skips sensors
reporting unknown or non-numeric states, converts Fahrenheit, and
stamps readings to the minute so a retried push is refused rather than
written twice.
2026-10-21 20:26:15 +02:00

2022 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* v1.25 — the SIGNAL REGISTRY backbone.
*
* One declarative table that describes a health signal ACROSS every surface
* it touches (DB measurement type, detail page, correlation engine, Coach
* snapshot, MCP read tool, FHIR coding). Today a single signal is described
* 8–16 times in independent tables keyed by slightly different vocabularies,
* and nothing fails the build when they disagree. This registry promotes the
* proto-registry in `metric-status-registry.ts` to the canonical definition
* and lets the per-surface tables DERIVE from it (see `./adapters/*`) instead
* of being hand-maintained beside it.
*
* Coexistence, not replacement: the DB enum stays the source of truth for
* persistence. The registry sits ABOVE it. Surfaces are flipped one at a time
* to read from the registry; the registry-invariant test pins every derived
* table byte-for-byte to the values the hand-written tables carry today, so a
* flip is a no-op diff and future drift fails CI.
*
* Adding a NEW signal (the clinical-signals wave): append ONE
* `SignalDefinition` object below, add its i18n keys, and — for a
* `measurement`/`score` — one Prisma enum member + migration. The derived
* tables (FHIR, correlation, …) light up with no further per-surface edits.
*/
import type { MeasurementType } from "@/generated/prisma/client";
import type {
MetricArchetype,
MetricDirection,
MetricNormalRange,
} from "@/lib/insights/metric-status-registry";
import type { FhirObservationCategory } from "@/lib/fhir/loinc-map";
import { FEVER_BAND_C } from "@/lib/clinical-floors";
/**
* Discriminator: a device/manual reading, a lab biomarker, a computed score, or
* an environmental exposure. `environment` (v1.25 W-ENV) is NOT backed by a
* `MeasurementType` — its daily value lives in `EnvironmentContext` and feeds
* the correlation engine through the env channel series builder, not the
* measurement-keyed `BUCKETED_TYPES`. It registers here so the env signals get
* a stable key + display metadata in the one cross-surface table.
*/
/** Reuse the assessment registry's direction vocabulary verbatim. */
export type SignalDirection = MetricDirection;
/** Coarse population placement band; the user's own baseline still leads. */
export type SignalBand = MetricNormalRange;
/**
* FHIR R4 coding facet. Field names align with `loinc-map.ts`'s `LoincMapping`
* (`unit` is the UCUM string, also used as the `code`) so the FHIR adapter is a
* straight projection with no renaming.
*/
export interface SignalFhirCoding {
/** LOINC code, or null when no stable LOINC applies (local text fallback). */
loinc: string | null;
display: string;
/** UCUM unit string (also used as the FHIR `code`). */
unit: string;
category: FhirObservationCategory;
}
/** Chart-overlay facet. Typed loosely (string overlayKey) to keep the backbone free of the dashboard-layout runtime dep. */
export interface SignalChart {
overlayKey: string;
color?: string;
yAxisUnit?: string;
/** Display rescale (e.g. m/s → km/h); applied at the chart edge only. */
valueScale?: number;
}
/**
* Per-surface eligibility — declarative flags, not branches scattered across
* files. `coachSnapshot.scope` is the Coach scope-source token (typed as a
* string here so the backbone does not depend on the coach module; the coach
* flip will tighten it to `CoachScopeSource`).
*/
export interface SignalSurfaces {
/** Mount the generic HealthKit/biomarker detail template. */
detailPage: boolean;
/** Feed the FDR correlation channels (⇒ `BUCKETED_TYPES`). */
correlationEligible: boolean;
/** Coach snapshot inclusion + its scope token, or `false` to opt out. */
coachSnapshot: false | { scope: string };
/** Exposed to the MCP `get_metric_series` / search inventory. */
mcp: boolean;
}
interface SignalCommon {
/** Stable signal key — the ONE vocabulary every surface speaks. */
key: string;
/** i18n key prefix resolving `.title`/`.description`/`.chartTitle`/`.emptyState.*`. Optional until the clinical-signals wave fills it per signal. */
i18nPrefix?: string;
/** Stable English display name (the model localises its own prose). */
displayName: string;
/** Canonical storage unit (matches the DB column semantics). */
unit: string;
direction: SignalDirection;
archetype: MetricArchetype;
normalRange?: SignalBand;
/** Single-reading fever band line (°C) for temperature signals. */
feverBandC?: number;
/** Richer clinical bands defer to a `reference-ranges.ts` entry by key. */
referenceMetric?: string;
/** Cross-source dedup ladder key; omit ⇒ not cross-source-deduped. */
sourcePriorityKey?: string;
chart?: SignalChart;
surfaces: SignalSurfaces;
fhir?: SignalFhirCoding;
}
/** How a `measurement`/`score` signal is sourced from the DB enum. */
export interface MeasurementSignalSource {
measurementType: MeasurementType;
/** Fallback measurement type (e.g. an SDNN→RMSSD HRV proxy). */
fallbackMeasurementType?: MeasurementType;
}
/** How a `biomarker` signal is sourced from the labs catalog. */
export interface BiomarkerSignalSource {
biomarkerKey: string;
}
/**
* How an `environment` signal is sourced (v1.25 W-ENV). The value is a daily
* `EnvironmentContext` column, surfaced to the correlation engine under
* `environmentChannelKey` (the `ENV_*` channel key from
* `@/lib/environment/fields`). No `MeasurementType` — env data is never a
* Measurement row.
*/
export interface EnvironmentSignalSource {
environmentChannelKey: string;
}
/**
* The canonical cross-surface signal definition, discriminated on `kind`.
* `measurement` and `score` both resolve to a `MeasurementType`; `biomarker`
* resolves to a labs-catalog key.
*/
export type SignalDefinition =
| (SignalCommon & { kind: "measurement"; source: MeasurementSignalSource })
| (SignalCommon & { kind: "score"; source: MeasurementSignalSource })
| (SignalCommon & { kind: "biomarker"; source: BiomarkerSignalSource })
| (SignalCommon & { kind: "environment"; source: EnvironmentSignalSource });
/** Convenience: build a Coach-snapshot facet from a scope token. */
function coachScope(scope: string): { scope: string } {
return { scope };
}
/**
* The registry. Keyed by stable signal key. Generic HealthKit signals key by
* their `metric-status-registry` id (STEPS, RESTING_HEART_RATE, …); the six
* specialised signals (WEIGHT/BMI/PULSE/BODY_FAT/BP_SYS/BP_DIA) key by their
* `MeasurementType` and carry `detailPage:false` so the generic template never
* double-renders them — they still appear in the cross-surface indices (FHIR,
* correlation, Coach, MCP).
*
* Every value here is the value the hand-written per-surface table carries
* today; the registry-invariant test pins them byte-for-byte.
*/
export const SIGNALS: Record<string, SignalDefinition> = {
// ── physiological-vital ──────────────────────────────────────────────
RESTING_HEART_RATE: {
key: "RESTING_HEART_RATE",
kind: "measurement",
source: { measurementType: "RESTING_HEART_RATE" },
sourcePriorityKey: "restingHeartRate",
i18nPrefix: "insights.restingHr",
displayName: "Resting heart rate",
unit: "bpm",
direction: "lower-better",
archetype: "physiological-vital",
normalRange: { low: 50, high: 100 },
referenceMetric: "RESTING_HEART_RATE",
chart: {
overlayKey: "restingHr",
color: "var(--destructive)",
yAxisUnit: "bpm",
},
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("resting_hr"),
mcp: true,
},
fhir: {
loinc: "40443-4",
display: "Heart rate --resting",
unit: "/min",
category: "vital-signs",
},
},
HEART_RATE_VARIABILITY: {
key: "HEART_RATE_VARIABILITY",
kind: "measurement",
source: { measurementType: "HEART_RATE_VARIABILITY" },
sourcePriorityKey: "hrv",
displayName: "Heart-rate variability (SDNN)",
unit: "ms",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("hrv"),
mcp: true,
},
fhir: {
loinc: "80404-7",
display: "R-R interval.standard deviation (Heart rate variability)",
unit: "ms",
category: "vital-signs",
},
},
OXYGEN_SATURATION: {
key: "OXYGEN_SATURATION",
kind: "measurement",
source: { measurementType: "OXYGEN_SATURATION" },
sourcePriorityKey: "spo2",
displayName: "Blood oxygen (SpO₂)",
unit: "%",
direction: "higher-better",
archetype: "physiological-vital",
normalRange: { low: 95, high: 100 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("spo2"),
mcp: true,
},
fhir: {
loinc: "59408-5",
display: "Oxygen saturation in Arterial blood by Pulse oximetry",
unit: "%",
category: "vital-signs",
},
},
RESPIRATORY_RATE: {
key: "RESPIRATORY_RATE",
kind: "measurement",
source: { measurementType: "RESPIRATORY_RATE" },
sourcePriorityKey: "respiratoryRate",
displayName: "Respiratory rate",
unit: "breaths/min",
direction: "target-band",
archetype: "physiological-vital",
normalRange: { low: 12, high: 20 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("respiratory_rate"),
mcp: true,
},
fhir: {
loinc: "9279-1",
display: "Respiratory rate",
unit: "/min",
category: "vital-signs",
},
},
BODY_TEMPERATURE: {
key: "BODY_TEMPERATURE",
kind: "measurement",
source: { measurementType: "BODY_TEMPERATURE" },
sourcePriorityKey: "bodyTemperature",
displayName: "Body temperature",
unit: "°C",
direction: "target-band",
archetype: "physiological-vital",
normalRange: { low: 36.1, high: 37.2 },
feverBandC: FEVER_BAND_C,
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("body_temp"),
mcp: true,
},
fhir: {
loinc: "8310-5",
display: "Body temperature",
unit: "Cel",
category: "vital-signs",
},
},
SKIN_TEMPERATURE: {
key: "SKIN_TEMPERATURE",
kind: "measurement",
source: { measurementType: "SKIN_TEMPERATURE" },
sourcePriorityKey: "skinTemperature",
displayName: "Wrist skin temperature",
unit: "°C",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("skin_temp"),
mcp: true,
},
},
BLOOD_GLUCOSE: {
key: "BLOOD_GLUCOSE",
kind: "measurement",
source: { measurementType: "BLOOD_GLUCOSE" },
displayName: "Blood glucose",
unit: "mg/dL",
direction: "target-band",
archetype: "physiological-vital",
normalRange: { low: 70, high: 140 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("glucose"),
mcp: true,
},
},
WALKING_HEART_RATE_AVERAGE: {
key: "WALKING_HEART_RATE_AVERAGE",
kind: "measurement",
source: { measurementType: "WALKING_HEART_RATE_AVERAGE" },
displayName: "Walking heart rate",
unit: "bpm",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_hr"),
mcp: true,
},
},
PULSE_WAVE_VELOCITY: {
key: "PULSE_WAVE_VELOCITY",
kind: "measurement",
source: { measurementType: "PULSE_WAVE_VELOCITY" },
displayName: "Pulse-wave velocity",
unit: "m/s",
direction: "lower-better",
archetype: "physiological-vital",
normalRange: { low: 0, high: 10 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("pulse_wave_velocity"),
mcp: true,
},
},
VASCULAR_AGE: {
key: "VASCULAR_AGE",
kind: "measurement",
source: { measurementType: "VASCULAR_AGE" },
displayName: "Vascular age",
unit: "years",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("vascular_age"),
mcp: true,
},
},
CARDIO_RECOVERY: {
key: "CARDIO_RECOVERY",
kind: "measurement",
source: { measurementType: "CARDIO_RECOVERY" },
displayName: "Cardio recovery",
unit: "bpm",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierHeartRateRecoveryOneMinute",
display: "Cardio recovery (1-minute heart-rate recovery)",
unit: "/min",
category: "vital-signs",
},
},
WRIST_TEMPERATURE: {
key: "WRIST_TEMPERATURE",
kind: "measurement",
source: { measurementType: "WRIST_TEMPERATURE" },
displayName: "Wrist temperature",
unit: "°C",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierAppleSleepingWristTemperature",
display: "Sleeping wrist temperature",
unit: "Cel",
category: "vital-signs",
},
},
ANS_CHARGE: {
key: "ANS_CHARGE",
kind: "measurement",
source: { measurementType: "ANS_CHARGE" },
displayName: "ANS charge",
unit: "score",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
AVERAGE_HEART_RATE: {
key: "AVERAGE_HEART_RATE",
kind: "measurement",
source: { measurementType: "AVERAGE_HEART_RATE" },
displayName: "Average heart rate",
unit: "bpm",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
MAX_HEART_RATE: {
key: "MAX_HEART_RATE",
kind: "measurement",
source: { measurementType: "MAX_HEART_RATE" },
displayName: "Max heart rate",
unit: "bpm",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
// ── activity-fitness ─────────────────────────────────────────────────
STEPS: {
key: "STEPS",
kind: "measurement",
source: { measurementType: "ACTIVITY_STEPS" },
sourcePriorityKey: "steps",
displayName: "Steps",
unit: "steps/day",
direction: "higher-better",
archetype: "activity-fitness",
normalRange: { low: 8000, high: 15000 },
surfaces: {
detailPage: true,
correlationEligible: true,
coachSnapshot: coachScope("steps"),
mcp: true,
},
fhir: {
loinc: "41950-7",
display: "Number of steps in 24 hour Measured",
unit: "{steps}",
category: "activity",
},
},
ACTIVE_ENERGY: {
key: "ACTIVE_ENERGY",
kind: "measurement",
source: { measurementType: "ACTIVE_ENERGY_BURNED" },
sourcePriorityKey: "activeEnergy",
displayName: "Active energy",
unit: "kcal/day",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("active_energy"),
mcp: true,
},
fhir: {
loinc: "41981-2",
display: "Calories burned",
unit: "kcal",
category: "activity",
},
},
FLIGHTS_CLIMBED: {
key: "FLIGHTS_CLIMBED",
kind: "measurement",
source: { measurementType: "FLIGHTS_CLIMBED" },
sourcePriorityKey: "flightsClimbed",
displayName: "Flights climbed",
unit: "flights/day",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("flights"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierFlightsClimbed",
display: "Flights climbed",
unit: "{flights}",
category: "activity",
},
},
WALKING_RUNNING_DISTANCE: {
key: "WALKING_RUNNING_DISTANCE",
kind: "measurement",
source: { measurementType: "WALKING_RUNNING_DISTANCE" },
sourcePriorityKey: "walkingRunningDistance",
displayName: "Walking + running distance",
unit: "m/day",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("distance"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierDistanceWalkingRunning",
display: "Distance walking/running",
unit: "m",
category: "activity",
},
},
TIME_IN_DAYLIGHT: {
key: "TIME_IN_DAYLIGHT",
kind: "measurement",
source: { measurementType: "TIME_IN_DAYLIGHT" },
displayName: "Time in daylight",
unit: "min/day",
direction: "higher-better",
archetype: "activity-fitness",
normalRange: { low: 30, high: 120 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("daylight"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierTimeInDaylight",
display: "Time in daylight",
unit: "min",
category: "activity",
},
},
VO2_MAX: {
key: "VO2_MAX",
kind: "measurement",
source: { measurementType: "VO2_MAX" },
sourcePriorityKey: "vo2Max",
displayName: "VO₂ max (cardio fitness)",
unit: "mL/(kg·min)",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("vo2_max"),
mcp: true,
},
fhir: {
// 96402-2, used before, does not exist in LOINC. The watch estimate is
// not a measured exercise-test VO2 peak (94122-9), so the HealthKit
// identifier rides the custom system.
loinc: "HKQuantityTypeIdentifierVO2Max",
display: "VO2 max (estimated)",
unit: "mL/min/kg",
category: "vital-signs",
},
},
DAY_STRAIN: {
key: "DAY_STRAIN",
kind: "measurement",
source: { measurementType: "DAY_STRAIN" },
displayName: "Day strain",
unit: "score",
direction: "target-band",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
WORKOUT_STRAIN: {
key: "WORKOUT_STRAIN",
kind: "measurement",
source: { measurementType: "WORKOUT_STRAIN" },
displayName: "Workout strain",
unit: "score",
direction: "target-band",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
CARDIO_LOAD: {
key: "CARDIO_LOAD",
kind: "measurement",
source: { measurementType: "CARDIO_LOAD" },
displayName: "Cardio load",
unit: "score",
direction: "target-band",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
ENERGY_EXPENDITURE_KJ: {
key: "ENERGY_EXPENDITURE_KJ",
kind: "measurement",
source: { measurementType: "ENERGY_EXPENDITURE_KJ" },
displayName: "Energy expenditure",
unit: "kJ",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
// ── body-composition ─────────────────────────────────────────────────
TOTAL_BODY_WATER: {
key: "TOTAL_BODY_WATER",
kind: "measurement",
source: { measurementType: "TOTAL_BODY_WATER" },
displayName: "Total body water",
unit: "kg",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("total_body_water"),
mcp: true,
},
fhir: {
// 73704-9, used before, is a nasogastric fluid output.
loinc: "101683-1",
display: "Body water mass",
unit: "kg",
category: "vital-signs",
},
},
BONE_MASS: {
key: "BONE_MASS",
kind: "measurement",
source: { measurementType: "BONE_MASS" },
displayName: "Bone mass",
unit: "kg",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("bone_mass"),
mcp: true,
},
fhir: {
// 73708-0, used before, is "Body fat [Mass] Calculated".
loinc: "101685-6",
display: "Body bone mass",
unit: "kg",
category: "vital-signs",
},
},
FAT_FREE_MASS: {
key: "FAT_FREE_MASS",
kind: "measurement",
source: { measurementType: "FAT_FREE_MASS" },
displayName: "Fat-free mass",
unit: "kg",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("fat_free_mass"),
mcp: true,
},
fhir: {
loinc: null,
display: "Fat-free mass",
unit: "kg",
category: "vital-signs",
},
},
FAT_MASS: {
key: "FAT_MASS",
kind: "measurement",
source: { measurementType: "FAT_MASS" },
displayName: "Fat mass",
unit: "kg",
direction: "lower-better",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("fat_mass"),
mcp: true,
},
fhir: {
loinc: null,
display: "Fat mass",
unit: "kg",
category: "vital-signs",
},
},
MUSCLE_MASS: {
key: "MUSCLE_MASS",
kind: "measurement",
source: { measurementType: "MUSCLE_MASS" },
displayName: "Muscle mass",
unit: "kg",
direction: "higher-better",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("muscle_mass"),
mcp: true,
},
fhir: {
loinc: null,
display: "Muscle mass",
unit: "kg",
category: "vital-signs",
},
},
LEAN_BODY_MASS: {
key: "LEAN_BODY_MASS",
kind: "measurement",
source: { measurementType: "LEAN_BODY_MASS" },
displayName: "Lean body mass",
unit: "kg",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("lean_body_mass"),
mcp: true,
},
fhir: {
loinc: null,
display: "Lean body mass",
unit: "kg",
category: "vital-signs",
},
},
VISCERAL_FAT: {
key: "VISCERAL_FAT",
kind: "measurement",
source: { measurementType: "VISCERAL_FAT" },
displayName: "Visceral fat rating",
unit: "rating",
direction: "lower-better",
archetype: "body-composition",
normalRange: { low: 1, high: 12 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("visceral_fat"),
mcp: true,
},
fhir: {
loinc: null,
display: "Visceral fat",
unit: "1",
category: "vital-signs",
},
},
// ── mobility-gait ────────────────────────────────────────────────────
WALKING_STEADINESS: {
key: "WALKING_STEADINESS",
kind: "measurement",
source: { measurementType: "WALKING_STEADINESS" },
displayName: "Walking steadiness",
unit: "%",
direction: "higher-better",
archetype: "mobility-gait",
normalRange: { low: 50, high: 100 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_steadiness"),
mcp: true,
},
},
WALKING_ASYMMETRY: {
key: "WALKING_ASYMMETRY",
kind: "measurement",
source: { measurementType: "WALKING_ASYMMETRY" },
displayName: "Walking asymmetry",
unit: "%",
direction: "lower-better",
archetype: "mobility-gait",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_asymmetry"),
mcp: true,
},
fhir: {
// 91557-1, used before, does not exist in LOINC.
loinc: "HKQuantityTypeIdentifierWalkingAsymmetryPercentage",
display: "Walking asymmetry percentage",
unit: "%",
category: "vital-signs",
},
},
WALKING_DOUBLE_SUPPORT: {
key: "WALKING_DOUBLE_SUPPORT",
kind: "measurement",
source: { measurementType: "WALKING_DOUBLE_SUPPORT" },
displayName: "Double-support time",
unit: "%",
direction: "lower-better",
archetype: "mobility-gait",
normalRange: { low: 20, high: 40 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_double_support"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage",
display: "Walking double support percentage",
unit: "%",
category: "vital-signs",
},
},
WALKING_STEP_LENGTH: {
key: "WALKING_STEP_LENGTH",
kind: "measurement",
source: { measurementType: "WALKING_STEP_LENGTH" },
displayName: "Step length",
unit: "m",
direction: "higher-better",
archetype: "mobility-gait",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_step_length"),
mcp: true,
},
fhir: {
// 41955-6, used before, is "Distance walked in 1 week"; LOINC has no
// step-length term, so the HealthKit identifier rides the custom system.
loinc: "HKQuantityTypeIdentifierWalkingStepLength",
display: "Walking step length",
unit: "m",
category: "vital-signs",
},
},
WALKING_SPEED: {
key: "WALKING_SPEED",
kind: "measurement",
source: { measurementType: "WALKING_SPEED" },
displayName: "Walking speed",
unit: "m/s",
direction: "higher-better",
archetype: "mobility-gait",
normalRange: { low: 1.2, high: 1.4 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("walking_speed"),
mcp: true,
},
fhir: {
// 41957-2, used before, is a 24-hour mean walking speed; the value
// here is a HealthKit walking-speed reading.
loinc: "HKQuantityTypeIdentifierWalkingSpeed",
display: "Walking speed",
unit: "m/s",
category: "vital-signs",
},
},
FALL_COUNT: {
key: "FALL_COUNT",
kind: "measurement",
source: { measurementType: "FALL_COUNT" },
displayName: "Falls",
unit: "falls/day",
direction: "lower-better",
archetype: "mobility-gait",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierNumberOfTimesFallen",
display: "Number of times fallen",
unit: "{falls}",
category: "activity",
},
},
SIX_MINUTE_WALK_DISTANCE: {
key: "SIX_MINUTE_WALK_DISTANCE",
kind: "measurement",
source: { measurementType: "SIX_MINUTE_WALK_DISTANCE" },
displayName: "Six-minute walk distance",
unit: "m",
direction: "higher-better",
archetype: "mobility-gait",
normalRange: { low: 400, high: 700 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierSixMinuteWalkTestDistance",
display: "Six-minute walk test distance",
unit: "m",
category: "activity",
},
},
STAIR_ASCENT_SPEED: {
key: "STAIR_ASCENT_SPEED",
kind: "measurement",
source: { measurementType: "STAIR_ASCENT_SPEED" },
displayName: "Stair ascent speed",
unit: "m/s",
direction: "higher-better",
archetype: "mobility-gait",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierStairAscentSpeed",
display: "Stair ascent speed",
unit: "m/s",
category: "vital-signs",
},
},
STAIR_DESCENT_SPEED: {
key: "STAIR_DESCENT_SPEED",
kind: "measurement",
source: { measurementType: "STAIR_DESCENT_SPEED" },
displayName: "Stair descent speed",
unit: "m/s",
direction: "higher-better",
archetype: "mobility-gait",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierStairDescentSpeed",
display: "Stair descent speed",
unit: "m/s",
category: "vital-signs",
},
},
// ── environmental-exposure ───────────────────────────────────────────
AUDIO_EXPOSURE_ENV: {
key: "AUDIO_EXPOSURE_ENV",
kind: "measurement",
source: { measurementType: "AUDIO_EXPOSURE_ENV" },
displayName: "Environmental sound exposure",
unit: "dBA",
direction: "lower-better",
archetype: "environmental-exposure",
normalRange: { low: 0, high: 80 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("audio_env"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierEnvironmentalAudioExposure",
display: "Environmental audio exposure",
unit: "dB[A]",
category: "vital-signs",
},
},
AUDIO_EXPOSURE_HEADPHONE: {
key: "AUDIO_EXPOSURE_HEADPHONE",
kind: "measurement",
source: { measurementType: "AUDIO_EXPOSURE_HEADPHONE" },
displayName: "Headphone audio exposure",
unit: "dBA",
direction: "lower-better",
archetype: "environmental-exposure",
normalRange: { low: 0, high: 80 },
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("audio_headphone"),
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierHeadphoneAudioExposure",
display: "Headphone audio exposure",
unit: "dB[A]",
category: "vital-signs",
},
},
AUDIO_EXPOSURE_EVENT: {
key: "AUDIO_EXPOSURE_EVENT",
kind: "measurement",
source: { measurementType: "AUDIO_EXPOSURE_EVENT" },
displayName: "Loud-exposure events",
unit: "events",
direction: "lower-better",
archetype: "environmental-exposure",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: coachScope("audio_event"),
mcp: true,
},
},
// ── sleep (dedicated template) ───────────────────────────────────────
SLEEP_DURATION: {
key: "SLEEP_DURATION",
kind: "measurement",
source: { measurementType: "SLEEP_DURATION" },
sourcePriorityKey: "sleep",
displayName: "Sleep duration",
unit: "min",
direction: "target-band",
archetype: "sleep",
normalRange: { low: 420, high: 540 },
surfaces: {
detailPage: true,
correlationEligible: true,
coachSnapshot: coachScope("sleep"),
mcp: true,
},
fhir: {
loinc: "93832-4",
display: "Sleep duration",
unit: "h",
category: "activity",
},
},
BREATHING_DISTURBANCES: {
key: "BREATHING_DISTURBANCES",
kind: "measurement",
source: { measurementType: "BREATHING_DISTURBANCES" },
displayName: "Sleep breathing disturbances",
unit: "count",
direction: "lower-better",
archetype: "sleep",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances",
display: "Sleeping breathing disturbances",
unit: "{count}",
category: "vital-signs",
},
},
SLEEP_SCORE: {
key: "SLEEP_SCORE",
kind: "measurement",
source: { measurementType: "SLEEP_SCORE" },
displayName: "Sleep score",
unit: "score",
direction: "higher-better",
archetype: "sleep",
surfaces: {
detailPage: true,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
// ── specialised metrics (bespoke status files; detailPage:false) ──────
// These keep their dedicated scopes + status generators; they register
// only so the cross-surface indices (FHIR, correlation, Coach, MCP) see
// them without routing them through the generic detail template.
WEIGHT: {
key: "WEIGHT",
kind: "measurement",
source: { measurementType: "WEIGHT" },
sourcePriorityKey: "weight",
displayName: "Weight",
unit: "kg",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: coachScope("weight"),
mcp: true,
},
fhir: {
loinc: "29463-7",
display: "Body weight",
unit: "kg",
category: "vital-signs",
},
},
BODY_MASS_INDEX: {
key: "BODY_MASS_INDEX",
kind: "measurement",
source: { measurementType: "BODY_MASS_INDEX" },
displayName: "Body mass index",
unit: "kg/m²",
direction: "target-band",
archetype: "body-composition",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: coachScope("bmi"),
mcp: true,
},
fhir: {
loinc: "39156-5",
display: "Body mass index (BMI) [Ratio]",
unit: "kg/m2",
category: "vital-signs",
},
},
PULSE: {
key: "PULSE",
kind: "measurement",
source: { measurementType: "PULSE" },
sourcePriorityKey: "pulse",
displayName: "Pulse",
unit: "bpm",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: coachScope("pulse"),
mcp: true,
},
fhir: {
loinc: "8867-4",
display: "Heart rate",
unit: "/min",
category: "vital-signs",
},
},
BODY_FAT: {
key: "BODY_FAT",
kind: "measurement",
source: { measurementType: "BODY_FAT" },
sourcePriorityKey: "bodyFat",
displayName: "Body fat",
unit: "%",
direction: "lower-better",
archetype: "body-composition",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: coachScope("body_fat"),
mcp: true,
},
fhir: {
loinc: "41982-0",
display: "Percentage of body fat Measured",
unit: "%",
category: "vital-signs",
},
},
BLOOD_PRESSURE_SYS: {
key: "BLOOD_PRESSURE_SYS",
kind: "measurement",
source: { measurementType: "BLOOD_PRESSURE_SYS" },
sourcePriorityKey: "bloodPressure",
displayName: "Blood pressure (systolic)",
unit: "mmHg",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: coachScope("bp"),
mcp: true,
},
},
BLOOD_PRESSURE_DIA: {
key: "BLOOD_PRESSURE_DIA",
kind: "measurement",
source: { measurementType: "BLOOD_PRESSURE_DIA" },
sourcePriorityKey: "bloodPressure",
displayName: "Blood pressure (diastolic)",
unit: "mmHg",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: coachScope("bp"),
mcp: true,
},
},
// ── v1.25 clinical-signals wave — validated mental-health screeners ──────
// PHQ-9 / GAD-7 totals. Opt-in, beside mood tracking. Deliberately kept OFF
// the Coach snapshot + MCP surfaces (`coachSnapshot:false` ⇒ `mcp:false`):
// mental-health item content is excluded from AI by construction, and the
// score trend stays a quiet local signal. FHIR rides the `survey` category
// with the instrument total-score LOINC.
PHQ9_SCORE: {
key: "PHQ9_SCORE",
kind: "score",
source: { measurementType: "PHQ9_SCORE" },
displayName: "PHQ-9 depression screen (total)",
unit: "score",
direction: "lower-better",
archetype: "physiological-vital",
normalRange: { low: 0, high: 4 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "44261-6",
display:
"Patient Health Questionnaire 9 item (PHQ-9) total score [Reported]",
unit: "{score}",
category: "survey",
},
},
GAD7_SCORE: {
key: "GAD7_SCORE",
kind: "score",
source: { measurementType: "GAD7_SCORE" },
displayName: "GAD-7 anxiety screen (total)",
unit: "score",
direction: "lower-better",
archetype: "physiological-vital",
normalRange: { low: 0, high: 4 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "70274-6",
display:
"Generalized anxiety disorder 7 item (GAD-7) total score [Reported.PHQ]",
unit: "{score}",
category: "survey",
},
},
// ── v1.27.9 — WHO-5 well-being + Sleep Condition Indicator totals ───────
// Same privacy posture as PHQ-9 / GAD-7 (off Coach snapshot + MCP; item
// content never rides a Measurement row). Direction inverts: HIGHER is
// better for both. No verified total-score LOINC exists for either, so both
// carry the `loinc: null` text-only survey concept the physical signals
// already use — an uncoded finding a clinician can read, never an invented
// code. Without a `fhir` facet the FHIR adapter drops a signal entirely,
// which is how a total the PDF and the share view show went missing from
// every FHIR output.
WHO5_SCORE: {
key: "WHO5_SCORE",
kind: "score",
source: { measurementType: "WHO5_SCORE" },
displayName: "WHO-5 well-being index (0–100)",
unit: "score",
direction: "higher-better",
archetype: "physiological-vital",
// "Below 50" is the WHO-suggested cut-off; achievable totals are
// multiples of 4, so ≥ 52 is the good side.
normalRange: { low: 52, high: 100 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// No verified total-score LOINC — local text concept, `{score}` UCUM.
loinc: null,
display: "WHO-5 well-being index total score",
unit: "{score}",
category: "survey",
},
},
SCI_SCORE: {
key: "SCI_SCORE",
kind: "score",
source: { measurementType: "SCI_SCORE" },
displayName: "Sleep Condition Indicator (total)",
unit: "score",
direction: "higher-better",
archetype: "physiological-vital",
// > 16 identified "no insomnia disorder" in Espie et al. 2014.
normalRange: { low: 17, high: 32 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// No verified total-score LOINC — local text concept, `{score}` UCUM.
loinc: null,
display: "Sleep Condition Indicator total score",
unit: "{score}",
category: "survey",
},
},
// ── v1.25 clinical-signals wave — physical / clinical measurements ──────
GRIP_STRENGTH: {
key: "GRIP_STRENGTH",
kind: "measurement",
source: { measurementType: "GRIP_STRENGTH" },
displayName: "Grip strength",
unit: "kg",
direction: "higher-better",
archetype: "activity-fitness",
// Coarse population floor; the EWGSOP2 cut-off is sex-specific (men < 27,
// women < 16 kg) and resolved at the display edge by the sex-aware band
// resolver (`norms.ts` GRIP_STRENGTH table) the detail-page assessment
// threads through `lookupNormalRange`.
normalRange: { low: 16, high: 60 },
surfaces: {
detailPage: true,
correlationEligible: false,
// Off the Coach snapshot, but reachable over MCP: the `mcp` facet is
// independent of `coachSnapshot`. The MCP rich reads expose it through
// the rollup-backed baseline / compare / change-point reads.
coachSnapshot: false,
mcp: true,
},
fhir: {
// NEEDS-VERIFY: no hand-grip-strength LOINC confirmed this pass —
// emits a local text concept until a stable LOINC is checked on loinc.org.
loinc: null,
display: "Hand grip strength",
unit: "kg",
category: "vital-signs",
},
},
PAIN_NRS: {
key: "PAIN_NRS",
kind: "measurement",
source: { measurementType: "PAIN_NRS" },
displayName: "Pain (0–10 NRS)",
unit: "score",
direction: "lower-better",
archetype: "physiological-vital",
normalRange: { low: 0, high: 3 },
surfaces: {
detailPage: true,
correlationEligible: false,
// Off the Coach snapshot, reachable over MCP (independent facets).
coachSnapshot: false,
mcp: true,
},
fhir: {
loinc: "72514-3",
display: "Pain severity - 0-10 verbal numeric rating [Score] - Reported",
unit: "{score}",
category: "survey",
},
},
WAIST_CIRCUMFERENCE: {
key: "WAIST_CIRCUMFERENCE",
kind: "measurement",
source: { measurementType: "WAIST_CIRCUMFERENCE" },
sourcePriorityKey: "waist",
displayName: "Waist circumference",
unit: "cm",
direction: "lower-better",
archetype: "body-composition",
// WHO European-origin increased-risk threshold (men > 94); the sex-aware
// resolver (`norms.ts` WAIST_CIRCUMFERENCE table: men 94 / women 80)
// sharpens it at the display edge via `lookupNormalRange`.
normalRange: { low: 0, high: 94 },
surfaces: {
detailPage: true,
correlationEligible: false,
// Off the Coach snapshot, reachable over MCP (independent facets).
coachSnapshot: false,
mcp: true,
},
fhir: {
loinc: "8280-0",
display: "Waist circumference at umbilicus by Tape measure",
unit: "cm",
category: "vital-signs",
},
},
WAIST_TO_HEIGHT: {
key: "WAIST_TO_HEIGHT",
kind: "measurement",
source: { measurementType: "WAIST_TO_HEIGHT" },
displayName: "Waist-to-height ratio",
unit: "ratio",
direction: "lower-better",
archetype: "body-composition",
// NICE: keep waist under half your height — WHtR ≥ 0.5 flags increased
// risk, so the last in-range value sits just below the 0.5 threshold
// (`high: 0.49`) and a 0.50 reading classifies as increased, not normal.
normalRange: { low: 0, high: 0.49 },
surfaces: {
detailPage: true,
correlationEligible: false,
// Off the Coach snapshot, reachable over MCP (independent facets).
coachSnapshot: false,
mcp: true,
},
fhir: {
// NEEDS-VERIFY: WHtR LOINC not confirmed this pass — local text concept.
loinc: null,
display: "Waist to height ratio",
unit: "1",
category: "vital-signs",
},
},
// ── v1.25 clinical-signals wave — longevity lab panel (kind:"biomarker") ─
// Registered through the registry to prove the labs path under one definition
// shape. The lab FHIR/UCUM coding lives in `fhir/lab-loinc.ts`; the
// biomarker-detail rail renders these via the labs catalog. Off Coach/MCP
// here (the labs surface has its own `get_labs` MCP tool).
APOB: {
key: "APOB",
kind: "biomarker",
source: { biomarkerKey: "apob" },
displayName: "Apolipoprotein B",
unit: "mg/dL",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
LPA: {
key: "LPA",
kind: "biomarker",
source: { biomarkerKey: "lp-a" },
displayName: "Lipoprotein(a)",
unit: "nmol/L",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
HS_CRP: {
key: "HS_CRP",
kind: "biomarker",
source: { biomarkerKey: "hs-crp" },
displayName: "hs-CRP",
unit: "mg/L",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
HBA1C: {
key: "HBA1C",
kind: "biomarker",
source: { biomarkerKey: "hba1c" },
displayName: "HbA1c",
unit: "%",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
FASTING_GLUCOSE: {
key: "FASTING_GLUCOSE",
kind: "biomarker",
source: { biomarkerKey: "fasting-glucose" },
displayName: "Fasting glucose",
unit: "mg/dL",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
FASTING_INSULIN: {
key: "FASTING_INSULIN",
kind: "biomarker",
source: { biomarkerKey: "fasting-insulin" },
displayName: "Fasting insulin",
unit: "µIU/mL",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
EGFR: {
key: "EGFR",
kind: "biomarker",
source: { biomarkerKey: "egfr" },
displayName: "eGFR (CKD-EPI 2021)",
unit: "mL/min/1.73m²",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
GGT: {
key: "GGT",
kind: "biomarker",
source: { biomarkerKey: "ggt" },
displayName: "GGT",
unit: "U/L",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
FERRITIN: {
key: "FERRITIN",
kind: "biomarker",
source: { biomarkerKey: "ferritin" },
displayName: "Ferritin",
unit: "ng/mL",
direction: "target-band",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
OMEGA3_INDEX: {
key: "OMEGA3_INDEX",
kind: "biomarker",
source: { biomarkerKey: "omega-3-index" },
displayName: "Omega-3 index",
unit: "%",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
},
// ── v1.25 (W-ENV) environmental-context module — exposure signals ─────────
// Daily weather/daylight, fetched server-side from Open-Meteo for the user's
// coarse home (or a travel override) and stored in `EnvironmentContext`. NOT
// `MeasurementType`-backed: `correlationEligible:true` declares intent, but
// the wiring runs through the env channel series builder, not the
// measurement-keyed `BUCKETED_TYPES` (the correlation adapter skips
// `kind:"environment"`). Off the Coach snapshot + MCP surfaces in v1
// (`coachSnapshot:false ⇒ mcp:false`); no FHIR facet (env observations are
// not Measurement rows, so they never enter `MEASUREMENT_LOINC`). Direction is
// `target-band` — a neutral exposure, not a "better/worse" vital.
ENV_TEMP_MEAN: {
key: "ENV_TEMP_MEAN",
kind: "environment",
source: { environmentChannelKey: "ENV_TEMP_MEAN" },
i18nPrefix: "environment.fields.tempMean",
displayName: "Daily temperature",
unit: "°C",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_TEMP_MIN: {
key: "ENV_TEMP_MIN",
kind: "environment",
source: { environmentChannelKey: "ENV_TEMP_MIN" },
i18nPrefix: "environment.fields.tempMin",
displayName: "Overnight low temperature",
unit: "°C",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_SUNSHINE: {
key: "ENV_SUNSHINE",
kind: "environment",
source: { environmentChannelKey: "ENV_SUNSHINE" },
i18nPrefix: "environment.fields.sunshine",
displayName: "Sunshine duration",
unit: "h",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_DAYLIGHT: {
key: "ENV_DAYLIGHT",
kind: "environment",
source: { environmentChannelKey: "ENV_DAYLIGHT" },
i18nPrefix: "environment.fields.daylight",
displayName: "Daylight length",
unit: "h",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_PRECIP: {
key: "ENV_PRECIP",
kind: "environment",
source: { environmentChannelKey: "ENV_PRECIP" },
i18nPrefix: "environment.fields.precip",
displayName: "Precipitation",
unit: "mm",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_PRESSURE_MEAN: {
key: "ENV_PRESSURE_MEAN",
kind: "environment",
source: { environmentChannelKey: "ENV_PRESSURE_MEAN" },
i18nPrefix: "environment.fields.pressureMean",
displayName: "Barometric pressure",
unit: "hPa",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_PRESSURE_DELTA: {
key: "ENV_PRESSURE_DELTA",
kind: "environment",
source: { environmentChannelKey: "ENV_PRESSURE_DELTA" },
i18nPrefix: "environment.fields.pressureDelta",
displayName: "Pressure swing",
unit: "hPa",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
}, // ── v1.42 (#615) air-quality exposure channels ───────────────────────────
// Three correlation channels out of the air-quality feed: fine particles,
// the ozone 8-hour high, and the day's highest pollen count. UV, NO2 and
// dust are display and Coach context only. Same surface flags as the weather
// channels above; the channel series builder carries the wiring.
ENV_PM25: {
key: "ENV_PM25",
kind: "environment",
source: { environmentChannelKey: "ENV_PM25" },
i18nPrefix: "environment.fields.pm25",
displayName: "Fine particles (PM2.5)",
unit: "µg/m³",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_OZONE_8H: {
key: "ENV_OZONE_8H",
kind: "environment",
source: { environmentChannelKey: "ENV_OZONE_8H" },
i18nPrefix: "environment.fields.ozone8h",
displayName: "Ozone (8-hour high)",
unit: "µg/m³",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
ENV_POLLEN_MAX: {
key: "ENV_POLLEN_MAX",
kind: "environment",
source: { environmentChannelKey: "ENV_POLLEN_MAX" },
i18nPrefix: "environment.fields.pollenMax",
displayName: "Pollen (highest)",
unit: "grains/m³",
direction: "target-band",
archetype: "environmental-exposure",
surfaces: {
detailPage: false,
correlationEligible: true,
coachSnapshot: false,
mcp: false,
},
},
// ── v1.44 — Apple Health activity rings, mindfulness, State of Mind, AFib ──
// The three ring metrics are read through MCP like steps and daylight. The
// mindful minutes, the State of Mind valence and AFib burden stay off every
// AI-facing surface (Coach, MCP, correlations): mental-health self-reports
// and an AFib awareness signal are shared only where the person asks.
EXERCISE_MINUTES: {
key: "EXERCISE_MINUTES",
kind: "measurement",
source: { measurementType: "EXERCISE_MINUTES" },
displayName: "Exercise minutes",
unit: "min/day",
direction: "higher-better",
archetype: "activity-fitness",
// Apple's exercise ring default and the WHO weekly 150 minutes / 7.
normalRange: { low: 22, high: 60 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierAppleExerciseTime",
display: "Exercise time",
unit: "min",
category: "activity",
},
},
STAND_MINUTES: {
key: "STAND_MINUTES",
kind: "measurement",
source: { measurementType: "STAND_MINUTES" },
displayName: "Standing minutes",
unit: "min/day",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: true,
},
fhir: {
loinc: "HKQuantityTypeIdentifierAppleStandTime",
display: "Stand time",
unit: "min",
category: "activity",
},
},
STAND_HOURS: {
key: "STAND_HOURS",
kind: "measurement",
source: { measurementType: "STAND_HOURS" },
displayName: "Stand hours",
unit: "h/day",
direction: "higher-better",
archetype: "activity-fitness",
// Apple's stand-ring goal is 12 hours.
normalRange: { low: 12, high: 24 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: true,
},
fhir: {
loinc: "HKCategoryTypeIdentifierAppleStandHour",
display: "Stand hours",
unit: "h",
category: "activity",
},
},
MINDFUL_MINUTES: {
key: "MINDFUL_MINUTES",
kind: "measurement",
source: { measurementType: "MINDFUL_MINUTES" },
displayName: "Mindful minutes",
unit: "min/day",
direction: "higher-better",
archetype: "activity-fitness",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKCategoryTypeIdentifierMindfulSession",
display: "Mindful session",
unit: "min",
category: "activity",
},
},
STATE_OF_MIND_VALENCE: {
key: "STATE_OF_MIND_VALENCE",
kind: "score",
source: { measurementType: "STATE_OF_MIND_VALENCE" },
displayName: "State of mind (valence)",
unit: "valence",
direction: "higher-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// A self-report on HealthKit's −1..1 scale; no LOINC concept.
loinc: null,
display: "State of mind valence",
unit: "1",
category: "survey",
},
},
AFIB_BURDEN: {
key: "AFIB_BURDEN",
kind: "measurement",
source: { measurementType: "AFIB_BURDEN" },
displayName: "AFib burden",
unit: "%",
direction: "lower-better",
archetype: "physiological-vital",
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
loinc: "HKQuantityTypeIdentifierAtrialFibrillationBurden",
display: "Atrial fibrillation burden",
unit: "%",
category: "vital-signs",
},
},
// ── v1.44 — room climate pushed through the ingest token ──
// Bands are the usual comfort guidance (CO2 under 1000 ppm as the long-
// standing ventilation threshold, 18–24 °C, 40–60 % RH); placement only.
INDOOR_CO2: {
key: "INDOOR_CO2",
kind: "measurement",
source: { measurementType: "INDOOR_CO2" },
displayName: "Indoor CO2",
unit: "ppm",
direction: "target-band",
archetype: "environmental-exposure",
normalRange: { low: 400, high: 1000 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// A room reading, not a body observation; no LOINC concept applies.
loinc: null,
display: "Indoor carbon dioxide",
unit: "[ppm]",
category: "activity",
},
},
INDOOR_TEMPERATURE: {
key: "INDOOR_TEMPERATURE",
kind: "measurement",
source: { measurementType: "INDOOR_TEMPERATURE" },
displayName: "Indoor temperature",
unit: "°C",
direction: "target-band",
archetype: "environmental-exposure",
normalRange: { low: 18, high: 24 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// A room reading, not a body observation; no LOINC concept applies.
loinc: null,
display: "Indoor air temperature",
unit: "Cel",
category: "activity",
},
},
INDOOR_HUMIDITY: {
key: "INDOOR_HUMIDITY",
kind: "measurement",
source: { measurementType: "INDOOR_HUMIDITY" },
displayName: "Indoor humidity",
unit: "%",
direction: "target-band",
archetype: "environmental-exposure",
normalRange: { low: 40, high: 60 },
surfaces: {
detailPage: false,
correlationEligible: false,
coachSnapshot: false,
mcp: false,
},
fhir: {
// A room reading, not a body observation; no LOINC concept applies.
loinc: null,
display: "Indoor relative humidity",
unit: "%",
category: "activity",
},
},
};
/** Type guard: narrow an arbitrary string to a registered signal key. */
export function isSignalKey(key: string): boolean {
return Object.prototype.hasOwnProperty.call(SIGNALS, key);
}
/** Resolve a signal by its key, or null when unregistered. */
export function getSignal(key: string): SignalDefinition | null {
return isSignalKey(key) ? SIGNALS[key] : null;
}
/** Every registered signal, in declaration order. */
export function allSignals(): SignalDefinition[] {
return Object.values(SIGNALS);
}
/** Reverse index: DB `MeasurementType` → the signal it backs (built once). */
const MEASUREMENT_TYPE_TO_SIGNAL = new Map<MeasurementType, SignalDefinition>(
allSignals()
.filter(
(s): s is Extract<SignalDefinition, { kind: "measurement" | "score" }> =>
s.kind === "measurement" || s.kind === "score",
)
.map((s) => [s.source.measurementType, s]),
);
/** Resolve the signal a `MeasurementType` backs, or null when unregistered. */
export function signalForMeasurementType(
type: MeasurementType,
): SignalDefinition | null {
return MEASUREMENT_TYPE_TO_SIGNAL.get(type) ?? null;
}