diff --git a/src/components/measurements/measurement-list-meta.ts b/src/components/measurements/measurement-list-meta.ts index 0f0f53b8a..745e6e1e9 100644 --- a/src/components/measurements/measurement-list-meta.ts +++ b/src/components/measurements/measurement-list-meta.ts @@ -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 = { 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 = { @@ -74,6 +83,19 @@ export const MEASUREMENT_TYPE_ICONS: Record = { 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 = { @@ -96,4 +118,14 @@ export const MEASUREMENT_TYPE_COLORS: Record = { 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", }; diff --git a/src/lib/__tests__/measurement-type-enum-coverage.test.ts b/src/lib/__tests__/measurement-type-enum-coverage.test.ts index efd9ecbb5..c4a08dd77 100644 --- a/src/lib/__tests__/measurement-type-enum-coverage.test.ts +++ b/src/lib/__tests__/measurement-type-enum-coverage.test.ts @@ -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", () => { diff --git a/src/lib/insights/chart-tokens.ts b/src/lib/insights/chart-tokens.ts index 4480e7370..c8b6e2384 100644 --- a/src/lib/insights/chart-tokens.ts +++ b/src/lib/insights/chart-tokens.ts @@ -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"