feat(measurements): add ANS_CHARGE and CARDIO_LOAD measurement types

Polar Nightly Recharge and Training Load Pro carry two native signals with
no existing home: the HRV-based autonomic-nervous-system charge (distinct
from the 1-6 recovery band that maps to RECOVERY_SCORE) and the device-native
cardio-load strain figure (distinct from WHOOP DAY_STRAIN and the COMPUTED
STRAIN_SCORE). Add both to the canonical enum (migration 0166) and wire them
through the validation enum, canonical units, plausibility ranges, category
cluster, chart-token allowlist, PR-direction resolver, and the measurement-
list label/icon/colour maps, with labels across all six locales. Both are
device-derived composites, so they read no PR direction and stay out of the
clinical vitals PDF, matching the WHOOP-native score classes.

Regenerate the OpenAPI contract for the two new enum members.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 00:07:30 +02:00
1 parent 44eed87e89
commit fd39580ee6
16 files changed
+117 -1

No files matched your search

@@ -114,6 +114,9 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record<string, string> = {
AVERAGE_HEART_RATE: "measurements.typeAverageHeartRate",
MAX_HEART_RATE: "measurements.typeMaxHeartRate",
SLEEP_DISTURBANCE_COUNT: "measurements.typeSleepDisturbanceCount",
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
ANS_CHARGE: "measurements.typeAnsCharge",
CARDIO_LOAD: "measurements.typeCardioLoad",
};
export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
@@ -225,6 +228,11 @@ export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
AVERAGE_HEART_RATE: HeartPulse,
MAX_HEART_RATE: Heart,
SLEEP_DISTURBANCE_COUNT: Waves,
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// HeartPulse reads as the autonomic-charge signal; Gauge carries the
// cardio-load strain figure (same family as DAY_STRAIN).
ANS_CHARGE: HeartPulse,
CARDIO_LOAD: Gauge,
};
export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
@@ -326,4 +334,8 @@ export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
AVERAGE_HEART_RATE: "bg-chart-3/20 text-chart-3",
MAX_HEART_RATE: "bg-chart-3/20 text-chart-3",
SLEEP_DISTURBANCE_COUNT: "bg-chart-2/20 text-chart-2",
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// chart-1 (strain/score family) for both, matching DAY_STRAIN.
ANS_CHARGE: "bg-chart-1/20 text-chart-1",
CARDIO_LOAD: "bg-chart-1/20 text-chart-1",
};
@@ -90,10 +90,13 @@ const EXPECTED_TYPES = [
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
"ANS_CHARGE",
"CARDIO_LOAD",
] as const;
describe("measurementTypeEnum coverage", () => {
it("exposes the 63 canonical measurement types", () => {
it("exposes the 65 canonical measurement types", () => {
expect([...measurementTypeEnum.options].sort()).toEqual(
[...EXPECTED_TYPES].sort(),
);
@@ -215,6 +218,13 @@ describe("measurementTypeEnum coverage", () => {
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components. ANS
// charge and Cardio Load are device-derived recovery / strain composites,
// not measured clinical vitals; they surface on their own Insights cluster
// with a "descriptive, not clinical" framing and never belong in the
// clinical vitals PDF. See doctor-report-pdf-core.ts.
"ANS_CHARGE",
"CARDIO_LOAD",
]);
it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => {
+5
View File
@@ -115,6 +115,11 @@ export const ALLOWED_CHART_TOKENS = [
"metric:AVERAGE_HEART_RATE",
"metric:MAX_HEART_RATE",
"metric:SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar-native recovery / strain components. Continuous daily
// series, so each carries a `metric:<TYPE>` token and renders through the
// generic chart renderer alongside the WHOOP-native score classes.
"metric:ANS_CHARGE",
"metric:CARDIO_LOAD",
] as const;
export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
@@ -60,6 +60,12 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components. These
// ingest server-side from the Polar AccessLink API (source = POLAR), never
// from HealthKit: Apple ships no identifier for Polar's autonomic charge or
// its device-native cardio-load strain figure. No HK mapping by design.
"ANS_CHARGE",
"CARDIO_LOAD",
]);
describe("APPLE_HEALTH_TYPE_MAP", () => {
+6
View File
@@ -188,6 +188,12 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
["AVERAGE_HEART_RATE", "cardiovascular"],
["MAX_HEART_RATE", "cardiovascular"],
["SLEEP_DISTURBANCE_COUNT", "sleep"],
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// ANS charge + Cardio Load are device-native recovery / strain composites,
// so they join the scores cluster alongside DAY_STRAIN.
["ANS_CHARGE", "scores"],
["CARDIO_LOAD", "scores"],
]);
/**
+7
View File
@@ -176,6 +176,13 @@ export function getPRDirection(
// WALKING_HEART_RATE_AVERAGE case above. Defer to a null PR direction.
case "AVERAGE_HEART_RATE":
case "MAX_HEART_RATE":
// v1.17.1 — Polar-native recovery / strain components. ANS_CHARGE is a
// baseline-relative autonomic deviation (homeostatic, not a goal axis) and
// CARDIO_LOAD is a device-derived cardiovascular-load composite — neither a
// higher nor a lower value is an achievement, so they stay null like the
// other device-derived scores (HRV_RMSSD / DAY_STRAIN).
case "ANS_CHARGE":
case "CARDIO_LOAD":
return null;
}
}
+17
View File
@@ -100,6 +100,13 @@ export const measurementTypeEnum = z.enum([
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components ingested
// server-side as `source = POLAR`. ANS_CHARGE is the HRV-based autonomic
// charge (distinct from the 1–6 recovery band that maps to RECOVERY_SCORE);
// CARDIO_LOAD is Polar's device-native cardiovascular-strain figure (distinct
// from WHOOP DAY_STRAIN and the COMPUTED STRAIN_SCORE).
"ANS_CHARGE",
"CARDIO_LOAD",
]);
/**
@@ -312,6 +319,9 @@ const unitMap: Record<string, string> = {
MAX_HEART_RATE: "bpm",
// Per-night sleep disturbance tally — a plain integer count.
SLEEP_DISTURBANCE_COUNT: "count",
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
ANS_CHARGE: "score",
CARDIO_LOAD: "score",
};
export function getUnitForType(type: string): string {
@@ -487,6 +497,13 @@ const VALUE_RANGES: Record<string, { min: number; max: number }> = {
// Per-night sleep disturbance count — 0 is an undisturbed night; 200 is a
// generous ceiling over any plausible severely-fragmented night.
SLEEP_DISTURBANCE_COUNT: { min: 0, max: 200 },
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// ANS charge is a baseline-relative autonomic deviation that can run
// negative; a generous symmetric ±100 band covers any plausible reading.
// Cardio Load is a non-negative cumulative-strain figure; 1000 is a generous
// ceiling over any plausible single-day training load.
ANS_CHARGE: { min: -100, max: 100 },
CARDIO_LOAD: { min: 0, max: 1000 },
};
export function validateMeasurementRange(