fix(insights): unify sleep reconstruction and make WHOOP recovery canonical

Route the derived Sleep Score through the canonical night reconstruction
instead of its own UTC-day grouping, so a night logged by more than one
source is deduplicated to a single writer rather than summed. The score and
the readiness that folds it now read true per-night totals; multi-source
nights drop to their real figure. Emitted minutes and the night stage map
round to integers.

Resolve recovery to one canonical series — WHOOP-native when present, the
computed proxy as fallback — in the wellness reader, the doctor report, and
the coach snapshot, and stop presenting the proxy and the native score as two
numbers. A WHOOP-connected account's recovery now reads the native value on
the tile, the chart, and the printed report alike.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 17:24:59 +02:00
commit e27bd8b9bf
16 files changed
+529 -106

No files matched your search

@@ -61,6 +61,32 @@ describe("buildDerivedSnapshotBlock", () => {
]);
});
it("drops RECOVERY_SCORE when it mirrors READINESS (COMPUTED proxy, no native)", async () => {
// With no WHOOP-native recovery, the COMPUTED proxy IS the readiness blend
// verbatim — feeding both labels hands the model the same number twice.
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 64, band: "yellow" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
expect(block).not.toBeNull();
expect(block!.READINESS).toBeDefined();
// The redundant proxy is removed — readiness once, not twice.
expect(block!.RECOVERY_SCORE).toBeUndefined();
});
it("keeps RECOVERY_SCORE when the WHOOP-native value differs from READINESS", async () => {
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") return ok({ score: 64, band: "yellow" });
if (args?.metric === "RECOVERY_SCORE") return ok({ score: 80, band: "green" });
return insufficient;
});
const block = await buildDerivedSnapshotBlock("u1", PROFILE, NOW);
expect(block!.READINESS?.value).toBe(64);
expect(block!.RECOVERY_SCORE?.value).toBe(80);
});
it("isolates a per-metric compute failure", async () => {
compute.mockImplementation(async (args?: { metric?: string }) => {
if (args?.metric === "READINESS") throw new Error("boom");
+13
View File
@@ -124,5 +124,18 @@ export async function buildDerivedSnapshotBlock(
};
}
// Recovery dedup (one number, one engine). The COMPUTED recovery proxy IS the
// readiness blend verbatim, so when no WHOOP-native row exists the resolved
// recovery equals readiness — feeding both labels would hand the model the
// identical score under two names. Drop the redundant recovery line in that
// case; a WHOOP-native recovery resolves to a DIFFERENT number and both stay.
if (
block.READINESS !== undefined &&
block.RECOVERY_SCORE !== undefined &&
block.RECOVERY_SCORE.value === block.READINESS.value
) {
delete block.RECOVERY_SCORE;
}
return Object.keys(block).length > 0 ? block : null;
}