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:
Marc-André Bombeck committed 2026-06-19 22:20:31 +02:00
1 parent b3fea2ef59
commit 1f4ba5939d
1000 files changed
+12130 -8385

No files matched your search

+10 -8
View File
@@ -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),
);
+127 -62
View File
@@ -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", () => {
+1 -4
View File
@@ -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
+6 -1
View File
@@ -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,
+79 -34
View File
@@ -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,
};
}
/**
+8 -1
View File
@@ -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),
]);
-1
View File
@@ -104,4 +104,3 @@ export function pearsonCorrelation(
return { r, strength, n };
}
+4 -5
View File
@@ -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;
+12 -10
View File
@@ -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
+1 -2
View File
@@ -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);
+2 -4
View File
@@ -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),
+5 -3
View File
@@ -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:
+10 -2
View File
@@ -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,
+1 -4
View File
@@ -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" };
}
+3 -1
View File
@@ -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) {
+10 -2
View File
@@ -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([