feat(insights): wire CorrelationRow + TrendsRow into /insights

Compute the three pre-defined correlation hypotheses
(BP × compliance, mood × pulse, weight × weekday) on the trailing
30-day window in `/api/analytics` and surface the typed result
shapes on the response payload. Each runner is gated on n >= 14
+ p < 0.05; below the bar the result.status === "insufficient"
and the per-card EmptyState paints in place of a misleading
near-zero scatter.

The insights page now mounts:

- `<CorrelationRow>` between the Daily Briefing and the Advisor
  card — the layout slot the artboard reserves for the patterns
  surface.
- `<TrendsRow>` immediately below it, fed from
  `advisor.payload.trendAnnotations` (lifted client-side via
  `trendAnnotationsSchema.safeParse`). Legacy cached payloads
  predating PROMPT_VERSION 4.20.1 surface the per-metric
  empty-state hint instead of a half-rendered annotation.

The wide-event annotation captures per-hypothesis status so admin
observability can attribute coverage gaps without re-querying
the DB.

10 source-scan tests pin the load-bearing imports, the JSX mounts,
the analytics-route surfacing, and the malformed-payload null
fallback in the advisor hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 16:53:29 +02:00
1 parent 42591c6cec
commit c83cb2311d
4 files changed
+359

No files matched your search

@@ -4,7 +4,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { InsightResult } from "@/lib/ai/types";
import {
dailyBriefingSchema,
trendAnnotationsSchema,
type DailyBriefing as DailyBriefingPayload,
type TrendAnnotations,
} from "@/lib/ai/schema";
import { queryKeys } from "@/lib/query-keys";
@@ -39,6 +41,12 @@ export interface InsightAdvisorPayload {
* malformed payload from poisoning the briefing card.
*/
dailyBriefing?: DailyBriefingPayload | null;
/**
* v1.4.20 phase B3 — optional trend annotations for the Trends row.
* Same lift-pattern as `dailyBriefing` — validated client-side and
* left null when the cached payload predates PROMPT_VERSION 4.20.1.
*/
trendAnnotations?: TrendAnnotations | null;
}
async function fetchAdvisor(
@@ -77,6 +85,19 @@ async function fetchAdvisor(
} else {
payload.dailyBriefing = null;
}
// v1.4.20 phase B3 — same lift for `trendAnnotations`. Cached payloads
// from the 4.20.0 line predate the field, so null is the expected
// default. A malformed candidate also resolves to null so the UI
// surfaces the per-metric empty hint instead of a half-rendered card.
const annotationsCandidate = (payload?.insights as Record<string, unknown>)
?.trendAnnotations;
if (annotationsCandidate != null) {
const parsed = trendAnnotationsSchema.safeParse(annotationsCandidate);
payload.trendAnnotations = parsed.success ? parsed.data : null;
} else {
payload.trendAnnotations = null;
}
return payload;
}