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.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 17:35:35 +02:00
1 parent 988bbf1a2f
commit 6a76f7463e
4 files changed
+45 -1

No files matched your search

@@ -51,6 +51,10 @@ const WINDOW_KEYS: Record<CoachProvenance["windows"][number], string> = {
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",
};
@@ -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);
+15
View File
@@ -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<string, unknown> = {};
const windows = new Set<CoachProvenance["windows"][number]>();
// 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<CoachProvenance["metrics"][number]>();
const counts: NonNullable<CoachProvenance["counts"]> = {};
+8 -1
View File
@@ -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.