mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
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:
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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user