feat(insights): widen per-card window from 30 days to 360+24 buckets

Every per-card KI insight generator was clipping each metric to the
last 30 daily means before sending the prompt. The model therefore
saw a one-month window per metric — far too narrow to detect drift
that plays out over a year, and the cause of repeated complaints that
a "Stable" trend assessment ignored an obvious 9-month decline.

New shared helper `applyPayloadBudget()` builds a `{ daily, monthly }`
view: the most recent 360 days as daily means (`dayOffset 0..359`)
plus 24 thirty-day windows for the older history (`monthOffset
12..35`). Empty buckets are dropped. If the canonical payload would
serialise above 50 KB the helper transparently re-buckets at 180
daily days so the prompt token budget stays bounded; a per-generator
`annotate({ payload_size_bytes })` makes the chosen window
observable in Wide Events.

All seven generators now consume the bucketed view as the metric
`series` field (general-status, blood-pressure-status, pulse-status,
weight-status, bmi-status, mood-status, medication-compliance-
status) — including their cross-metric joins (BP-pair, weight ↔ mean
BP correlation, medication-compliance per day) which are rebuilt on
top of the shared `dayOffset` instead of date strings. Mood and
medication queries widened to ~3 years so the older monthly window
can actually be filled.

One focused vitest per generator captures the prompt that goes to the
provider, JSON-parses the embedded snapshot, and asserts every
metric series carries both `daily` and `monthly` arrays with the
expected shape. The bucketing helper also has its own unit tests
covering offsets, leap-year-safe Berlin-day boundaries, and the
50 KB → 180-day fallback.

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-09 01:50:10 +02:00
1 parent c3ca8614a6
commit 1adda80944
17 files changed
+1379 -481

No files matched your search

+3 -4
View File
@@ -53,12 +53,11 @@ describe("POST /api/feedback bug-report toggle gate", () => {
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}),
}) as never,
);
expect(res.status).toBe(503);
const body = (await res.json()) as { error: string };
expect(body.error).toMatch(/disabled/i);
// Must not even attempt to create the row when the toggle is off.
expect(prisma.feedback.create).not.toHaveBeenCalled();
});
@@ -78,7 +77,7 @@ describe("POST /api/feedback bug-report toggle gate", () => {
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}),
}) as never,
);
expect(res.status).toBe(201);
expect(prisma.feedback.create).toHaveBeenCalledTimes(1);
@@ -98,7 +97,7 @@ describe("POST /api/feedback bug-report toggle gate", () => {
category: "BUG",
subject: "test",
description: "Anything at least ten characters long.",
}),
}) as never,
);
expect(res.status).toBe(201);
expect(prisma.feedback.create).toHaveBeenCalledTimes(1);