mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
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:
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>([
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user