mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(insights): order the correlations read desc so a hit cap keeps recent rows
The FDR discovery read capped a dense account's 180-day window at orderBy asc + take, so exceeding the cap silently dropped the NEWEST rows. The early-detection pass exists specifically to see the recent window, so a capped read gave it nothing to work with. Order desc for both the measurement and mood reads, re-sort ascending in JS afterward (every downstream day-bucketing pass groups by day key, so pre-sort order doesn't matter to them), and annotate when a read actually hit its cap.
This commit is contained in:
1 parent
a2cb4fb4a9
commit
5169be5887
2 files changed
+51
-5
No files matched your search
@@ -196,6 +196,27 @@ describe("GET /api/insights/correlations", () => {
|
||||
expect(prisma.measurement.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// v1.30 (DATAINT M1) — `asc` + `take` truncated the RECENT end of the
|
||||
// 180-day window on a dense account; the read must order `desc` so the
|
||||
// cap keeps the newest rows (re-sorted `asc` in JS before series-building).
|
||||
it("reads the measurement + mood window ordered desc so the cap keeps the newest rows", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
await callGet(makeReq());
|
||||
|
||||
expect(prisma.measurement.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { measuredAt: "desc" },
|
||||
take: 20000,
|
||||
}),
|
||||
);
|
||||
expect(prisma.moodEntry.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { moodLoggedAt: "desc" },
|
||||
take: 5000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// v1.18.0 (B2) — the route now also requires the `insights` module.
|
||||
it("returns 403 + module.disabled when the insights module is off", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
|
||||
@@ -101,7 +101,17 @@ export const GET = apiHandler(async () => {
|
||||
// `buildMeasurementDailySeries`'s per-type grain resolution below (source
|
||||
// collapse for cumulative types, per-night reconstruction for sleep); the
|
||||
// rest of the channels never look at them.
|
||||
const [measurements, moodEntries, priorityJson] = await Promise.all([
|
||||
// v1.30 (PERFAUDIT M1) — `orderBy asc` + `take` truncates from the START
|
||||
// of the window. A dense account (unconsolidated step chunks alone can
|
||||
// run ~200 rows/day → 36 000 rows over 180 days) can exceed the cap, and
|
||||
// an ascending order then drops the NEWEST rows — exactly backwards for
|
||||
// `discoverEmergingCorrelations`, whose entire job is the recent window.
|
||||
// Order DESC so the cap keeps the most-recent rows, then re-sort ASC in
|
||||
// JS before folding into per-type series (every downstream day-bucketing
|
||||
// pass is order-insensitive, but callers document an ascending contract).
|
||||
const MEASUREMENT_READ_CAP = 20000;
|
||||
const MOOD_READ_CAP = 5000;
|
||||
const [measurementsDesc, moodEntriesDesc, priorityJson] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
@@ -109,8 +119,8 @@ export const GET = apiHandler(async () => {
|
||||
measuredAt: { gte: since },
|
||||
type: { in: [...behaviourTypes, ...outcomeTypes] },
|
||||
},
|
||||
orderBy: { measuredAt: "asc" },
|
||||
take: 20000,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
take: MEASUREMENT_READ_CAP,
|
||||
select: {
|
||||
type: true,
|
||||
value: true,
|
||||
@@ -122,12 +132,20 @@ export const GET = apiHandler(async () => {
|
||||
}),
|
||||
prisma.moodEntry.findMany({
|
||||
where: { userId: user.id, deletedAt: null, moodLoggedAt: { gte: since } },
|
||||
orderBy: { moodLoggedAt: "asc" },
|
||||
take: 5000,
|
||||
orderBy: { moodLoggedAt: "desc" },
|
||||
take: MOOD_READ_CAP,
|
||||
select: { score: true, moodLoggedAt: true },
|
||||
}),
|
||||
loadUserSourcePriority(user.id),
|
||||
]);
|
||||
const measurementsCapped = measurementsDesc.length >= MEASUREMENT_READ_CAP;
|
||||
const moodCapped = moodEntriesDesc.length >= MOOD_READ_CAP;
|
||||
const measurements = [...measurementsDesc].sort(
|
||||
(a, b) => a.measuredAt.getTime() - b.measuredAt.getTime(),
|
||||
);
|
||||
const moodEntries = [...moodEntriesDesc].sort(
|
||||
(a, b) => a.moodLoggedAt.getTime() - b.moodLoggedAt.getTime(),
|
||||
);
|
||||
|
||||
const measurementsByType = new Map<string, MeasurementSeriesRow[]>();
|
||||
for (const m of measurements) {
|
||||
@@ -221,6 +239,13 @@ export const GET = apiHandler(async () => {
|
||||
pairs_tested: result.pairsTested,
|
||||
discovered: result.discovered.length,
|
||||
fdr_q: result.fdrQ,
|
||||
// PERFAUDIT M1 — surfaces when a dense account's window exceeded the
|
||||
// read cap. The cap now falls on the OLDEST rows (desc + take), so a
|
||||
// capped read still covers the recent window `discoverEmergingCorrelations`
|
||||
// needs; this only tells a dashboard the retrospective scan's older
|
||||
// half of the window may be thin.
|
||||
measurements_capped: measurementsCapped,
|
||||
mood_entries_capped: moodCapped,
|
||||
// FDREXTEND — per-channel day-counts so a dashboard can see whether the
|
||||
// two sparse new channels reached the n ≥ 20 floor or degraded to absent.
|
||||
compliance_days: complianceSeries.points.length,
|
||||
|
||||
Reference in new issue
Block a user