fix(measurements): wire W5d Withings enum values into list-meta + chart-tokens

The 7 W5d additions (fat-free mass, fat mass, muscle mass, skin
temperature, pulse-wave velocity, vascular age, visceral fat) landed
in measurementTypeEnum but the per-type lookup maps and AI-chart
allowlist were never extended. Six drift-guard tests caught the gap.

Adds icons (Dumbbell for muscle; family-consistent reuse for the
rest), chart-bucket colors (mass → chart-1, fat/temp → chart-4,
cardio → chart-3, pulse-derived → chart-5), AI chart-token allowlist
entries, and orphan-enum strip list entries so raw enum identifiers
can never leak through prose. Doctor-report PDF holds the 7 new
types under the same v1.5 gate as the v1.4.23 Apple Health metrics
until clinical layout + reference ranges are agreed.

Baseline: 2537 → 2543 passing tests; typecheck clean.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 14:59:01 +02:00
1 parent d72c60c098
commit ca3c225a07
3 files changed
+74 -1

No files matched your search

@@ -29,6 +29,7 @@ import {
TrendingUp,
Thermometer,
Gauge,
Dumbbell,
type LucideIcon,
} from "lucide-react";
@@ -52,6 +53,14 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record<string, string> = {
WALKING_RUNNING_DISTANCE: "measurements.typeWalkingRunningDistance",
VO2_MAX: "measurements.typeVo2Max",
BODY_TEMPERATURE: "measurements.typeBodyTemperature",
// ── v1.4.25 W5d Withings full coverage ──
FAT_FREE_MASS: "measurements.typeFatFreeMass",
FAT_MASS: "measurements.typeFatMass",
MUSCLE_MASS: "measurements.typeMuscleMass",
SKIN_TEMPERATURE: "measurements.typeSkinTemperature",
PULSE_WAVE_VELOCITY: "measurements.typePulseWaveVelocity",
VASCULAR_AGE: "measurements.typeVascularAge",
VISCERAL_FAT: "measurements.typeVisceralFat",
};
export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
@@ -74,6 +83,19 @@ export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
WALKING_RUNNING_DISTANCE: Footprints,
VO2_MAX: Gauge,
BODY_TEMPERATURE: Thermometer,
// ── v1.4.25 W5d Withings full coverage ──
// Body-composition trio: Scale carries the mass-family (FFM is the
// weight residual after fat); Droplets carries the fat-family
// (BODY_FAT already uses it, so FAT_MASS + VISCERAL_FAT match);
// Dumbbell is reserved for muscle so the three rows stay distinct
// in the list view.
FAT_FREE_MASS: Scale,
FAT_MASS: Droplets,
MUSCLE_MASS: Dumbbell,
SKIN_TEMPERATURE: Thermometer,
PULSE_WAVE_VELOCITY: Activity,
VASCULAR_AGE: HeartPulse,
VISCERAL_FAT: Droplets,
};
export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
@@ -96,4 +118,14 @@ export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
WALKING_RUNNING_DISTANCE: "bg-chart-2/20 text-chart-2",
VO2_MAX: "bg-chart-1/20 text-chart-1",
BODY_TEMPERATURE: "bg-chart-4/20 text-chart-4",
// ── v1.4.25 W5d Withings full coverage ──
// chart-1 (mass), chart-3 (cardio), chart-4 (fat/temp), chart-5
// (pulse-derived) — extend the existing color-family conventions.
FAT_FREE_MASS: "bg-chart-1/20 text-chart-1",
FAT_MASS: "bg-chart-4/20 text-chart-4",
MUSCLE_MASS: "bg-chart-1/20 text-chart-1",
SKIN_TEMPERATURE: "bg-chart-4/20 text-chart-4",
PULSE_WAVE_VELOCITY: "bg-chart-5/20 text-chart-5",
VASCULAR_AGE: "bg-chart-3/20 text-chart-3",
VISCERAL_FAT: "bg-chart-4/20 text-chart-4",
};
@@ -34,10 +34,18 @@ const EXPECTED_TYPES = [
"WALKING_RUNNING_DISTANCE",
"VO2_MAX",
"BODY_TEMPERATURE",
// ── v1.4.25 W5d Withings full coverage ──
"FAT_FREE_MASS",
"FAT_MASS",
"MUSCLE_MASS",
"SKIN_TEMPERATURE",
"PULSE_WAVE_VELOCITY",
"VASCULAR_AGE",
"VISCERAL_FAT",
] as const;
describe("measurementTypeEnum coverage", () => {
it("exposes the 18 canonical measurement types", () => {
it("exposes the 25 canonical measurement types", () => {
expect([...measurementTypeEnum.options].sort()).toEqual(
[...EXPECTED_TYPES].sort(),
);
@@ -52,6 +60,11 @@ describe("measurementTypeEnum coverage", () => {
// the v1.4.23 release of the doctor PDF — they ship into the
// clinical surface alongside the iOS app's first paying-customer
// sync in v1.5 once layout + reference ranges are agreed.
// - v1.4.25 Withings additions (fat-free / fat / muscle mass, skin
// temperature, pulse-wave velocity, vascular age, visceral fat) are
// held under the same v1.5 gate. Body composition + cardiovascular
// risk markers warrant their own clinical layout (reference ranges
// differ by sex/age) which lands with the iOS-app clinical surface.
// Updates to this set MUST be paired with a comment in
// doctor-report-pdf-core.ts so the rationale stays discoverable.
const PDF_VITAL_EXCLUSIONS = new Set([
@@ -65,6 +78,13 @@ describe("measurementTypeEnum coverage", () => {
"WALKING_RUNNING_DISTANCE",
"VO2_MAX",
"BODY_TEMPERATURE",
"FAT_FREE_MASS",
"FAT_MASS",
"MUSCLE_MASS",
"SKIN_TEMPERATURE",
"PULSE_WAVE_VELOCITY",
"VASCULAR_AGE",
"VISCERAL_FAT",
]);
it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => {
+21
View File
@@ -43,6 +43,17 @@ export const ALLOWED_CHART_TOKENS = [
"metric:WALKING_RUNNING_DISTANCE",
"metric:VO2_MAX",
"metric:BODY_TEMPERATURE",
// v1.4.25 W5d — Withings full coverage. Same posture as v1.4.23 — the
// LLM can reference these in prose; dedicated chart components land in
// v1.5 when the Insights body-composition + cardiovascular sub-pages
// are designed.
"metric:FAT_FREE_MASS",
"metric:FAT_MASS",
"metric:MUSCLE_MASS",
"metric:SKIN_TEMPERATURE",
"metric:PULSE_WAVE_VELOCITY",
"metric:VASCULAR_AGE",
"metric:VISCERAL_FAT",
] as const;
export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
@@ -105,6 +116,16 @@ const ORPHAN_ENUMS = [
"VO2_MAX",
"BODY_TEMPERATURE",
"SLEEP_DURATION",
// v1.4.25 W5d Withings additions. Multi-word upper-snake enum names
// never appear in legitimate user-facing prose, so stripping them
// unconditionally is safe.
"FAT_FREE_MASS",
"FAT_MASS",
"MUSCLE_MASS",
"SKIN_TEMPERATURE",
"PULSE_WAVE_VELOCITY",
"VASCULAR_AGE",
"VISCERAL_FAT",
] as const;
// `\b` boundaries keep ordinary English prose untouched — "weight"