mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
1 parent
b3fea2ef59
commit
1f4ba5939d
1000 files changed
+12130
-8385
No files matched your search
@@ -89,8 +89,8 @@ describe("gradeBpScore", () => {
|
||||
// floor and descends smoothly. Walk across the systolic floor (90):
|
||||
// dia 66 sits on the optimal plateau (offset −13 → 100) so the
|
||||
// systolic axis is the worst-of(sys,dia) winner across the walk.
|
||||
const sysWalk = [92, 91, 90, 89, 88].map(
|
||||
(sys) => gradeBpScore({ sys, dia: 66, target: UNDER65 }),
|
||||
const sysWalk = [92, 91, 90, 89, 88].map((sys) =>
|
||||
gradeBpScore({ sys, dia: 66, target: UNDER65 }),
|
||||
);
|
||||
// AT and ABOVE the floor sit on the plateau (100).
|
||||
expect(sysWalk[2]).toBe(100); // sys 90
|
||||
@@ -108,8 +108,8 @@ describe("gradeBpScore", () => {
|
||||
// the diastolic axis is the worst-of(sys,dia) winner throughout.
|
||||
// sys 117 sits on the optimal plateau (offset −12 → 100) so the
|
||||
// diastolic axis is the worst-of(sys,dia) winner across the walk.
|
||||
const diaWalk = [52, 51, 50, 49, 48].map(
|
||||
(dia) => gradeBpScore({ sys: 117, dia, target: UNDER65 }),
|
||||
const diaWalk = [52, 51, 50, 49, 48].map((dia) =>
|
||||
gradeBpScore({ sys: 117, dia, target: UNDER65 }),
|
||||
);
|
||||
expect(diaWalk[2]).toBe(100); // dia 50
|
||||
expect(diaWalk[1]).toBe(100); // dia 51
|
||||
@@ -177,9 +177,9 @@ describe("gradeBpScoreFromSeries", () => {
|
||||
{ at: NOW, sys: 134, dia: 87 },
|
||||
{ at: NOW, sys: 134, dia: 87 },
|
||||
];
|
||||
expect(
|
||||
gradeBpScoreFromSeries({ pairs, target: UNDER65, now: NOW }),
|
||||
).toBe(gradeBpScore({ sys: 134, dia: 87, target: UNDER65 }));
|
||||
expect(gradeBpScoreFromSeries({ pairs, target: UNDER65, now: NOW })).toBe(
|
||||
gradeBpScore({ sys: 134, dia: 87, target: UNDER65 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rollup (per-day-mean + count) and live (per-event) agree on the same data", () => {
|
||||
@@ -218,6 +218,8 @@ describe("gradeBpScoreFromSeries", () => {
|
||||
});
|
||||
expect(live).not.toBeNull();
|
||||
expect(rollup).not.toBeNull();
|
||||
expect(Math.abs((live as number) - (rollup as number))).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs((live as number) - (rollup as number))).toBeLessThanOrEqual(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -35,8 +35,10 @@ import {
|
||||
} from "@/lib/rollups/measurement-coverage";
|
||||
import { computeBpInTargetFastPath } from "../bp-in-target-fast-path";
|
||||
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
|
||||
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
|
||||
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
|
||||
@@ -223,11 +225,18 @@ describe("computeBpInTargetFastPath", () => {
|
||||
|
||||
// First findMany = SYS rows, second = DIA rows.
|
||||
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
|
||||
{ id: "1", measuredAt: new Date("2026-05-15T08:00:00.000Z"), value: 122 },
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{ id: "2", measuredAt: new Date("2026-05-15T08:00:00.000Z"), value: 76 },
|
||||
]);
|
||||
{
|
||||
id: "1",
|
||||
measuredAt: new Date("2026-05-15T08:00:00.000Z"),
|
||||
value: 122,
|
||||
},
|
||||
]).mockResolvedValueOnce([
|
||||
{
|
||||
id: "2",
|
||||
measuredAt: new Date("2026-05-15T08:00:00.000Z"),
|
||||
value: 76,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await computeBpInTargetFastPath({
|
||||
userId: "user-partial",
|
||||
|
||||
@@ -94,13 +94,29 @@ interface FixtureEvent {
|
||||
function dailyFixtureEvents(): FixtureEvent[] {
|
||||
return [
|
||||
// Taken on time across several days.
|
||||
{ takenAt: slotAt(1, "08:05"), skipped: false, scheduledFor: slotAt(1, "08:00") },
|
||||
{ takenAt: slotAt(2, "08:10"), skipped: false, scheduledFor: slotAt(2, "08:00") },
|
||||
{ takenAt: slotAt(4, "07:55"), skipped: false, scheduledFor: slotAt(4, "08:00") },
|
||||
{
|
||||
takenAt: slotAt(1, "08:05"),
|
||||
skipped: false,
|
||||
scheduledFor: slotAt(1, "08:00"),
|
||||
},
|
||||
{
|
||||
takenAt: slotAt(2, "08:10"),
|
||||
skipped: false,
|
||||
scheduledFor: slotAt(2, "08:00"),
|
||||
},
|
||||
{
|
||||
takenAt: slotAt(4, "07:55"),
|
||||
skipped: false,
|
||||
scheduledFor: slotAt(4, "08:00"),
|
||||
},
|
||||
// A deliberate skip (excluded from the denominator).
|
||||
{ takenAt: null, skipped: true, scheduledFor: slotAt(3, "08:00") },
|
||||
// A taken-late dose (inside the late tail).
|
||||
{ takenAt: slotAt(5, "10:30"), skipped: false, scheduledFor: slotAt(5, "08:00") },
|
||||
{
|
||||
takenAt: slotAt(5, "10:30"),
|
||||
skipped: false,
|
||||
scheduledFor: slotAt(5, "08:00"),
|
||||
},
|
||||
// Day 6 has no event → the unfilled slot reads missed.
|
||||
// An auto-missed forgotten dose.
|
||||
{
|
||||
@@ -254,7 +270,8 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
|
||||
// counted as a miss.
|
||||
const from = new Date(NOW.getTime() - 30 * DAY_MS);
|
||||
const windowRows = bundle.ledgerRows.filter(
|
||||
(r) => r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
|
||||
(r) =>
|
||||
r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
|
||||
);
|
||||
const series = dailyComplianceRatesFromLedger(windowRows);
|
||||
|
||||
@@ -269,8 +286,7 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
|
||||
|
||||
// The series' mean rate equals the canonical compliance30 rate: the two
|
||||
// are computed from the SAME ledger and cannot contradict each other.
|
||||
const meanRate =
|
||||
series.reduce((sum, d) => sum + d.rate, 0) / series.length;
|
||||
const meanRate = series.reduce((sum, d) => sum + d.rate, 0) / series.length;
|
||||
expect(Math.round(meanRate)).toBe(bundle.compliance30.rate);
|
||||
});
|
||||
|
||||
@@ -300,7 +316,8 @@ describe("dailyComplianceRatesFromLedger — cadence-aware per-day series", () =
|
||||
|
||||
const from = new Date(NOW.getTime() - 30 * DAY_MS);
|
||||
const windowRows = bundle.ledgerRows.filter(
|
||||
(r) => r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
|
||||
(r) =>
|
||||
r.at.getTime() >= from.getTime() && r.at.getTime() <= NOW.getTime(),
|
||||
);
|
||||
const series = dailyComplianceRatesFromLedger(windowRows);
|
||||
|
||||
@@ -331,7 +348,14 @@ describe("tallyLedgerRows — windowed tallies over one shared ledger", () => {
|
||||
);
|
||||
const from = new Date(NOW.getTime() - 30 * DAY_MS);
|
||||
|
||||
const rows = buildComplianceLedgerRows(events, schedules, ctx, from, NOW, NOW);
|
||||
const rows = buildComplianceLedgerRows(
|
||||
events,
|
||||
schedules,
|
||||
ctx,
|
||||
from,
|
||||
NOW,
|
||||
NOW,
|
||||
);
|
||||
expect(tallyLedgerRows(rows)).toEqual(
|
||||
tallyComplianceFromLedger(events, schedules, ctx, from, NOW, NOW),
|
||||
);
|
||||
|
||||
@@ -194,10 +194,7 @@ describe("calculateCompliance — cadence-aware adapter", () => {
|
||||
// `deletedAt: null` filter) and re-added as a fresh taken event.
|
||||
const morning = new Date("2025-01-15T07:05:00Z");
|
||||
const eveningReAdded = new Date("2025-01-15T19:10:00Z");
|
||||
const events = [
|
||||
eventAt(morning, true),
|
||||
eventAt(eveningReAdded, true),
|
||||
];
|
||||
const events = [eventAt(morning, true), eventAt(eveningReAdded, true)];
|
||||
const result = calculateCompliance(events, schedules, 1, undefined, {
|
||||
now: lateNow,
|
||||
});
|
||||
@@ -429,14 +426,16 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function ctx(overrides: Partial<{
|
||||
startsOn: Date | null;
|
||||
endsOn: Date | null;
|
||||
oneShot: boolean;
|
||||
createdAt: Date;
|
||||
lastIntakeAt: Date | null;
|
||||
timeZone: string;
|
||||
}> = {}) {
|
||||
function ctx(
|
||||
overrides: Partial<{
|
||||
startsOn: Date | null;
|
||||
endsOn: Date | null;
|
||||
oneShot: boolean;
|
||||
createdAt: Date;
|
||||
lastIntakeAt: Date | null;
|
||||
timeZone: string;
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
startsOn: null,
|
||||
endsOn: null,
|
||||
@@ -457,7 +456,12 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
|
||||
// tail ends at 12:00) so it reads upcoming, not missed → a clean 100%.
|
||||
// (The off-time / stricter behaviour is pinned in the next test.)
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null, timesOfDay: ["08:00"] },
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const onTime = (d: Date) => ({
|
||||
scheduledFor: d,
|
||||
@@ -489,7 +493,12 @@ describe("calculateCompliance — engine-routed (medicationContext)", () => {
|
||||
// percentage now agrees with the history view (the dose cannot read
|
||||
// "taken" in the % while the ledger calls it ad-hoc).
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null, timesOfDay: ["08:00"] },
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
// takenAt = 08:00 slot + 4.5h = 12:30 (eventAt adds 30min onto a 12:00
|
||||
// scheduledFor; here we pin the off-time take explicitly).
|
||||
@@ -1186,7 +1195,12 @@ describe("expectedSlotCountForDay + lastNonSkippedTakenAt", () => {
|
||||
// the 14th is 19:00 UTC on the 13th — a UTC slice would mislabel it.
|
||||
expect(userDayKey(start, tz)).toBe("2025-01-14");
|
||||
|
||||
const count = expectedSlotCountForDay(schedules, start, dayEnd, ctx({ timeZone: tz }));
|
||||
const count = expectedSlotCountForDay(
|
||||
schedules,
|
||||
start,
|
||||
dayEnd,
|
||||
ctx({ timeZone: tz }),
|
||||
);
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
@@ -1302,7 +1316,7 @@ describe("classifyIntakeTiming", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('respects the configurable `lateMinutes` tail', () => {
|
||||
it("respects the configurable `lateMinutes` tail", () => {
|
||||
// With lateMinutes=30 the late tail collapses to 30 min after the
|
||||
// 3h on-time grace. windowEnd is 09:00 so `on_time` extends to
|
||||
// 12:00 and `late` extends to 12:30. A dose at 12:15 falls in
|
||||
@@ -1733,11 +1747,21 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
|
||||
const slot1900 = new Date("2025-06-15T19:00:00Z");
|
||||
const events = [
|
||||
// 07:00 dose logged on time.
|
||||
{ scheduledFor: slot0700, takenAt: new Date("2025-06-15T07:05:00Z"), skipped: false },
|
||||
{
|
||||
scheduledFor: slot0700,
|
||||
takenAt: new Date("2025-06-15T07:05:00Z"),
|
||||
skipped: false,
|
||||
},
|
||||
// 19:00 dose logged EARLY (before its slot) — snapped onto the 19:00 row.
|
||||
{ scheduledFor: slot1900, takenAt: new Date("2025-06-15T11:30:00Z"), skipped: false },
|
||||
{
|
||||
scheduledFor: slot1900,
|
||||
takenAt: new Date("2025-06-15T11:30:00Z"),
|
||||
skipped: false,
|
||||
},
|
||||
];
|
||||
const display = buildComplianceDisplay(events, schedules, ctx(), { now: NOW });
|
||||
const display = buildComplianceDisplay(events, schedules, ctx(), {
|
||||
now: NOW,
|
||||
});
|
||||
expect(display.currentCycle.nextDueAt).not.toBeNull();
|
||||
// Next open slot is tomorrow 07:00 UTC, not today's resolved 19:00.
|
||||
expect(display.currentCycle.nextDueAt!.toISOString()).toBe(
|
||||
@@ -1756,9 +1780,15 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
|
||||
];
|
||||
const slot0700 = new Date("2025-06-15T07:00:00Z");
|
||||
const events = [
|
||||
{ scheduledFor: slot0700, takenAt: new Date("2025-06-15T07:05:00Z"), skipped: false },
|
||||
{
|
||||
scheduledFor: slot0700,
|
||||
takenAt: new Date("2025-06-15T07:05:00Z"),
|
||||
skipped: false,
|
||||
},
|
||||
];
|
||||
const display = buildComplianceDisplay(events, schedules, ctx(), { now: NOW });
|
||||
const display = buildComplianceDisplay(events, schedules, ctx(), {
|
||||
now: NOW,
|
||||
});
|
||||
expect(display.currentCycle.nextDueAt).not.toBeNull();
|
||||
// Today's 19:00 is still open.
|
||||
expect(display.currentCycle.nextDueAt!.toISOString()).toBe(
|
||||
@@ -2152,16 +2182,25 @@ describe("calculateCompliance — autoMissed forgotten doses (BUG #2 / #3)", ()
|
||||
},
|
||||
];
|
||||
const ctxVal = buildComplianceMedicationContext(
|
||||
{ startsOn: null, endsOn: null, oneShot: false, createdAt: new Date("2025-01-08T00:00:00Z") },
|
||||
{
|
||||
startsOn: null,
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
createdAt: new Date("2025-01-08T00:00:00Z"),
|
||||
},
|
||||
lastNonSkippedTakenAt(events),
|
||||
"UTC",
|
||||
);
|
||||
const display = buildComplianceDisplay(events, daily, ctxVal, { now: NOW });
|
||||
// `expected` is the rate denominator (taken + missed), `missed` counts the
|
||||
// forgotten doses, `taken` the numerator — the "taken / expected" triple.
|
||||
expect(display.short.expected).toBe(display.short.taken + display.short.missed);
|
||||
expect(display.short.expected).toBe(
|
||||
display.short.taken + display.short.missed,
|
||||
);
|
||||
expect(display.short.missed).toBeGreaterThanOrEqual(1);
|
||||
expect(display.long.expected).toBe(display.long.taken + display.long.missed);
|
||||
expect(display.long.expected).toBe(
|
||||
display.long.taken + display.long.missed,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2223,7 +2262,11 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
|
||||
// Cover EVERY slot the window emits so there is no stray uncovered slot:
|
||||
// a 3-day window (NOW − 3d = Jan-12 12:00 .. Jan-15 12:00) holds Jan-12
|
||||
// 19:00, Jan-13 07:00+19:00, Jan-14 07:00+19:00, Jan-15 07:00 = 6 slots.
|
||||
const onTime = (s: Date) => ({ scheduledFor: s, takenAt: s, skipped: false });
|
||||
const onTime = (s: Date) => ({
|
||||
scheduledFor: s,
|
||||
takenAt: s,
|
||||
skipped: false,
|
||||
});
|
||||
const events = [
|
||||
onTime(slot(12, "19:00")),
|
||||
onTime(slot(13, "07:00")),
|
||||
@@ -2231,7 +2274,9 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
|
||||
// 07:00 slot, logged at 08:13 (within the late tail → taken_late).
|
||||
{
|
||||
scheduledFor: slot(14, "07:00"),
|
||||
takenAt: new Date(slot(14, "07:00").getTime() + (1 * 60 + 13) * 60 * 1000),
|
||||
takenAt: new Date(
|
||||
slot(14, "07:00").getTime() + (1 * 60 + 13) * 60 * 1000,
|
||||
),
|
||||
skipped: false,
|
||||
},
|
||||
onTime(slot(14, "19:00")),
|
||||
@@ -2304,7 +2349,11 @@ describe("calculateCompliance — irregular twice-daily taker (BUG 3 verify)", (
|
||||
skipped: false,
|
||||
autoMissed: true,
|
||||
});
|
||||
const extra = (s: Date) => ({ scheduledFor: s, takenAt: s, skipped: false });
|
||||
const extra = (s: Date) => ({
|
||||
scheduledFor: s,
|
||||
takenAt: s,
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
const events = [
|
||||
taken(slot(8, "19:00"), 20, 10),
|
||||
@@ -2355,35 +2404,35 @@ describe("deriveDoseStatus — daily / intraday windows", () => {
|
||||
const target = new Date("2025-01-15T08:00:00Z");
|
||||
|
||||
it("on-time within ±60 min of target", () => {
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T08:30:00Z"))).toBe(
|
||||
"on_time_window",
|
||||
);
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T07:15:00Z"))).toBe(
|
||||
"on_time_window",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T08:30:00Z")),
|
||||
).toBe("on_time_window");
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T07:15:00Z")),
|
||||
).toBe("on_time_window");
|
||||
});
|
||||
|
||||
it("upcoming before the on-time window opens", () => {
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T06:30:00Z"))).toBe(
|
||||
"upcoming",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T06:30:00Z")),
|
||||
).toBe("upcoming");
|
||||
});
|
||||
|
||||
it("overdue between +60 min and +240 min", () => {
|
||||
// +90 min → past on-time (60), before miss cutoff (240).
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T09:30:00Z"))).toBe(
|
||||
"overdue",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T09:30:00Z")),
|
||||
).toBe("overdue");
|
||||
// +239 min → still overdue (takeable).
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T11:59:00Z"))).toBe(
|
||||
"overdue",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T11:59:00Z")),
|
||||
).toBe("overdue");
|
||||
});
|
||||
|
||||
it("missed past +240 min", () => {
|
||||
expect(deriveDoseStatus(target, "daily", new Date("2025-01-15T12:30:00Z"))).toBe(
|
||||
"missed",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "daily", new Date("2025-01-15T12:30:00Z")),
|
||||
).toBe("missed");
|
||||
});
|
||||
|
||||
it("never overlaps the next dose's on-time window", () => {
|
||||
@@ -2424,19 +2473,19 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
|
||||
const target = new Date("2025-01-13T08:00:00Z"); // a Monday shot
|
||||
|
||||
it("on-time within ±1 day of the target day", () => {
|
||||
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-13T20:00:00Z"))).toBe(
|
||||
"on_time_window",
|
||||
);
|
||||
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-14T07:00:00Z"))).toBe(
|
||||
"on_time_window",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "weekly", new Date("2025-01-13T20:00:00Z")),
|
||||
).toBe("on_time_window");
|
||||
expect(
|
||||
deriveDoseStatus(target, "weekly", new Date("2025-01-14T07:00:00Z")),
|
||||
).toBe("on_time_window");
|
||||
});
|
||||
|
||||
it("overdue up to +4 days (still counts when taken — the clinical rule)", () => {
|
||||
// +3 days from target → past the ±1-day on-time window, before +4 days.
|
||||
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-16T12:00:00Z"))).toBe(
|
||||
"overdue",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "weekly", new Date("2025-01-16T12:00:00Z")),
|
||||
).toBe("overdue");
|
||||
// Taken on day +3 still counts as late, not missed.
|
||||
expect(
|
||||
deriveDoseStatus(target, "weekly", new Date("2025-01-20T00:00:00Z"), {
|
||||
@@ -2446,15 +2495,16 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
|
||||
});
|
||||
|
||||
it("missed past +4 days", () => {
|
||||
expect(deriveDoseStatus(target, "weekly", new Date("2025-01-18T12:00:00Z"))).toBe(
|
||||
"missed",
|
||||
);
|
||||
expect(
|
||||
deriveDoseStatus(target, "weekly", new Date("2025-01-18T12:00:00Z")),
|
||||
).toBe("missed");
|
||||
});
|
||||
|
||||
it("defaults match the documented 60-min / 240-min / 4-day boundaries", () => {
|
||||
expect(DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes).toBe(60);
|
||||
expect(
|
||||
DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes + DOSE_WINDOW_DEFAULTS.dailyOverdueMinutes,
|
||||
DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes +
|
||||
DOSE_WINDOW_DEFAULTS.dailyOverdueMinutes,
|
||||
).toBe(240);
|
||||
expect(DOSE_WINDOW_DEFAULTS.weeklyOverdueDays).toBe(4);
|
||||
});
|
||||
@@ -2463,7 +2513,11 @@ describe("deriveDoseStatus — weekly GLP-1 (4-day rule)", () => {
|
||||
describe("doseCadenceFamily", () => {
|
||||
it("rolling ≥ 2 days → weekly", () => {
|
||||
expect(
|
||||
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", rollingIntervalDays: 7 }),
|
||||
doseCadenceFamily({
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
rollingIntervalDays: 7,
|
||||
}),
|
||||
).toBe("weekly");
|
||||
});
|
||||
it("WEEKLY rrule → weekly", () => {
|
||||
@@ -2477,12 +2531,20 @@ describe("doseCadenceFamily", () => {
|
||||
});
|
||||
it("daily rrule → daily", () => {
|
||||
expect(
|
||||
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", rrule: "FREQ=DAILY" }),
|
||||
doseCadenceFamily({
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
rrule: "FREQ=DAILY",
|
||||
}),
|
||||
).toBe("daily");
|
||||
});
|
||||
it("plain legacy daily → daily", () => {
|
||||
expect(
|
||||
doseCadenceFamily({ windowStart: "08:00", windowEnd: "09:00", daysOfWeek: null }),
|
||||
doseCadenceFamily({
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
}),
|
||||
).toBe("daily");
|
||||
});
|
||||
});
|
||||
@@ -2506,7 +2568,10 @@ import {
|
||||
reconstructDoseHistory,
|
||||
type HistoryIntake,
|
||||
} from "@/lib/medications/scheduling/dose-history";
|
||||
import type { CanonicalSchedule, RecurrenceContext } from "@/lib/medications/scheduling/recurrence";
|
||||
import type {
|
||||
CanonicalSchedule,
|
||||
RecurrenceContext,
|
||||
} from "@/lib/medications/scheduling/recurrence";
|
||||
|
||||
describe("tallyComplianceFromLedger — the unified % keystone", () => {
|
||||
const TZ = "Europe/Berlin";
|
||||
|
||||
@@ -40,10 +40,15 @@ import {
|
||||
computeCorrelationHypothesesFastPath,
|
||||
} from "../correlations-fast-path";
|
||||
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
|
||||
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
|
||||
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
|
||||
@@ -82,8 +87,7 @@ describe("computeCorrelationHypothesesFastPath", () => {
|
||||
FULLY_COVERED.mockReturnValue(true);
|
||||
PROBE.mockResolvedValue(coverage);
|
||||
|
||||
ROLLUP_FIND_MANY
|
||||
.mockResolvedValueOnce([]) // SYS buckets
|
||||
ROLLUP_FIND_MANY.mockResolvedValueOnce([]) // SYS buckets
|
||||
.mockResolvedValueOnce([]) // PULSE buckets
|
||||
.mockResolvedValueOnce([]); // WEIGHT buckets
|
||||
|
||||
@@ -141,8 +145,7 @@ describe("computeCorrelationHypothesesFastPath", () => {
|
||||
FULLY_COVERED.mockReturnValue(false);
|
||||
PROBE.mockResolvedValue(coverage);
|
||||
|
||||
MEASUREMENT_FIND_MANY
|
||||
.mockResolvedValueOnce([]) // SYS rows
|
||||
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // SYS rows
|
||||
.mockResolvedValueOnce([]) // PULSE rows
|
||||
.mockResolvedValueOnce([]); // WEIGHT rows
|
||||
|
||||
@@ -250,7 +253,9 @@ describe("computeCorrelationHypothesesFastPath", () => {
|
||||
|
||||
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
|
||||
const corrCall = calls.find((c) => c?.meta?.correlations !== undefined);
|
||||
expect(corrCall?.meta.correlations.tz_guard).toBe("non-utc-live-fallback");
|
||||
expect(corrCall?.meta.correlations.tz_guard).toBe(
|
||||
"non-utc-live-fallback",
|
||||
);
|
||||
expect(corrCall?.meta.correlations.path).toBe("live");
|
||||
});
|
||||
|
||||
|
||||
@@ -102,4 +102,3 @@ describe("pearsonCorrelation", () => {
|
||||
expect(result.strength).toBe("keine");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,13 +254,10 @@ describe("computeGlucoseClinicalMetrics — window + adequacy", () => {
|
||||
// 30/day. That clears the bar legitimately, but a handful in a day must
|
||||
// not: 10 readings in one day = 10/day < 24 → still a spot estimate.
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
const readings: GlucoseReading[] = Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) => ({
|
||||
mgdl: 115,
|
||||
measuredAt: new Date(now.getTime() - i * HOUR),
|
||||
}),
|
||||
);
|
||||
const readings: GlucoseReading[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
mgdl: 115,
|
||||
measuredAt: new Date(now.getTime() - i * HOUR),
|
||||
}));
|
||||
const m = computeGlucoseClinicalMetrics(readings, { now });
|
||||
expect(m.isSpotEstimate).toBe(true);
|
||||
});
|
||||
@@ -309,10 +306,9 @@ describe("computeGlucoseClinicalMetrics — window + adequacy", () => {
|
||||
it("clears above both thresholds and asserts the panel", () => {
|
||||
// 14 readings, one per day → 13-day span, count 14
|
||||
const values = Array.from({ length: 14 }, () => 154);
|
||||
const m = computeGlucoseClinicalMetrics(
|
||||
dailyReadings(values, now),
|
||||
{ now },
|
||||
);
|
||||
const m = computeGlucoseClinicalMetrics(dailyReadings(values, now), {
|
||||
now,
|
||||
});
|
||||
expect(m.readingCount).toBe(14);
|
||||
expect(m.actualSpanDays).toBeCloseTo(13, 6);
|
||||
expect(m.stillLearning).toBe(false);
|
||||
|
||||
@@ -38,9 +38,7 @@ function readings() {
|
||||
}
|
||||
|
||||
/** Coach projection — mirrors `snapshot.glucose.clinical` (asserted branch). */
|
||||
function coachProjection(
|
||||
m: ReturnType<typeof computeGlucoseClinicalMetrics>,
|
||||
) {
|
||||
function coachProjection(m: ReturnType<typeof computeGlucoseClinicalMetrics>) {
|
||||
return {
|
||||
tirPercent: m.distribution ? Math.round(m.distribution.tir * 100) : null,
|
||||
gmi: m.gmi !== null ? Math.round(m.gmi * 10) / 10 : null,
|
||||
@@ -52,9 +50,7 @@ function coachProjection(
|
||||
}
|
||||
|
||||
/** Panel projection — mirrors the panel's display rounding. */
|
||||
function panelProjection(
|
||||
m: ReturnType<typeof computeGlucoseClinicalMetrics>,
|
||||
) {
|
||||
function panelProjection(m: ReturnType<typeof computeGlucoseClinicalMetrics>) {
|
||||
return {
|
||||
tirPercent: m.distribution ? Math.round(m.distribution.tir * 100) : null,
|
||||
gmi: m.gmi !== null ? Number(m.gmi.toFixed(1)) : null,
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_WINDOW_DAYS, GLUCOSE_PANEL_WINDOW_DAYS } from "../glucose-metrics";
|
||||
import {
|
||||
DEFAULT_WINDOW_DAYS,
|
||||
GLUCOSE_PANEL_WINDOW_DAYS,
|
||||
} from "../glucose-metrics";
|
||||
|
||||
const ROOT = join(__dirname, "..", "..", "..", "..");
|
||||
|
||||
@@ -43,7 +46,9 @@ describe("glucose panel window constant", () => {
|
||||
// It must NOT be a bare `windowDays: 30` glucose literal.
|
||||
const usesConstantDirectly = /GLUCOSE_PANEL_WINDOW_DAYS/.test(src);
|
||||
const aliasesConstant =
|
||||
/GLUCOSE_CLINICAL_WINDOW_DAYS\s*=\s*GLUCOSE_PANEL_WINDOW_DAYS/.test(src);
|
||||
/GLUCOSE_CLINICAL_WINDOW_DAYS\s*=\s*GLUCOSE_PANEL_WINDOW_DAYS/.test(
|
||||
src,
|
||||
);
|
||||
expect(usesConstantDirectly || aliasesConstant).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,11 +37,17 @@ import {
|
||||
} from "@/lib/rollups/measurement-coverage";
|
||||
import { computeUserHealthScoreFastPath } from "../health-score-fast-path";
|
||||
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MEDICATION_FIND_MANY = prisma.medication.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MEASUREMENT_FIND_MANY = prisma.measurement
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const MOOD_FIND_MANY = prisma.moodEntry.findMany as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const MEDICATION_FIND_MANY = prisma.medication
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const INTAKE_FIND_MANY = prisma.medicationIntakeEvent
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const PROBE = probeRollupCoverage as unknown as ReturnType<typeof vi.fn>;
|
||||
const FULLY_COVERED = isFullyCovered as unknown as ReturnType<typeof vi.fn>;
|
||||
const ANNOTATE = annotate as unknown as ReturnType<typeof vi.fn>;
|
||||
@@ -110,17 +116,16 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
// null` and the score shape is unchanged.
|
||||
ROLLUP_FIND_MANY.mockResolvedValueOnce([]);
|
||||
// Weight source-attribution read (narrow 2-column).
|
||||
MEASUREMENT_FIND_MANY
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
|
||||
source: "WITHINGS",
|
||||
},
|
||||
{
|
||||
measuredAt: new Date("2026-05-14T08:00:00.000Z"),
|
||||
source: "WITHINGS",
|
||||
},
|
||||
])
|
||||
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
|
||||
{
|
||||
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
|
||||
source: "WITHINGS",
|
||||
},
|
||||
{
|
||||
measuredAt: new Date("2026-05-14T08:00:00.000Z"),
|
||||
source: "WITHINGS",
|
||||
},
|
||||
])
|
||||
// BP-SYS source attribution read (parallel after rollup).
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
@@ -311,18 +316,14 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
// raw weight findMany is already doing the work.
|
||||
expect(ROLLUP_FIND_MANY).not.toHaveBeenCalled();
|
||||
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
|
||||
const pathCall = calls.find(
|
||||
(c) => c?.meta?.healthScore?.path === "live",
|
||||
);
|
||||
const pathCall = calls.find((c) => c?.meta?.healthScore?.path === "live");
|
||||
expect(pathCall?.meta?.healthScore?.weightLongWindow).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("live fallback — WEIGHT not covered", () => {
|
||||
it("reads raw weight rows and pins path:live", async () => {
|
||||
const coverage = new Map<string, boolean>([
|
||||
["WEIGHT", false],
|
||||
]);
|
||||
const coverage = new Map<string, boolean>([["WEIGHT", false]]);
|
||||
FULLY_COVERED.mockReturnValue(false);
|
||||
PROBE.mockResolvedValue(coverage);
|
||||
|
||||
@@ -354,9 +355,7 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
|
||||
const pathCall = calls.find(
|
||||
(c) => c?.meta?.healthScore?.path === "live",
|
||||
);
|
||||
const pathCall = calls.find((c) => c?.meta?.healthScore?.path === "live");
|
||||
expect(pathCall).toBeDefined();
|
||||
// Live path read raw weight (value selected).
|
||||
const weightCall = MEASUREMENT_FIND_MANY.mock.calls[0][0];
|
||||
@@ -491,9 +490,9 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
// care here that the two windows are NOT pinned to the same
|
||||
// value any more.
|
||||
const componentDelta = result?.delta;
|
||||
expect(typeof componentDelta === "number" || componentDelta === null).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
typeof componentDelta === "number" || componentDelta === null,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to bpInTargetPct when bpInTargetPctPriorWeek is omitted", async () => {
|
||||
@@ -802,8 +801,7 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
|
||||
const now = new Date("2026-06-07T12:00:00.000Z");
|
||||
// Live fallback — no weight/mood/med data; only BP signal present.
|
||||
MEASUREMENT_FIND_MANY
|
||||
.mockResolvedValueOnce([]) // weight raw read (live fallback)
|
||||
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // weight raw read (live fallback)
|
||||
.mockResolvedValueOnce([]); // BP-SYS source attribution
|
||||
|
||||
const result = await computeUserHealthScoreFastPath({
|
||||
@@ -828,8 +826,7 @@ describe("computeUserHealthScoreFastPath", () => {
|
||||
PROBE.mockResolvedValue(coverage);
|
||||
|
||||
const now = new Date("2026-06-07T12:00:00.000Z");
|
||||
MEASUREMENT_FIND_MANY
|
||||
.mockResolvedValueOnce([]) // weight raw read
|
||||
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]) // weight raw read
|
||||
.mockResolvedValueOnce([]); // BP-SYS source attribution
|
||||
// Mood present so the score is computable even without BP.
|
||||
MOOD_FIND_MANY.mockResolvedValue(
|
||||
|
||||
@@ -76,7 +76,11 @@ describe("buildHealthScoreBpInputs — single Health-Score BP input builder", ()
|
||||
// path), but here it is genuinely thin so the rate is the all-time pct.
|
||||
// Pin the stricter case: when all-time is also null the pillar collapses.
|
||||
const thin = buildHealthScoreBpInputs(
|
||||
env({ last90Days: { pct: 100, pairs: 4 }, allTime: null, gradedScore: 95 }),
|
||||
env({
|
||||
last90Days: { pct: 100, pairs: 4 },
|
||||
allTime: null,
|
||||
gradedScore: 95,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
expect(thin.bpInTargetPct).toBeNull();
|
||||
|
||||
@@ -101,7 +101,10 @@ describe("weightTrendAlignment", () => {
|
||||
it("scores the bare trend (non-null) when the target is missing but ≥2 readings exist", () => {
|
||||
// No target → fall back to trend-only scoring instead of returning
|
||||
// null, so a height-less user with weight data gets a populated pillar.
|
||||
const result = weightTrendAlignment(weightSeries([80, 80.5, 81, 81.5]), null);
|
||||
const result = weightTrendAlignment(
|
||||
weightSeries([80, 80.5, 81, 81.5]),
|
||||
null,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!).toBeGreaterThanOrEqual(0);
|
||||
expect(result!).toBeLessThanOrEqual(100);
|
||||
@@ -112,13 +115,19 @@ describe("weightTrendAlignment", () => {
|
||||
});
|
||||
|
||||
it("rewards a gentle decline (>75) when there is no target", () => {
|
||||
const result = weightTrendAlignment(weightSeries([84, 83.5, 83, 82.5]), null);
|
||||
const result = weightTrendAlignment(
|
||||
weightSeries([84, 83.5, 83, 82.5]),
|
||||
null,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!).toBeGreaterThan(75);
|
||||
});
|
||||
|
||||
it("penalises a sustained rise (<75) when there is no target", () => {
|
||||
const result = weightTrendAlignment(weightSeries([80, 81, 82, 83, 84]), null);
|
||||
const result = weightTrendAlignment(
|
||||
weightSeries([80, 81, 82, 83, 84]),
|
||||
null,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!).toBeLessThan(75);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,9 @@ describe("deriveRestingProxyFromPulse", () => {
|
||||
// One Berlin day: 10 resting reads ~70, 500 workout reads ~150.
|
||||
const samples: PulseSample[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
samples.push(day(`2026-06-01T06:${String(i).padStart(2, "0")}:00`, 68 + i));
|
||||
samples.push(
|
||||
day(`2026-06-01T06:${String(i).padStart(2, "0")}:00`, 68 + i),
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < 500; i++) {
|
||||
samples.push(
|
||||
@@ -48,7 +50,12 @@ describe("deriveRestingProxyFromPulse", () => {
|
||||
// Realistic day: 30 waking/spot reads 70-95, 10 workout reads ~150.
|
||||
const samples: PulseSample[] = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
samples.push(day(`2026-06-02T08:${String(i % 60).padStart(2, "0")}:00`, 70 + (i % 25)));
|
||||
samples.push(
|
||||
day(
|
||||
`2026-06-02T08:${String(i % 60).padStart(2, "0")}:00`,
|
||||
70 + (i % 25),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
samples.push(day(`2026-06-02T18:${String(i).padStart(2, "0")}:00`, 150));
|
||||
@@ -107,7 +114,10 @@ describe("deriveRestingProxyFromPulse", () => {
|
||||
describe("resolveRestingPulseSeries", () => {
|
||||
it("prefers RESTING_HEART_RATE — a clean ~72 series reads as resting", () => {
|
||||
const restingSamples: PulseSample[] = Array.from({ length: 30 }, (_v, i) =>
|
||||
day(`2026-06-${String((i % 28) + 1).padStart(2, "0")}T06:00:00`, 70 + (i % 5)),
|
||||
day(
|
||||
`2026-06-${String((i % 28) + 1).padStart(2, "0")}T06:00:00`,
|
||||
70 + (i % 5),
|
||||
),
|
||||
);
|
||||
// Workout-polluted PULSE that should be IGNORED in favour of resting.
|
||||
const pulseSamples: PulseSample[] = Array.from({ length: 500 }, () =>
|
||||
|
||||
@@ -486,9 +486,7 @@ describe("reconstructSleepSessions", () => {
|
||||
expect(sessions[0].source).toBe("APPLE_HEALTH");
|
||||
// The surviving segments carry a real timeline: more than one
|
||||
// distinct end instant.
|
||||
const ends = new Set(
|
||||
sessions[0].segments.map((seg) => seg.end.getTime()),
|
||||
);
|
||||
const ends = new Set(sessions[0].segments.map((seg) => seg.end.getTime()));
|
||||
expect(ends.size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -243,12 +243,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
|
||||
default: ["phone", "watch"],
|
||||
},
|
||||
};
|
||||
const out = pickCanonicalSourceRows(
|
||||
rows,
|
||||
"steps",
|
||||
userPriority,
|
||||
isoDayKey,
|
||||
);
|
||||
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
|
||||
expect(out.canonicalRows).toHaveLength(1);
|
||||
expect(out.canonicalRows[0].deviceType).toBe("phone");
|
||||
expect(out.canonicalRows[0].value).toBe(4100);
|
||||
@@ -280,12 +275,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
|
||||
ACTIVITY_STEPS: ["phone", "watch"],
|
||||
},
|
||||
};
|
||||
const out = pickCanonicalSourceRows(
|
||||
rows,
|
||||
"steps",
|
||||
userPriority,
|
||||
isoDayKey,
|
||||
);
|
||||
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
|
||||
expect(out.canonicalRows).toHaveLength(1);
|
||||
expect(out.canonicalRows[0].deviceType).toBe("phone");
|
||||
});
|
||||
@@ -365,12 +355,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
|
||||
default: ["ring"],
|
||||
},
|
||||
};
|
||||
const out = pickCanonicalSourceRows(
|
||||
rows,
|
||||
"steps",
|
||||
userPriority,
|
||||
isoDayKey,
|
||||
);
|
||||
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
|
||||
expect(out.canonicalRows).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -425,12 +410,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
|
||||
ACTIVITY_STEPS: ["watch", "phone"],
|
||||
},
|
||||
};
|
||||
const out = pickCanonicalSourceRows(
|
||||
rows,
|
||||
"steps",
|
||||
userPriority,
|
||||
isoDayKey,
|
||||
);
|
||||
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
|
||||
// Expect: WEIGHT's scale row + ACTIVITY_STEPS' watch row. Both
|
||||
// types' losers (WEIGHT's watch row, ACTIVITY_STEPS' phone row)
|
||||
// are dropped against their OWN ladder, not the bucket's first
|
||||
@@ -485,12 +465,7 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
|
||||
WEIGHT: ["ring"],
|
||||
},
|
||||
};
|
||||
const out = pickCanonicalSourceRows(
|
||||
rows,
|
||||
"steps",
|
||||
userPriority,
|
||||
isoDayKey,
|
||||
);
|
||||
const out = pickCanonicalSourceRows(rows, "steps", userPriority, isoDayKey);
|
||||
// 2 WEIGHT rows kept (per-type fall-through) + 1 ACTIVITY_STEPS
|
||||
// watch row (phone dropped against default ladder).
|
||||
expect(out.canonicalRows).toHaveLength(3);
|
||||
|
||||
@@ -69,16 +69,18 @@ import { computeSummariesSlice } from "../summaries-slice";
|
||||
|
||||
const RAW = prisma.$queryRaw as unknown as ReturnType<typeof vi.fn>;
|
||||
const UNSAFE = prisma.$queryRawUnsafe as unknown as ReturnType<typeof vi.fn>;
|
||||
const USER_FIND_UNIQUE =
|
||||
prisma.user.findUnique as unknown as ReturnType<typeof vi.fn>;
|
||||
const MEASUREMENT_FIND_FIRST =
|
||||
prisma.measurement.findFirst as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY =
|
||||
prisma.measurementRollup.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_FIRST =
|
||||
prisma.measurementRollup.findFirst as unknown as ReturnType<typeof vi.fn>;
|
||||
const CANONICAL_FROM =
|
||||
canonicalMeasurementsFrom as unknown as ReturnType<typeof vi.fn>;
|
||||
const USER_FIND_UNIQUE = prisma.user.findUnique as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const MEASUREMENT_FIND_FIRST = prisma.measurement
|
||||
.findFirst as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_MANY = prisma.measurementRollup
|
||||
.findMany as unknown as ReturnType<typeof vi.fn>;
|
||||
const ROLLUP_FIND_FIRST = prisma.measurementRollup
|
||||
.findFirst as unknown as ReturnType<typeof vi.fn>;
|
||||
const CANONICAL_FROM = canonicalMeasurementsFrom as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
RAW.mockReset();
|
||||
@@ -603,4 +605,3 @@ describe("computeSummariesSlice", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
} from "../window-confidence";
|
||||
|
||||
const NOW = new Date("2026-06-14T12:00:00.000Z");
|
||||
const daysAgo = (n: number) => new Date(NOW.getTime() - n * 24 * 60 * 60 * 1000);
|
||||
const daysAgo = (n: number) =>
|
||||
new Date(NOW.getTime() - n * 24 * 60 * 60 * 1000);
|
||||
|
||||
describe("computeWindowConfidence", () => {
|
||||
it("flags a sample below the floor as insufficient", () => {
|
||||
|
||||
@@ -51,10 +51,7 @@
|
||||
* Pure & deterministic — fully unit-tested in `__tests__/bp-grade.test.ts`.
|
||||
*/
|
||||
import type { BpTargets } from "./bp-targets";
|
||||
import {
|
||||
SYS_HYPOTENSION_FLOOR,
|
||||
DIA_HYPOTENSION_FLOOR,
|
||||
} from "./bp-in-target";
|
||||
import { SYS_HYPOTENSION_FLOOR, DIA_HYPOTENSION_FLOOR } from "./bp-in-target";
|
||||
|
||||
/**
|
||||
* Offset-from-ceiling → axis-score anchors. Offsets are in mmHg; the
|
||||
|
||||
@@ -310,7 +310,12 @@ async function computeFromRollups(
|
||||
// All-time on the rollup path is bounded to the 395-day read window.
|
||||
// Documented above — same trade-off the per-event path makes when the
|
||||
// user's history extends beyond 365 days.
|
||||
const allTime = bucketWindow(pairsByDay, threeNinetyFiveDaysAgo, now, targets);
|
||||
const allTime = bucketWindow(
|
||||
pairsByDay,
|
||||
threeNinetyFiveDaysAgo,
|
||||
now,
|
||||
targets,
|
||||
);
|
||||
const priorMonth = bucketWindow(
|
||||
pairsByDay,
|
||||
sixtyDaysAgo,
|
||||
|
||||
@@ -328,8 +328,13 @@ export function deriveDoseStatus(
|
||||
* the 4-day rule); everything denser (daily, intraday multi-dose) is
|
||||
* `daily` (minute-scale windows).
|
||||
*/
|
||||
export function doseCadenceFamily(schedule: ComplianceSchedule): DoseCadenceFamily {
|
||||
if (schedule.rollingIntervalDays != null && schedule.rollingIntervalDays >= 2) {
|
||||
export function doseCadenceFamily(
|
||||
schedule: ComplianceSchedule,
|
||||
): DoseCadenceFamily {
|
||||
if (
|
||||
schedule.rollingIntervalDays != null &&
|
||||
schedule.rollingIntervalDays >= 2
|
||||
) {
|
||||
return "weekly";
|
||||
}
|
||||
const rrule = schedule.rrule ?? "";
|
||||
@@ -496,7 +501,9 @@ function rollingIntakeInstants(
|
||||
return events
|
||||
.filter(
|
||||
(e): e is { takenAt: Date; skipped: boolean } =>
|
||||
!e.skipped && e.takenAt !== null && e.takenAt.getTime() <= now.getTime(),
|
||||
!e.skipped &&
|
||||
e.takenAt !== null &&
|
||||
e.takenAt.getTime() <= now.getTime(),
|
||||
)
|
||||
.map((e) => e.takenAt)
|
||||
.sort((a, b) => a.getTime() - b.getTime());
|
||||
@@ -584,7 +591,9 @@ function intakeInstantsAtOrBefore(
|
||||
return intakes
|
||||
.filter(
|
||||
(e): e is { takenAt: Date; skipped: boolean } =>
|
||||
!e.skipped && e.takenAt !== null && e.takenAt.getTime() <= now.getTime(),
|
||||
!e.skipped &&
|
||||
e.takenAt !== null &&
|
||||
e.takenAt.getTime() <= now.getTime(),
|
||||
)
|
||||
.map((e) => e.takenAt)
|
||||
.sort((a, b) => a.getTime() - b.getTime());
|
||||
@@ -658,7 +667,13 @@ export function expectedSlotCountForDay(
|
||||
ctx.scheduleRevisions ?? [],
|
||||
schedules.map((s, i) => toCanonicalSchedule(s, `compliance-daily-${i}`)),
|
||||
(schedule, eraFrom, eraTo) =>
|
||||
expandComplianceOccurrences(schedule, recurrenceCtx, eraFrom, eraTo, retro),
|
||||
expandComplianceOccurrences(
|
||||
schedule,
|
||||
recurrenceCtx,
|
||||
eraFrom,
|
||||
eraTo,
|
||||
retro,
|
||||
),
|
||||
{ oneShot: ctx.oneShot },
|
||||
).length;
|
||||
}
|
||||
@@ -712,7 +727,13 @@ export function expectedSlotsBetween(
|
||||
ctx.scheduleRevisions ?? [],
|
||||
schedules.map((s, i) => toCanonicalSchedule(s, `compliance-slots-${i}`)),
|
||||
(schedule, eraFrom, eraTo) =>
|
||||
expandComplianceOccurrences(schedule, recurrenceCtx, eraFrom, eraTo, retro),
|
||||
expandComplianceOccurrences(
|
||||
schedule,
|
||||
recurrenceCtx,
|
||||
eraFrom,
|
||||
eraTo,
|
||||
retro,
|
||||
),
|
||||
{ oneShot: ctx.oneShot },
|
||||
);
|
||||
}
|
||||
@@ -735,12 +756,13 @@ export const MIN_STABLE_DOSES = 4;
|
||||
* expected doses to mean something, up to a 12-month long window for very
|
||||
* rare meds.
|
||||
*/
|
||||
export const COMPLIANCE_WINDOW_LADDER: ReadonlyArray<readonly [number, number]> =
|
||||
[
|
||||
[7, 30],
|
||||
[30, 90],
|
||||
[90, 365],
|
||||
];
|
||||
export const COMPLIANCE_WINDOW_LADDER: ReadonlyArray<
|
||||
readonly [number, number]
|
||||
> = [
|
||||
[7, 30],
|
||||
[30, 90],
|
||||
[90, 365],
|
||||
];
|
||||
|
||||
/**
|
||||
* v1.8.6 — the day-counts of the two compliance windows a medication's
|
||||
@@ -803,10 +825,7 @@ export function selectComplianceWindows(
|
||||
for (const [shortDays, longDays] of COMPLIANCE_WINDOW_LADDER) {
|
||||
const expectedShort = expected(shortDays);
|
||||
const expectedLong = expected(longDays);
|
||||
if (
|
||||
expectedShort >= MIN_STABLE_DOSES &&
|
||||
expectedLong >= MIN_STABLE_DOSES
|
||||
) {
|
||||
if (expectedShort >= MIN_STABLE_DOSES && expectedLong >= MIN_STABLE_DOSES) {
|
||||
return { shortDays, longDays, expectedShort, expectedLong };
|
||||
}
|
||||
}
|
||||
@@ -886,7 +905,13 @@ export interface ComplianceDisplay {
|
||||
* `missed` the count counting against the rate. The card can render
|
||||
* "26 / 30 · 87%" from `taken` / `expected`.
|
||||
*/
|
||||
short: { rate: number; taken: number; expected: number; missed: number; streak: number };
|
||||
short: {
|
||||
rate: number;
|
||||
taken: number;
|
||||
expected: number;
|
||||
missed: number;
|
||||
streak: number;
|
||||
};
|
||||
/** Compliance percentage + counts over the long window. */
|
||||
long: { rate: number; taken: number; expected: number; missed: number };
|
||||
/**
|
||||
@@ -967,8 +992,7 @@ export function buildCurrentCycle(
|
||||
const recurrenceCtx = toRecurrenceCtx(ctx, "compliance-cycle");
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const allIntakes = intakes ?? [];
|
||||
const lastIntakeAt =
|
||||
lastNonSkippedTakenAt(allIntakes) ?? ctx.lastIntakeAt;
|
||||
const lastIntakeAt = lastNonSkippedTakenAt(allIntakes) ?? ctx.lastIntakeAt;
|
||||
|
||||
// The open cycle's due instant + grace, picked as the SOONEST across every
|
||||
// schedule. Two paths:
|
||||
@@ -999,10 +1023,9 @@ export function buildCurrentCycle(
|
||||
const anchor =
|
||||
lastIntakeAt !== null
|
||||
? new Date(lastIntakeAt.getTime() + n * DAY_MS)
|
||||
: ctx.startsOn ?? ctx.createdAt;
|
||||
: (ctx.startsOn ?? ctx.createdAt);
|
||||
if (ctx.endsOn && anchor.getTime() > ctx.endsOn.getTime()) continue;
|
||||
const graceMs =
|
||||
(canonical.reminderGraceMinutes ?? 60) * 60 * 1000;
|
||||
const graceMs = (canonical.reminderGraceMinutes ?? 60) * 60 * 1000;
|
||||
consider(anchor, new Date(anchor.getTime() + graceMs));
|
||||
continue;
|
||||
}
|
||||
@@ -1032,7 +1055,12 @@ export function buildCurrentCycle(
|
||||
const hasClosedCycles = expectedShort > 0;
|
||||
|
||||
if (dueAt === null || graceUntil === null) {
|
||||
return { state: "none", nextDueAt: null, graceUntil: null, hasClosedCycles };
|
||||
return {
|
||||
state: "none",
|
||||
nextDueAt: null,
|
||||
graceUntil: null,
|
||||
hasClosedCycles,
|
||||
};
|
||||
}
|
||||
|
||||
// Narrow the union-mutated locals for the comparisons below.
|
||||
@@ -1073,10 +1101,16 @@ export function buildComplianceDisplay(
|
||||
const { shortDays, longDays, expectedShort, expectedLong } =
|
||||
selectComplianceWindows(schedules, ctx, { now, intakes: events });
|
||||
|
||||
const short = calculateCompliance(events, schedules, shortDays, ctx.createdAt, {
|
||||
now,
|
||||
medicationContext: ctx,
|
||||
});
|
||||
const short = calculateCompliance(
|
||||
events,
|
||||
schedules,
|
||||
shortDays,
|
||||
ctx.createdAt,
|
||||
{
|
||||
now,
|
||||
medicationContext: ctx,
|
||||
},
|
||||
);
|
||||
const long = calculateCompliance(events, schedules, longDays, ctx.createdAt, {
|
||||
now,
|
||||
medicationContext: ctx,
|
||||
@@ -1468,9 +1502,12 @@ export function calculateCompliance(
|
||||
// legacy timeline tally byte-stable — they have no engine context to mint
|
||||
// bands from.
|
||||
const ledgerCtx = options?.medicationContext;
|
||||
let ledgerCounts:
|
||||
| { taken: number; skipped: number; missed: number; rate: number }
|
||||
| null = null;
|
||||
let ledgerCounts: {
|
||||
taken: number;
|
||||
skipped: number;
|
||||
missed: number;
|
||||
rate: number;
|
||||
} | null = null;
|
||||
if (ledgerCtx) {
|
||||
const tally = tallyComplianceFromLedger(
|
||||
events,
|
||||
@@ -1598,8 +1635,7 @@ export function calculateCompliance(
|
||||
// Skipped doses are excluded from the denominator — they represent a
|
||||
// deliberate user decision rather than a missed dose.
|
||||
const denom = taken + missed;
|
||||
rate =
|
||||
denom > 0 ? Math.min(100, Math.round((taken / denom) * 100)) : 100;
|
||||
rate = denom > 0 ? Math.min(100, Math.round((taken / denom) * 100)) : 100;
|
||||
}
|
||||
|
||||
const totalExpected = taken + skipped + missed;
|
||||
@@ -1726,7 +1762,10 @@ export function buildMedicationComplianceBundle(
|
||||
1,
|
||||
Math.ceil((now.getTime() - effectiveStart.getTime()) / ONE_DAY_MS),
|
||||
);
|
||||
const tally = tallyLedgerRows(ledgerRows, { from: effectiveStart, to: now });
|
||||
const tally = tallyLedgerRows(ledgerRows, {
|
||||
from: effectiveStart,
|
||||
to: now,
|
||||
});
|
||||
const { current: streak } = streaksFromTimeline(
|
||||
timeline,
|
||||
now,
|
||||
@@ -1834,7 +1873,13 @@ export function buildMedicationComplianceBundle(
|
||||
currentDose,
|
||||
};
|
||||
|
||||
return { compliance7, compliance30, complianceDisplay, ledgerRows, ledgerFrom };
|
||||
return {
|
||||
compliance7,
|
||||
compliance30,
|
||||
complianceDisplay,
|
||||
ledgerRows,
|
||||
ledgerFrom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,7 +174,14 @@ export async function computeCorrelationHypothesesFastPath(
|
||||
// reader so the per-type fan-out doesn't re-query the user 3×.
|
||||
const priority = await loadUserSourcePriority(userId);
|
||||
const [sysBuckets, pulseBuckets, weightBuckets] = await Promise.all([
|
||||
readRollupBuckets(userId, "BLOOD_PRESSURE_SYS", "DAY", since, now, priority),
|
||||
readRollupBuckets(
|
||||
userId,
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
"DAY",
|
||||
since,
|
||||
now,
|
||||
priority,
|
||||
),
|
||||
readRollupBuckets(userId, "PULSE", "DAY", since, now, priority),
|
||||
readRollupBuckets(userId, "WEIGHT", "DAY", since, now, priority),
|
||||
]);
|
||||
|
||||
@@ -104,4 +104,3 @@ export function pearsonCorrelation(
|
||||
|
||||
return { r, strength, n };
|
||||
}
|
||||
|
||||
@@ -146,7 +146,6 @@ export interface TimeInRangeDistribution {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Variability summary: SD + CV% with the Monnier stability flag. */
|
||||
export interface GlucoseVariability {
|
||||
/** Sample standard deviation (n−1), mg/dL. */
|
||||
@@ -245,7 +244,9 @@ export function glucoseSD(values: number[]): number | null {
|
||||
* Monnier 2017, Diabetes Care 40(7):832-838, DOI 10.2337/dc16-1769;
|
||||
* endorsed in Battelino 2019.
|
||||
*/
|
||||
export function glucoseVariability(values: number[]): GlucoseVariability | null {
|
||||
export function glucoseVariability(
|
||||
values: number[],
|
||||
): GlucoseVariability | null {
|
||||
const sd = glucoseSD(values);
|
||||
if (sd === null) return null;
|
||||
const m = mean(values);
|
||||
@@ -344,9 +345,7 @@ export function bloodGlucoseRiskIndices(
|
||||
* level-1), so the five fractions are NOT mutually exclusive by design.
|
||||
* Battelino 2019, Diabetes Care 42(8):1593-1603, DOI 10.2337/dci19-0028.
|
||||
*/
|
||||
export function timeInRange(
|
||||
values: number[],
|
||||
): TimeInRangeDistribution | null {
|
||||
export function timeInRange(values: number[]): TimeInRangeDistribution | null {
|
||||
const n = values.length;
|
||||
if (n === 0) return null;
|
||||
|
||||
|
||||
@@ -235,7 +235,9 @@ export async function computeUserHealthScoreFastPath(
|
||||
kg: b.mean,
|
||||
}));
|
||||
weightSeriesPrev30d = dayBuckets
|
||||
.filter((b) => b.bucketStart >= prevSince30d && b.bucketStart <= prevUntil)
|
||||
.filter(
|
||||
(b) => b.bucketStart >= prevSince30d && b.bucketStart <= prevUntil,
|
||||
)
|
||||
.map((b) => ({
|
||||
date: b.bucketStart.toISOString(),
|
||||
kg: b.mean,
|
||||
@@ -266,7 +268,9 @@ export async function computeUserHealthScoreFastPath(
|
||||
select: { measuredAt: true, source: true },
|
||||
orderBy: { measuredAt: "asc" },
|
||||
});
|
||||
weightSourcesIn30d = uniqueComponentSources(sourceRows.map((r) => r.source));
|
||||
weightSourcesIn30d = uniqueComponentSources(
|
||||
sourceRows.map((r) => r.source),
|
||||
);
|
||||
latestWeightAsOf = sourceRows.at(-1)?.measuredAt.toISOString() ?? null;
|
||||
} else {
|
||||
// Live fallback — raw rows over the 37-day window. Mirrors the
|
||||
@@ -288,13 +292,13 @@ export async function computeUserHealthScoreFastPath(
|
||||
.filter((r) => r.measuredAt >= prevSince30d && r.measuredAt <= prevUntil)
|
||||
.map((r) => ({ date: r.measuredAt.toISOString(), kg: r.value }));
|
||||
weightSourcesIn30d = uniqueComponentSources(
|
||||
weightRows
|
||||
.filter((r) => r.measuredAt >= since30d)
|
||||
.map((r) => r.source),
|
||||
weightRows.filter((r) => r.measuredAt >= since30d).map((r) => r.source),
|
||||
);
|
||||
latestWeightAsOf =
|
||||
weightRows.filter((r) => r.measuredAt >= since30d).at(-1)?.measuredAt
|
||||
.toISOString() ?? null;
|
||||
weightRows
|
||||
.filter((r) => r.measuredAt >= since30d)
|
||||
.at(-1)
|
||||
?.measuredAt.toISOString() ?? null;
|
||||
}
|
||||
|
||||
// Remaining reads — mood, BP-SYS source attribution, medications +
|
||||
@@ -327,9 +331,7 @@ export async function computeUserHealthScoreFastPath(
|
||||
select: { score: true, moodLoggedAt: true },
|
||||
orderBy: { moodLoggedAt: "asc" },
|
||||
})
|
||||
: Promise.resolve(
|
||||
[] as Array<{ score: number; moodLoggedAt: Date }>,
|
||||
),
|
||||
: Promise.resolve([] as Array<{ score: number; moodLoggedAt: Date }>),
|
||||
prisma.medication.findMany({
|
||||
// v1.16.11 — as-needed (PRN) medications carry no expected doses
|
||||
// and are excluded from every compliance rate: they must
|
||||
|
||||
@@ -78,8 +78,7 @@ export function buildHealthScoreBpInputs(
|
||||
// 90-day window is itself sparse so a deep-history account survives a quiet
|
||||
// quarter, but never grade off a sub-floor 90-day sample.
|
||||
const last90Sufficient =
|
||||
current.last90Days !== null &&
|
||||
isWindowSufficient(current.last90Days.pairs);
|
||||
current.last90Days !== null && isWindowSufficient(current.last90Days.pairs);
|
||||
const bpInTargetPct = last90Sufficient
|
||||
? (current.last90Days?.pct ?? null)
|
||||
: (current.allTime?.pct ?? null);
|
||||
|
||||
@@ -267,8 +267,7 @@ function bareWeightTrendScore(
|
||||
// 25-point span keeps the unfavourable floor honest (a steep sustained
|
||||
// gain reads "concerning", not "failing") while the favourable ceiling
|
||||
// reaches the full 100.
|
||||
const score =
|
||||
slope < 0 ? 75 + 25 * normalised : 75 - 50 * normalised;
|
||||
const score = slope < 0 ? 75 + 25 * normalised : 75 - 50 * normalised;
|
||||
return Math.round(Math.max(0, Math.min(100, score)));
|
||||
}
|
||||
|
||||
@@ -502,8 +501,7 @@ function redistribute(
|
||||
const source = resolveSourceLabel(value !== null, attr.sources);
|
||||
// When a component is `none`, fall back to the window-end anchor so
|
||||
// the UI can still render "as of <today>" without inventing a date.
|
||||
const asOf =
|
||||
source === "none" ? windowEndAt : (attr.asOf ?? windowEndAt);
|
||||
const asOf = source === "none" ? windowEndAt : (attr.asOf ?? windowEndAt);
|
||||
return {
|
||||
value,
|
||||
weight: weightFor(key),
|
||||
|
||||
@@ -67,7 +67,9 @@ export interface MergedDashboardAnalytics {
|
||||
glucoseByContext: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
function hasContent(record: Record<string, unknown> | null | undefined): boolean {
|
||||
function hasContent(
|
||||
record: Record<string, unknown> | null | undefined,
|
||||
): boolean {
|
||||
return record != null && Object.keys(record).length > 0;
|
||||
}
|
||||
|
||||
@@ -91,8 +93,8 @@ export function mergeSlimAndThickAnalytics(
|
||||
const summaries: Record<string, DataSummary> = slimSummariesHaveContent
|
||||
? (slim!.summaries as Record<string, DataSummary>)
|
||||
: ((thick?.summaries as Record<string, DataSummary> | null | undefined) ??
|
||||
(slim?.summaries as Record<string, DataSummary> | null | undefined) ??
|
||||
{});
|
||||
(slim?.summaries as Record<string, DataSummary> | null | undefined) ??
|
||||
{});
|
||||
|
||||
const slimFreshnessHasContent = hasContent(slim?.lastSeenByType ?? null);
|
||||
const lastSeenByType:
|
||||
|
||||
@@ -121,7 +121,11 @@ export async function computeRangeDelta(
|
||||
const windowDays = rangeWindowDays(range);
|
||||
// Read the full 2N span in one go so the current and previous halves share
|
||||
// a granularity; the router picks the coarsest tier that resolves 2N.
|
||||
const resolved = await readBestGranularityRollups(userId, type, windowDays * 2);
|
||||
const resolved = await readBestGranularityRollups(
|
||||
userId,
|
||||
type,
|
||||
windowDays * 2,
|
||||
);
|
||||
if (!resolved) {
|
||||
const empty: WindowAggregate = {
|
||||
count: 0,
|
||||
@@ -140,7 +144,11 @@ export async function computeRangeDelta(
|
||||
deltaPct: null,
|
||||
};
|
||||
}
|
||||
const { current, previous } = sliceWindowDelta(resolved.rows, windowDays, now);
|
||||
const { current, previous } = sliceWindowDelta(
|
||||
resolved.rows,
|
||||
windowDays,
|
||||
now,
|
||||
);
|
||||
const { delta, deltaPct } = composeDelta(current, previous);
|
||||
return {
|
||||
range,
|
||||
|
||||
@@ -130,10 +130,7 @@ export function resolveRestingPulseSeries(input: {
|
||||
);
|
||||
return { series, which: "resting" };
|
||||
}
|
||||
const proxy = deriveRestingProxyFromPulse(
|
||||
input.pulseSamples,
|
||||
input.dayKeyOf,
|
||||
);
|
||||
const proxy = deriveRestingProxyFromPulse(input.pulseSamples, input.dayKeyOf);
|
||||
if (proxy.length > 0) return { series: proxy, which: "proxy" };
|
||||
return { series: [], which: "none" };
|
||||
}
|
||||
@@ -137,7 +137,9 @@ export function pickCanonicalSourceRows<T extends SourcePickerRow>(
|
||||
// rather than once per row. Resolution itself lives in
|
||||
// `getDeviceTypeLadder` — the cache is the only extra layer here.
|
||||
const ladderCache = new Map<string, readonly DeviceType[]>();
|
||||
function resolveLadder(rowType: MeasurementType | null | undefined): readonly DeviceType[] {
|
||||
function resolveLadder(
|
||||
rowType: MeasurementType | null | undefined,
|
||||
): readonly DeviceType[] {
|
||||
const cacheKey = rowType ?? "__default__";
|
||||
let ladder = ladderCache.get(cacheKey);
|
||||
if (!ladder) {
|
||||
|
||||
@@ -362,7 +362,11 @@ async function computeFromRollups(userId: string): Promise<SummariesSlice> {
|
||||
// the unqualified rollup + inner-subquery columns, `m."…"` for the latest
|
||||
// read's outer alias.
|
||||
const priorityJson = await loadUserSourcePriority(userId);
|
||||
const rankUnqualified = buildSourceRankCase(priorityJson, '"type"', '"source"');
|
||||
const rankUnqualified = buildSourceRankCase(
|
||||
priorityJson,
|
||||
'"type"',
|
||||
'"source"',
|
||||
);
|
||||
const rankM = buildSourceRankCase(priorityJson, 'm."type"', 'm."source"');
|
||||
|
||||
const [narrows, latests, dayBuckets] = await Promise.all([
|
||||
@@ -628,7 +632,11 @@ async function computeFromLiveAggregate(
|
||||
// parity). `canonicalMeasurementsFrom` swaps `FROM measurements m` for a
|
||||
// canonical-source-filtered subquery; the latest read uses the ladder rank.
|
||||
const priorityJson = await loadUserSourcePriority(userId);
|
||||
const rankUnqualified = buildSourceRankCase(priorityJson, '"type"', '"source"');
|
||||
const rankUnqualified = buildSourceRankCase(
|
||||
priorityJson,
|
||||
'"type"',
|
||||
'"source"',
|
||||
);
|
||||
const rankM = buildSourceRankCase(priorityJson, 'm."type"', 'm."source"');
|
||||
|
||||
const [allTime, windowed, latests] = await Promise.all([
|
||||
|
||||
Reference in new issue
Block a user