Derive recovery rank from the source-priority ladder

This commit is contained in:
Marc-André Bombeck committed 2026-06-14 21:23:55 +02:00
1 parent 46f9dfa47f
commit 2da7ea0881
2 files changed
+34 -13

No files matched your search

@@ -3,6 +3,7 @@ import {
resolveCanonicalRecovery,
type RecoveryRow,
} from "../recovery-resolve";
import { DEFAULT_SOURCE_PRIORITY } from "@/lib/validations/source-priority";
function row(iso: string, value: number, source: string): RecoveryRow {
return { value, measuredAt: new Date(iso), source: source as never };
@@ -116,4 +117,26 @@ describe("resolveCanonicalRecovery", () => {
it("returns an empty list for no rows", () => {
expect(resolveCanonicalRecovery([])).toEqual([]);
});
// The per-source authority is derived from `DEFAULT_SOURCE_PRIORITY.recovery`
// (one ordered ladder), not a second hardcoded rank map. For every adjacent
// pair on that ladder the earlier source must win the same night, proving the
// resolution order tracks the ladder rather than a copy of it.
it("resolves in the order of DEFAULT_SOURCE_PRIORITY.recovery", () => {
const ladder = DEFAULT_SOURCE_PRIORITY.recovery;
for (let i = 0; i < ladder.length - 1; i++) {
const higher = ladder[i];
const lower = ladder[i + 1];
// COMPUTED carries the day-that-ended stamp; everything else stamps the
// wake morning. Stamp both for the SAME wake day so they collide.
const stampFor = (src: string) =>
src === "COMPUTED" ? "2026-06-01T12:00:00Z" : "2026-06-02T06:00:00Z";
const resolved = resolveCanonicalRecovery([
row(stampFor(lower), 10, lower),
row(stampFor(higher), 90, higher),
]);
expect(resolved).toHaveLength(1);
expect(resolved[0].source).toBe(higher);
}
});
});
+11 -13
View File
@@ -41,6 +41,7 @@
import type { MeasurementSource } from "@/generated/prisma/client";
import { dayKeyForUserTz } from "@/lib/measurements/consolidation-tz";
import { resolveUserTimezone } from "@/lib/measurements/consolidation-base";
import { DEFAULT_SOURCE_PRIORITY } from "@/lib/validations/source-priority";
const MS_PER_DAY = 24 * 60 * 60 * 1000;
@@ -53,22 +54,19 @@ export interface RecoveryRow {
/**
* The per-source preference for `RECOVERY_SCORE`: a native device row outranks
* the COMPUTED proxy for the same day. Lower number = higher authority. The
* ladder mirrors `DEFAULT_SOURCE_PRIORITY.recovery`
* (`WHOOP > OURA > POLAR > COMPUTED`): WHOOP's all-night strap leads, then the
* Oura ring readiness and the Polar Nightly Recharge band, then the computed
* proxy as the fallback for users without a wearable. A source not listed falls
* below all of them.
* the COMPUTED proxy for the same day. Authority is read straight off
* `DEFAULT_SOURCE_PRIORITY.recovery` (`WHOOP > OURA > POLAR > COMPUTED`) — that
* ordered ladder is the single source of truth, so adding a recovery source
* updates only the priority list and this resolver inherits the new rank for
* free. WHOOP's all-night strap leads, then the Oura ring readiness and the
* Polar Nightly Recharge band, then the computed proxy as the fallback for
* users without a wearable. A source not on the ladder falls below all of them.
*/
const RECOVERY_SOURCE_RANK: Partial<Record<MeasurementSource, number>> = {
WHOOP: 0,
OURA: 1,
POLAR: 2,
COMPUTED: 3,
};
const RECOVERY_LADDER = DEFAULT_SOURCE_PRIORITY.recovery;
function rankOf(source: MeasurementSource): number {
return RECOVERY_SOURCE_RANK[source] ?? Number.MAX_SAFE_INTEGER;
const idx = RECOVERY_LADDER.indexOf(source);
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
}
/**