feat(insights): make sleep debt a configurable source ladder with a computed explainer

Generalise sleep debt onto the existing per-metric source-priority picker
(a new sleepDebt ladder, native-above-computed, default WHOOP > Oura > Polar >
computed) instead of assuming one provider. The rhythm read resolves the active
source off the ladder and carries it on the DTO; when the active source is our
own COMPUTED engine — every user today, since no provider ships a native
sleep-debt balance — the Sleep page explains what the figure means and that it
differs from a wearable's native number. The computed model itself is unchanged
and correct (a short rolling balance of need minus sleep, surplus-credited);
only stale comments that still referenced the old 14-night window are corrected.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 16:04:22 +02:00
1 parent c862b4880f
commit d570d558fa
15 files changed
+245 -23

No files matched your search

+37
View File
@@ -138,6 +138,12 @@ export const SOURCE_PRIORITY_METRIC_KEYS = [
// collapsing to the alphabetical rank-90 tiebreak. Single-producer today,
// so the ladder is a no-op until the second producer lands.
"stress",
// v1.25.0 — native-vs-computed SLEEP DEBT. HealthLog computes a COMPUTED
// rolling-balance debt off the canonical nights; a provider that ships a
// native sleep-debt balance (none today) is the deferred second source.
// Laddering it now keeps the picker deterministic — and the choice user-
// configurable, not WHOOP-hardcoded — the instant a native producer lands.
"sleepDebt",
] as const;
export type SourcePriorityMetricKey =
@@ -309,6 +315,11 @@ export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
// COMPUTED HRV-derived proxy; the proxy is the fallback for users without a
// device-native stress signal. Single-producer today.
stress: ["OURA", "COMPUTED"],
// v1.25.0 — native-vs-computed sleep debt. A device-native sleep-debt
// balance (none ships one today) would outrank HealthLog's COMPUTED
// rolling-balance debt; the COMPUTED engine is the fallback every user lands
// on now. Mirrors the recovery ladder's native-above-computed ordering.
sleepDebt: ["WHOOP", "OURA", "POLAR", "COMPUTED"],
};
/**
@@ -462,6 +473,32 @@ function buildResolved(
return resolved;
}
/**
* v1.25.0 — sources we can currently surface a SLEEP-DEBT figure from. Only
* the COMPUTED engine produces one today; a provider that ships a native
* sleep-debt balance is added here (and ingested) and then wins per the
* user's `sleepDebt` ladder with no change at the call site.
*/
export const SLEEP_DEBT_SUPPLIERS: ReadonlySet<
z.infer<typeof measurementSourceEnum>
> = new Set(["COMPUTED"]);
/**
* v1.25.0 — resolve the active sleep-debt source for a user: walk the
* `sleepDebt` ladder and return the highest-ranked source we can actually
* produce a debt for. Falls back to COMPUTED (always a supplier) when the
* ladder lists no available native source — which is every user today, so the
* UI knows to explain that the figure is our own computed estimate.
*/
export function resolveSleepDebtSource(
resolved: ResolvedSourcePriority,
): z.infer<typeof measurementSourceEnum> {
for (const source of resolved.metricPriority.sleepDebt) {
if (SLEEP_DEBT_SUPPLIERS.has(source)) return source;
}
return "COMPUTED";
}
/**
* v1.4.25 W8c — runtime device-type checker for ingest paths. Returns
* a `DeviceType` for any input the iOS app or Withings webhook might