feat(insights): surface early-detection and lab correlations to the correlations route and coach

Thread the rolling early-detection pass and the labs -> outcome pass through the
correlations route and the coach correlations reader, both of which already
build the same daily series. The route response gains an optional `emerging`
block (recent-window pairs not yet established, flagged provisional) and a
`labCorrelations` block; the coach reader gains matching `emerging` and
`labDrivers` fields. Regenerate the OpenAPI contract for the new response shape.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 13:34:43 +02:00
1 parent 2f22d66ef7
commit 0aaaaac081
6 files changed
+339 -8

No files matched your search

@@ -18,6 +18,8 @@ vi.mock("@/lib/db", () => ({
medicationIntakeEvent: { findMany: vi.fn().mockResolvedValue([]) },
illnessEpisode: { findMany: vi.fn().mockResolvedValue([]) },
illnessDayLog: { findMany: vi.fn().mockResolvedValue([]) },
// v1.22 — labs ↔ outcome pass fetches lab draws; default empty.
labResult: { findMany: vi.fn().mockResolvedValue([]) },
},
}));
@@ -109,6 +111,8 @@ beforeEach(() => {
(prisma.illnessDayLog.findMany as ReturnType<typeof vi.fn>).mockResolvedValue(
[],
);
// v1.22 — labs ↔ outcome pass reads lab draws; default empty.
(prisma.labResult.findMany as ReturnType<typeof vi.fn>).mockResolvedValue([]);
});
const callGet = GET as unknown as (req: NextRequest) => Promise<Response>;
+31 -3
View File
@@ -26,9 +26,12 @@ import { wallClockInTz } from "@/lib/tz/wall-clock";
import type { MeasurementType } from "@/generated/prisma/client";
import {
discoverCorrelations,
discoverEmergingCorrelations,
discoverLabOutcomeCorrelations,
discoveryMeasurementTypes,
DISCOVERY_BEHAVIOURS,
DISCOVERY_OUTCOMES,
EARLY_WINDOW_DAYS,
MEDICATION_COMPLIANCE_CHANNEL_KEY,
SYMPTOM_SEVERITY_CHANNEL_KEY,
type DailySeriesPoint,
@@ -36,6 +39,7 @@ import {
} from "@/lib/insights/correlation-discovery";
import {
fetchComplianceSeries,
fetchLabDraws,
fetchSymptomSeries,
} from "@/lib/insights/correlation-channel-series";
@@ -147,8 +151,12 @@ export const GET = apiHandler(async () => {
// v1.21.0 (FDREXTEND) — build the two non-measurement, non-mood channels from
// their own sources. Each degrades to an empty series when the user has no
// data, so the discovery loop drops the channel (it cannot clear n ≥ 20).
const complianceSeries = await fetchComplianceSeries(user.id, tz, since);
const symptomSeries = await fetchSymptomSeries(user.id, tz, since);
// v1.22 — lab draws (for the labs ↔ outcome pass) fetch alongside.
const [complianceSeries, symptomSeries, labDraws] = await Promise.all([
fetchComplianceSeries(user.id, tz, since),
fetchSymptomSeries(user.id, tz, since),
fetchLabDraws(user.id, tz, since),
]);
const points = (key: string): DailySeriesPoint[] =>
key === "MOOD"
@@ -175,6 +183,21 @@ export const GET = apiHandler(async () => {
const result = discoverCorrelations(series);
// v1.22 — rolling early-detection pass over the trailing window, re-using
// the already-built series (no extra DB read). Emerging pairs exclude anything
// the retrospective scan already established (no double-count).
const recentFromDayKey = tzDayKey(
new Date(now.getTime() - EARLY_WINDOW_DAYS * MS_PER_DAY),
tz,
);
const emerging = discoverEmergingCorrelations(series, result, {
recentFromDayKey,
});
// v1.22 — labs ↔ outcome pass (point-vs-window over sparse draws). Degrades
// to absent when the user has too few draws to clear the per-pair floor.
const labCorrelations = discoverLabOutcomeCorrelations(labDraws, series);
annotate({
action: { name: "insights.correlations.discover" },
meta: {
@@ -185,8 +208,13 @@ export const GET = apiHandler(async () => {
// two sparse new channels reached the n ≥ 20 floor or degraded to absent.
compliance_days: complianceSeries.points.length,
symptom_days: symptomSeries.points.length,
// v1.22 — early-detection + labs reach.
emerging: emerging.emerging.length,
emerging_window_days: emerging.windowDays,
lab_draws: labDraws.length,
lab_correlations: labCorrelations.discovered.length,
},
});
return apiSuccess(result);
return apiSuccess({ ...result, emerging, labCorrelations });
});