mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Derive recovery rank from the source-priority ladder
This commit is contained in:
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user