fix(sources): ladder STRESS_SCORE so the source collapse stays deterministic

STRESS_SCORE has no source-priority ladder, so metricKeyForType returns null
and the SQL source collapse falls to an alphabetical rank-90 tiebreak. It is
safe today because the only producer is the COMPUTED HRV-derived proxy, but
the instant a device-native producer (Oura daily_stress) is enabled both rows
survive and blend nondeterministically.

Add a stress metric key with an OURA-above-COMPUTED default ladder, wire it
through metricKeyForType and RANKED_TYPES, and surface it in the Sources
settings (all six locales). A guard test asserts every RANKED_TYPES member
resolves to a non-null metric key so a future ranked type can't ship unladdered.
Regenerate the OpenAPI for the additive source-priority key.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 16:52:10 +02:00
1 parent 013241ba74
commit 56d8cf19c5
12 files changed
+154 -7

No files matched your search

+96
View File
@@ -3606,6 +3606,22 @@ paths:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -3881,6 +3897,22 @@ paths:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
deviceTypePriority:
type: object
properties:
@@ -14406,6 +14438,22 @@ components:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -14681,6 +14729,22 @@ components:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
additionalProperties: false
deviceTypePriority:
type: object
@@ -15002,6 +15066,22 @@ components:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -15277,6 +15357,22 @@ components:
- NIGHTSCOUT
- POLAR
- OURA
stress:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
additionalProperties: false
deviceTypePriority:
type: object
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 max",
"skinTemperature": "Hauttemperatur",
"respiratoryRate": "Atemfrequenz",
"recovery": "Erholung"
"recovery": "Erholung",
"stress": "Stress"
},
"sourceLabels": {
"WITHINGS": "Withings",
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 max",
"skinTemperature": "Skin temperature",
"respiratoryRate": "Respiratory rate",
"recovery": "Recovery"
"recovery": "Recovery",
"stress": "Stress"
},
"sourceLabels": {
"WITHINGS": "Withings",
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 máx.",
"skinTemperature": "Temperatura de la piel",
"respiratoryRate": "Frecuencia respiratoria",
"recovery": "Recuperación"
"recovery": "Recuperación",
"stress": "Estrés"
},
"sourceLabels": {
"WITHINGS": "Withings",
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 max",
"skinTemperature": "Température cutanée",
"respiratoryRate": "Fréquence respiratoire",
"recovery": "Récupération"
"recovery": "Récupération",
"stress": "Stress"
},
"sourceLabels": {
"WITHINGS": "Withings",
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 max",
"skinTemperature": "Temperatura cutanea",
"respiratoryRate": "Frequenza respiratoria",
"recovery": "Recupero"
"recovery": "Recupero",
"stress": "Stress"
},
"sourceLabels": {
"WITHINGS": "Withings",
+2 -1
View File
@@ -3870,7 +3870,8 @@
"vo2Max": "VO2 max",
"skinTemperature": "Temperatura skóry",
"respiratoryRate": "Częstość oddechów",
"recovery": "Regeneracja"
"recovery": "Regeneracja",
"stress": "Stres"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -87,6 +87,8 @@ const METRIC_LABEL_KEYS: Record<SourcePriorityMetricKey, string> = {
skinTemperature: "settings.sections.sources.metrics.skinTemperature",
respiratoryRate: "settings.sections.sources.metrics.respiratoryRate",
recovery: "settings.sections.sources.metrics.recovery",
// v1.18.10 I-5 — native-vs-derived stress ladder.
stress: "settings.sections.sources.metrics.stress",
};
const SOURCE_LABEL_KEYS: Record<string, string> = {
+5 -1
View File
@@ -31,7 +31,7 @@ import {
* absent: they only ever have one source per (type, day), so the collapse is
* a no-op for them and they fall into the CASE's ELSE branch.
*/
const RANKED_TYPES: readonly MeasurementType[] = [
export const RANKED_TYPES: readonly MeasurementType[] = [
"RESTING_HEART_RATE",
"HEART_RATE_VARIABILITY",
"RESPIRATORY_RATE",
@@ -45,6 +45,10 @@ const RANKED_TYPES: readonly MeasurementType[] = [
"PULSE",
"VO2_MAX",
"RECOVERY_SCORE",
// v1.18.10 I-5 — STRESS_SCORE carries a source-priority ladder (`stress`)
// so the SQL collapse ranks a future device-native producer above the
// COMPUTED proxy deterministically. No-op until a second producer lands.
"STRESS_SCORE",
"SLEEP_DURATION",
"ACTIVITY_STEPS",
"ACTIVE_ENERGY_BURNED",
@@ -6,8 +6,10 @@ import {
CUMULATIVE_DAY_SUM_TYPES,
cumulativeMetricKey,
isCumulativeDaySumType,
metricKeyForType,
pickCumulativeDaySum,
} from "../cumulative-day-sum";
import { RANKED_TYPES } from "@/lib/analytics/source-rank-sql";
/**
* v1.4.36 W4c — pickCumulativeDaySum unit tests.
@@ -156,3 +158,22 @@ describe("cumulativeMetricKey", () => {
expect(cumulativeMetricKey("TIME_IN_DAYLIGHT")).toBeNull();
});
});
describe("metricKeyForType — source-priority ladder coverage (v1.18.10 I-5)", () => {
// Every type in RANKED_TYPES drives the SQL source-collapse CASE
// (`buildSourceRankCase` skips a type whose `metricKeyForType` is null).
// A ranked type that resolves to null would make `collapseRollupRowsBySource`
// fall to the alphabetical rank-90 tiebreak — nondeterministic the instant a
// second producer for that type is enabled. Pin the contract so a future
// ranked type cannot ship without its metric key.
it("resolves every RANKED_TYPES member to a non-null SourcePriorityMetricKey", () => {
for (const type of RANKED_TYPES) {
expect(metricKeyForType(type)).not.toBeNull();
}
});
it("ladders STRESS_SCORE so the COMPUTED-vs-device producer is deterministic", () => {
expect(metricKeyForType("STRESS_SCORE")).toBe("stress");
expect(RANKED_TYPES).toContain("STRESS_SCORE");
});
});
@@ -172,6 +172,12 @@ export function metricKeyForType(
return "vo2Max";
case "RECOVERY_SCORE":
return "recovery";
case "STRESS_SCORE":
// v1.18.10 I-5 — ladder the COMPUTED-vs-device stress producer so the
// rollup source collapse is deterministic the instant a second producer
// (Oura `daily_stress`) is enabled, rather than falling to the
// alphabetical rank-90 tiebreak in `collapseRollupRowsBySource`.
return "stress";
case "SLEEP_DURATION":
return "sleep";
default:
+12
View File
@@ -131,6 +131,13 @@ export const SOURCE_PRIORITY_METRIC_KEYS = [
// `RECOVERY_SCORE` rows distinguished by source — this ladder lets the
// same picker resolve native-above-proxy without a second engine.
"recovery",
// v1.18.10 I-5 — native-vs-derived stress. HealthLog computes a COMPUTED
// HRV-derived `STRESS_SCORE` proxy; a device-native producer (Oura
// `daily_stress`) is the deferred second source. Laddering it now makes the
// picker deterministic the instant a second producer is enabled, instead of
// collapsing to the alphabetical rank-90 tiebreak. Single-producer today,
// so the ladder is a no-op until the second producer lands.
"stress",
] as const;
export type SourcePriorityMetricKey =
@@ -297,6 +304,11 @@ export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
// fallback for users without a strap. v1.17.0 — Oura readiness + Polar
// nightly recovery slot below WHOOP and above the computed proxy.
recovery: ["WHOOP", "OURA", "POLAR", "COMPUTED"],
// v1.18.10 I-5 — native-vs-derived stress. Oura's device-native
// `daily_stress` (the deferred second producer) outranks HealthLog's
// COMPUTED HRV-derived proxy; the proxy is the fallback for users without a
// device-native stress signal. Single-producer today.
stress: ["OURA", "COMPUTED"],
};
/**