refactor(measurements): hoist cumulative metric-key mapping into shared module

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.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 15:19:06 +02:00
1 parent 3154c1bc36
commit 0750b97845
3 files changed
+73 -24

No files matched your search

+1 -24
View File
@@ -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
@@ -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<string>(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();
});
});
@@ -63,6 +63,7 @@ export function pickCumulativeDaySum<T extends CumulativeRow>(
}
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;
}
}