perf(health-score-fast-path): consume monthly rollup buckets for slope90

Wires readBestGranularityRollups into the WEIGHT-covered branch of
computeUserHealthScoreFastPath so the trailing-year weight baseline is
served from the WEEK / MONTH / YEAR rollup tier instead of falling
through to a live measurements walk. The 365-day window routes through
MONTH by default (~12 buckets) and falls back to WEEK -> DAY on per-
tier coverage miss. count / mean compose linearly across MONTH buckets
so the derived mean is mathematically equivalent to the per-row
average over the same window.

The read fires in parallel with the canonical DAY-bucket read for the
30-day weight series so wall-clock cost stays flat. Score shape is
unchanged; the weightLongWindow annotate carries the resolved
granularity + bucket count + mean so operators can verify the rollup
tier serves production traffic. The live-fallback branch deliberately
omits the long-window probe because the raw weight findMany already
does the work — guards against double-reading on cold mounts.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 12:05:02 +02:00
1 parent 9df23c3c87
commit 9009f1bb33
2 files changed
+231 -7

No files matched your search

@@ -102,6 +102,11 @@ describe("computeUserHealthScoreFastPath", () => {
computedAt: now,
},
]);
// v1.4.40 W-WMY-WIRE — the parallel long-window read fires
// alongside the DAY-bucket read; mock its `findMany` to return
// a coverage miss so the annotate carries `weightLongWindow:
// null` and the score shape is unchanged.
ROLLUP_FIND_MANY.mockResolvedValueOnce([]);
// Weight source-attribution read (narrow 2-column).
MEASUREMENT_FIND_MANY
.mockResolvedValueOnce([
@@ -147,6 +152,170 @@ describe("computeUserHealthScoreFastPath", () => {
});
});
/**
* v1.4.40 W-WMY-WIRE — pin the long-window weight wiring through
* `readBestGranularityRollups`. The score shape is unchanged; the
* weightLongWindow annotate makes the rollup-tier consumption
* verifiable in production wide-events.
*/
describe("long-window weight wiring (W-WMY-WIRE)", () => {
it("surfaces a MONTH-granularity weightLongWindow mean on the rollup branch", async () => {
const coverage = new Map<string, boolean>([["WEIGHT", true]]);
FULLY_COVERED.mockReturnValue(true);
PROBE.mockResolvedValue(coverage);
const now = new Date("2026-05-17T12:00:00.000Z");
// DAY-bucket read (trailing 37 days) — single bucket, irrelevant
// for the long-window assertion.
ROLLUP_FIND_MANY.mockResolvedValueOnce([
{
bucketStart: new Date("2026-05-10T00:00:00.000Z"),
count: 1,
mean: 82.5,
minValue: 82.5,
maxValue: 82.5,
sd: null,
slope: null,
r2: null,
computedAt: now,
},
]);
// readBestGranularityRollups(WEIGHT, 365) — first call is the
// MONTH probe (floor 181 d). Two MONTH buckets, count-weighted
// mean = (12*82 + 18*80) / 30 = 80.8.
ROLLUP_FIND_MANY.mockResolvedValueOnce([
{
bucketStart: new Date("2025-08-01T00:00:00.000Z"),
count: 12,
mean: 82,
minValue: 80,
maxValue: 84,
sd: 1,
slope: 0,
r2: 0,
sumValue: null,
},
{
bucketStart: new Date("2025-09-01T00:00:00.000Z"),
count: 18,
mean: 80,
minValue: 78,
maxValue: 83,
sd: 1,
slope: 0,
r2: 0,
sumValue: null,
},
]);
// Source attribution + BP-SYS attribution reads.
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
{
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
source: "WITHINGS",
},
]).mockResolvedValueOnce([]);
await computeUserHealthScoreFastPath({
userId: "user-longwindow",
bpInTargetPct: 85,
heightCm: 178,
now,
coverage,
});
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const pathCall = calls.find(
(c) => c?.meta?.healthScore?.path === "rollup",
);
expect(pathCall).toBeDefined();
const longWindow = pathCall?.meta?.healthScore?.weightLongWindow;
expect(longWindow).not.toBeNull();
expect(longWindow?.granularity).toBe("MONTH");
expect(longWindow?.buckets).toBe(2);
// Σ(count*mean) / Σcount = (12*82 + 18*80) / 30 = 80.8 → rounded.
expect(longWindow?.mean).toBeCloseTo(80.8, 5);
});
it("emits weightLongWindow:null when the WMY tier carries no buckets", async () => {
const coverage = new Map<string, boolean>([["WEIGHT", true]]);
FULLY_COVERED.mockReturnValue(true);
PROBE.mockResolvedValue(coverage);
const now = new Date("2026-05-17T12:00:00.000Z");
// DAY-bucket read.
ROLLUP_FIND_MANY.mockResolvedValueOnce([
{
bucketStart: new Date("2026-05-10T00:00:00.000Z"),
count: 1,
mean: 82.5,
minValue: 82.5,
maxValue: 82.5,
sd: null,
slope: null,
r2: null,
computedAt: now,
},
]);
// Long-window read — every granularity returns empty (router
// walks MONTH → WEEK → DAY; 365 d skips YEAR floor).
ROLLUP_FIND_MANY.mockResolvedValue([]);
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
{
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
source: "WITHINGS",
},
]).mockResolvedValueOnce([]);
await computeUserHealthScoreFastPath({
userId: "user-no-longwindow",
bpInTargetPct: 85,
heightCm: 178,
now,
coverage,
});
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const pathCall = calls.find(
(c) => c?.meta?.healthScore?.path === "rollup",
);
expect(pathCall?.meta?.healthScore?.weightLongWindow).toBeNull();
});
it("omits the long-window read on the live fallback branch", async () => {
const coverage = new Map<string, boolean>([["WEIGHT", false]]);
FULLY_COVERED.mockReturnValue(false);
PROBE.mockResolvedValue(coverage);
const now = new Date("2026-05-17T12:00:00.000Z");
// Weight live read + BP source attribution.
MEASUREMENT_FIND_MANY.mockResolvedValueOnce([
{
measuredAt: new Date("2026-05-10T08:00:00.000Z"),
value: 82.5,
source: "MANUAL",
},
]).mockResolvedValueOnce([]);
await computeUserHealthScoreFastPath({
userId: "user-live-nowmy",
bpInTargetPct: 80,
heightCm: 178,
now,
coverage,
});
// The live branch never issues the long-window probe — guards
// against accidentally double-reading on cold mounts where the
// raw weight findMany is already doing the work.
expect(ROLLUP_FIND_MANY).not.toHaveBeenCalled();
const calls = ANNOTATE.mock.calls.map((c) => c[0]);
const pathCall = calls.find(
(c) => c?.meta?.healthScore?.path === "live",
);
expect(pathCall?.meta?.healthScore?.weightLongWindow).toBeNull();
});
});
describe("live fallback — WEIGHT not covered", () => {
it("reads raw weight rows and pins path:live", async () => {
const coverage = new Map<string, boolean>([
+62 -7
View File
@@ -45,6 +45,7 @@ import {
probeRollupCoverage,
type RollupCoverageMap,
} from "@/lib/measurements/rollup-coverage";
import { readBestGranularityRollups } from "@/lib/measurements/rollup-read-wmy";
import { calculateCompliance } from "./compliance";
import {
computeHealthScore,
@@ -159,19 +160,38 @@ export async function computeUserHealthScoreFastPath(
let weightSeriesPrev30d: Array<{ date: string; kg: number }>;
let weightSourcesIn30d: ReadonlyArray<ContributingSource>;
let latestWeightAsOf: string | null;
// v1.4.40 W-WMY-WIRE — long-window weight baseline served from the
// WEEK / MONTH / YEAR rollup tier. Used for the diagnostic annotate
// so operators can verify the WMY readers actually serve production
// traffic instead of sitting as dead write amplification (see
// `.planning/round-v1438-perf-analysis.md` §2 + §5 P6). The score
// shape stays unchanged; the long-window mean is annotate-only so
// the v1.5 Coach drawer can opt into it later without breaking the
// current contract.
let weightLongWindowGranularity: string | null = null;
let weightLongWindowMean: number | null = null;
let weightLongWindowBucketCount = 0;
if (weightCovered) {
// Rollup-fast-path — per-day MEAN weight from `measurement_rollups`.
// Linear regression on per-day means produces a slope equivalent
// to per-event regression for a series with consistent sampling
// cadence (one weigh-in per day is the canonical pattern).
const dayBuckets = await readRollupBuckets(
userId,
"WEIGHT",
"DAY",
prevSince30d,
now,
);
//
// v1.4.40 W-WMY-WIRE — the long-window weight read runs in
// parallel with the canonical DAY-bucket read so the wall-clock
// cost stays flat. `readBestGranularityRollups(..., 365)` routes
// the trailing-year window through MONTH (floor 181 d) by default
// — typically 12 monthly buckets vs the ~365 DAY rows the live
// path would scan. count / mean are linearly composable across
// MONTH buckets so the derived mean is mathematically equivalent
// to the per-row average over the same year-long window.
// Coverage-fallback in the helper means a tenant with WEEK or DAY
// coverage but no MONTH buckets still resolves to a usable mean.
const [dayBuckets, longWindow] = await Promise.all([
readRollupBuckets(userId, "WEIGHT", "DAY", prevSince30d, now),
readBestGranularityRollups(userId, "WEIGHT", 365),
]);
weightSeriesLast30d = dayBuckets
.filter((b) => b.bucketStart >= since30d)
.map((b) => ({
@@ -184,6 +204,19 @@ export async function computeUserHealthScoreFastPath(
date: b.bucketStart.toISOString(),
kg: b.mean,
}));
if (longWindow && longWindow.rows.length > 0) {
weightLongWindowGranularity = longWindow.granularity;
weightLongWindowBucketCount = longWindow.rows.length;
let totalCount = 0;
let weighted = 0;
for (const row of longWindow.rows) {
totalCount += row.count;
weighted += row.count * row.mean;
}
if (totalCount > 0) {
weightLongWindowMean = weighted / totalCount;
}
}
// Source attribution still needs the raw rows — only the raw table
// carries the `source` enum. Pull the narrow 30-day window with a
// 2-column projection so the round-trip stays minimal.
@@ -338,6 +371,14 @@ export async function computeUserHealthScoreFastPath(
score: null,
reason: "no_components_available",
path: weightCovered ? "rollup" : "live",
weightLongWindow:
weightLongWindowMean !== null
? {
mean: Math.round(weightLongWindowMean * 100) / 100,
granularity: weightLongWindowGranularity,
buckets: weightLongWindowBucketCount,
}
: null,
},
},
});
@@ -406,6 +447,20 @@ export async function computeUserHealthScoreFastPath(
band: result.band,
delta: result.delta,
path: weightCovered ? "rollup" : "live",
// v1.4.40 W-WMY-WIRE — the long-window weight baseline drawn
// from the WEEK / MONTH / YEAR rollup tier. `null` when the
// user lacks long-tail coverage or when the live fallback
// branch ran (no probe issued). Surfaces the granularity the
// router landed on so operators can see "rollup tier served
// the year-long mean from MONTH buckets" in the wide-event.
weightLongWindow:
weightLongWindowMean !== null
? {
mean: Math.round(weightLongWindowMean * 100) / 100,
granularity: weightLongWindowGranularity,
buckets: weightLongWindowBucketCount,
}
: null,
},
},
});