mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
1 parent
013241ba74
commit
56d8cf19c5
12 files changed
+154
-7
No files matched your search
@@ -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
@@ -3870,7 +3870,8 @@
|
||||
"vo2Max": "VO2 max",
|
||||
"skinTemperature": "Hauttemperatur",
|
||||
"respiratoryRate": "Atemfrequenz",
|
||||
"recovery": "Erholung"
|
||||
"recovery": "Erholung",
|
||||
"stress": "Stress"
|
||||
},
|
||||
"sourceLabels": {
|
||||
"WITHINGS": "Withings",
|
||||
|
||||
+2
-1
@@ -3870,7 +3870,8 @@
|
||||
"vo2Max": "VO2 max",
|
||||
"skinTemperature": "Skin temperature",
|
||||
"respiratoryRate": "Respiratory rate",
|
||||
"recovery": "Recovery"
|
||||
"recovery": "Recovery",
|
||||
"stress": "Stress"
|
||||
},
|
||||
"sourceLabels": {
|
||||
"WITHINGS": "Withings",
|
||||
|
||||
+2
-1
@@ -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
@@ -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
@@ -3870,7 +3870,8 @@
|
||||
"vo2Max": "VO2 max",
|
||||
"skinTemperature": "Temperatura cutanea",
|
||||
"respiratoryRate": "Frequenza respiratoria",
|
||||
"recovery": "Recupero"
|
||||
"recovery": "Recupero",
|
||||
"stress": "Stress"
|
||||
},
|
||||
"sourceLabels": {
|
||||
"WITHINGS": "Withings",
|
||||
|
||||
+2
-1
@@ -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> = {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user