From 0750b97845618e965ab798598d4a56569f5851b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 15:19:06 +0200 Subject: [PATCH] refactor(measurements): hoist cumulative metric-key mapping into shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analytics route carried an inline MeasurementType → SourcePriorityMetricKey switch next to the cumulative-day-sum bucket-and-sum. Adding a new cumulative metric required updates in three places — the SourcePriorityMetricKey enum, the route-level switch, and CUMULATIVE_HK_TYPES — with no shared test to pin the drift. Same shape as the H-2 / W10-4 H3 finding the previous commit closed for the cumulative-type set. Move the lookup next to CUMULATIVE_DAY_SUM_TYPES in src/lib/measurements/cumulative-day-sum.ts and add vitest assertions: every CUMULATIVE_HK_TYPES member resolves either to a real SourcePriorityMetricKey or to null (the picker's pass-through escape hatch for types like TIME_IN_DAYLIGHT). The route now imports the helper instead of duplicating the switch. --- src/app/api/analytics/route.ts | 25 +------------ .../__tests__/cumulative-day-sum.test.ts | 37 +++++++++++++++++++ src/lib/measurements/cumulative-day-sum.ts | 35 ++++++++++++++++++ 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts index 65d2b3cf3..03c740849 100644 --- a/src/app/api/analytics/route.ts +++ b/src/app/api/analytics/route.ts @@ -14,42 +14,19 @@ import type { } from "@/generated/prisma/client"; import { measurementTypeEnum } from "@/lib/validations/measurement"; import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority"; -import type { SourcePriorityMetricKey } from "@/lib/validations/source-priority"; import { ensureUserRollupsFresh } from "@/lib/measurements/rollups"; import { probeRollupCoverage } from "@/lib/measurements/rollup-coverage"; import { computeBpInTargetFastPath } from "@/lib/analytics/bp-in-target-fast-path"; import { computeUserHealthScoreFastPath } from "@/lib/analytics/health-score-fast-path"; import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path"; import { + cumulativeMetricKey, isCumulativeDaySumType, pickCumulativeDaySum, } from "@/lib/measurements/cumulative-day-sum"; export const dynamic = "force-dynamic"; -/** - * v1.4.36 W4c — cumulative MeasurementType → SourcePriorityMetricKey - * for `pickCanonicalSourceRows`. Returns `null` for types without a - * dedicated priority ladder (e.g. TIME_IN_DAYLIGHT), which fall - * through the picker's "no ladder" pass-through branch. - */ -function cumulativeMetricKey( - type: MeasurementType, -): SourcePriorityMetricKey | null { - switch (type) { - case "ACTIVITY_STEPS": - return "steps"; - case "ACTIVE_ENERGY_BURNED": - return "activeEnergy"; - case "WALKING_RUNNING_DISTANCE": - return "walkingRunningDistance"; - case "FLIGHTS_CLIMBED": - return "flightsClimbed"; - default: - return null; - } -} - /** * v1.4.33 C1 — pull `?slice=…` from either a NextRequest (the * route's production wrapper) or a plain `Request` (the integration diff --git a/src/lib/measurements/__tests__/cumulative-day-sum.test.ts b/src/lib/measurements/__tests__/cumulative-day-sum.test.ts index 62f4c8c6b..d4a63684f 100644 --- a/src/lib/measurements/__tests__/cumulative-day-sum.test.ts +++ b/src/lib/measurements/__tests__/cumulative-day-sum.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest"; +import { SOURCE_PRIORITY_METRIC_KEYS } from "@/lib/validations/source-priority"; import { CUMULATIVE_HK_TYPES } from "../apple-health-mapping"; import { CUMULATIVE_DAY_SUM_TYPES, + cumulativeMetricKey, isCumulativeDaySumType, pickCumulativeDaySum, } from "../cumulative-day-sum"; @@ -119,3 +121,38 @@ describe("CUMULATIVE_DAY_SUM_TYPES / CUMULATIVE_HK_TYPES parity", () => { expect(new Set(CUMULATIVE_DAY_SUM_TYPES)).toEqual(CUMULATIVE_HK_TYPES); }); }); + +describe("cumulativeMetricKey", () => { + // v1.4.37 W10 — pinned mapping from MeasurementType → + // SourcePriorityMetricKey for the cumulative analytics fast-path. + // Every member of CUMULATIVE_HK_TYPES must resolve either to a real + // SourcePriorityMetricKey (so the canonical-source picker fires) or + // to `null` (so the picker's no-ladder pass-through branch runs). + // A new cumulative HK type without an explicit map entry falls + // through to the default branch (returns null) — that's the + // intended escape hatch for types like TIME_IN_DAYLIGHT that have + // no clinical competitor; pin the audit here so the operator is + // forced to think about it. + it("returns a known SourcePriorityMetricKey or null for every CUMULATIVE_HK_TYPES member", () => { + const validKeys = new Set(SOURCE_PRIORITY_METRIC_KEYS); + for (const type of CUMULATIVE_HK_TYPES) { + const key = cumulativeMetricKey(type); + if (key !== null) { + expect(validKeys.has(key)).toBe(true); + } + } + }); + + it("explicitly maps the four metrics that own a SourcePriorityMetricKey ladder", () => { + expect(cumulativeMetricKey("ACTIVITY_STEPS")).toBe("steps"); + expect(cumulativeMetricKey("ACTIVE_ENERGY_BURNED")).toBe("activeEnergy"); + expect(cumulativeMetricKey("WALKING_RUNNING_DISTANCE")).toBe( + "walkingRunningDistance", + ); + expect(cumulativeMetricKey("FLIGHTS_CLIMBED")).toBe("flightsClimbed"); + }); + + it("returns null for TIME_IN_DAYLIGHT (no clinical competitor today)", () => { + expect(cumulativeMetricKey("TIME_IN_DAYLIGHT")).toBeNull(); + }); +}); diff --git a/src/lib/measurements/cumulative-day-sum.ts b/src/lib/measurements/cumulative-day-sum.ts index f09ac657e..3c09c056c 100644 --- a/src/lib/measurements/cumulative-day-sum.ts +++ b/src/lib/measurements/cumulative-day-sum.ts @@ -63,6 +63,7 @@ export function pickCumulativeDaySum( } import type { MeasurementType } from "@/generated/prisma/client"; +import type { SourcePriorityMetricKey } from "@/lib/validations/source-priority"; import { CUMULATIVE_HK_TYPES } from "./apple-health-mapping"; /** @@ -96,3 +97,37 @@ export function isCumulativeDaySumType( ): type is CumulativeDaySumType { return (CUMULATIVE_DAY_SUM_TYPES as readonly string[]).includes(type); } + +/** + * v1.4.36 W4c / v1.4.37 W10 — cumulative MeasurementType → + * SourcePriorityMetricKey lookup for `pickCanonicalSourceRows`. + * + * Returns `null` for types without a dedicated priority ladder + * (e.g. TIME_IN_DAYLIGHT, which lacks a clinical-grade competitor + * to Apple Health for daylight minutes today). The picker treats a + * `null` ladder as the "no priority — pass through every row" + * branch, so the bucket-and-sum runs over every source. + * + * Hoisted out of `src/app/api/analytics/route.ts` so adding a new + * cumulative metric is a single-line edit here rather than a + * route-level switch that drifts from the `SourcePriorityMetricKey` + * enum + `CUMULATIVE_HK_TYPES` set. A parity test pins the contract: + * every member of `CUMULATIVE_HK_TYPES` either resolves to a real + * `SourcePriorityMetricKey` or is explicitly mapped to `null`. + */ +export function cumulativeMetricKey( + type: MeasurementType, +): SourcePriorityMetricKey | null { + switch (type) { + case "ACTIVITY_STEPS": + return "steps"; + case "ACTIVE_ENERGY_BURNED": + return "activeEnergy"; + case "WALKING_RUNNING_DISTANCE": + return "walkingRunningDistance"; + case "FLIGHTS_CLIMBED": + return "flightsClimbed"; + default: + return null; + } +}