From 6a76f7463e08681afa16d2c78856c084eeea4214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 15 May 2026 17:35:35 +0200 Subject: [PATCH] feat(coach): extend the snapshot window enum with lastYear Adds a year-in-review window between last90days and allTime so the Coach drawer can ground a long-horizon question (seasonal mood patterns, year-over-year weight trajectory) without falling back to the unbounded `allTime` window. The new enum value flows through the scope schema, the snapshot builder's window-to-day-count mapping, the provenance window-set seeding so the source-chips render the year-in-review chip, and the Coach source-chips Record. Extends the snapshot test with a `lastYear`-specific assertion that pins the scope window and the provenance envelope. --- .../insights/coach-panel/source-chips.tsx | 4 ++++ src/lib/ai/coach/__tests__/snapshot.test.ts | 18 ++++++++++++++++++ src/lib/ai/coach/snapshot.ts | 15 +++++++++++++++ src/lib/ai/coach/types.ts | 9 ++++++++- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/components/insights/coach-panel/source-chips.tsx b/src/components/insights/coach-panel/source-chips.tsx index 868b72c3e..d3ed935be 100644 --- a/src/components/insights/coach-panel/source-chips.tsx +++ b/src/components/insights/coach-panel/source-chips.tsx @@ -51,6 +51,10 @@ const WINDOW_KEYS: Record = { last7days: "insights.coach.window.last7days", last30days: "insights.coach.window.last30days", last90days: "insights.coach.window.last90days", + // v1.4.27 B7 / BL-P6-4 — year-in-review window. The i18n key + // resolves to a localised label; absent translations fall back + // through the resolver to the English source. + lastYear: "insights.coach.window.lastYear", allTime: "insights.coach.window.allTime", }; diff --git a/src/lib/ai/coach/__tests__/snapshot.test.ts b/src/lib/ai/coach/__tests__/snapshot.test.ts index f940c7721..df931eee8 100644 --- a/src/lib/ai/coach/__tests__/snapshot.test.ts +++ b/src/lib/ai/coach/__tests__/snapshot.test.ts @@ -132,6 +132,24 @@ describe("buildCoachSnapshot", () => { expect(parsed.pulse.timeline.recent.length).toBe(1); }); + it("respects the scope.window — lastYear flags the year-in-review window", async () => { + featuresMock.mockResolvedValue({ + bloodPressure: { avg30Sys: 124, coverage: { count: 50 } }, + }); + prismaMock.measurement.findMany.mockResolvedValue([ + daysAgo(120, 124, "BLOOD_PRESSURE_SYS"), + daysAgo(120, 81, "BLOOD_PRESSURE_DIA"), + ]); + + const out = await buildCoachSnapshot("user-1", { + sources: ["bp"], + window: "lastYear", + }); + const parsed = JSON.parse(out.snapshotJson); + expect(parsed.scope.window).toBe("lastYear"); + expect(out.provenance.windows).toContain("lastYear"); + }); + it("defaults to all-source last30days when no scope is provided", async () => { const out = await buildCoachSnapshot("user-1"); const parsed = JSON.parse(out.snapshotJson); diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index c4169b61b..6c500670f 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -69,6 +69,13 @@ function windowToDays(window: CoachScopeWindow): number { return 30; case "last90days": return 90; + case "lastYear": + // v1.4.27 B7 / BL-P6-4 — year-in-review window. Same 365-day + // ceiling as the historical `allTime` cap so the prompt budget + // stays bounded, but with explicit semantics — the Coach knows + // the user is asking about a long-horizon view rather than the + // unbounded "all time" backfill. + return 365; case "allTime": // Cap "allTime" at one year for the timeline. The aggregate // section already cites multi-year ranges via the features @@ -331,6 +338,14 @@ export async function buildCoachSnapshot( // budget for free-tier accounts. const snapshot: Record = {}; const windows = new Set(); + // v1.4.27 B7 / BL-P6-4 — seed the provenance window set with the + // user's resolved scope so the year-in-review window surfaces in the + // provenance envelope even when the per-metric branches below only + // emit `last30days` / `last90days` chips. Older windows are added + // by the metric branches as before. + if (window === "lastYear" || window === "allTime") { + windows.add(window); + } const metrics = new Set(); const counts: NonNullable = {}; diff --git a/src/lib/ai/coach/types.ts b/src/lib/ai/coach/types.ts index a18f128e0..7752b3528 100644 --- a/src/lib/ai/coach/types.ts +++ b/src/lib/ai/coach/types.ts @@ -41,6 +41,11 @@ export const coachScopeWindowSchema = z.enum([ "last7days", "last30days", "last90days", + // v1.4.27 B7 / BL-P6-4 — long-horizon window for the year-in-review + // surfaces. Sits between `last90days` and `allTime` so the snapshot + // builder can sample a denser timeline (one row per week) than the + // unbounded fallback while still surfacing seasonal patterns. + "lastYear", "allTime", ]); @@ -145,7 +150,9 @@ export interface CoachProvenance { * Time windows the assistant drew on. Same enum the strict insight * schema uses elsewhere so the UI can pin a mini-chart to the chip. */ - windows: ReadonlyArray<"last7days" | "last30days" | "last90days" | "allTime">; + windows: ReadonlyArray< + "last7days" | "last30days" | "last90days" | "lastYear" | "allTime" + >; /** * Metric topics referenced. Stable contract keys — translated by the * UI, never by the server.