From bb72cbe0923038c094e656c99f61f5210da38596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 1 Jul 2026 18:51:36 +0200 Subject: [PATCH] fix(insights): stop the daily briefing vanishing on grounded vitals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daily-briefing number-grounding gate strips the whole dailyBriefing block to null when it finds a restated figure it cannot trace to a pre-computed server value. Its allow-set covered signalsOfDay plus the glucose / labs / preventive-care / workout / grip / waist / pain aggregates, but omitted the core vitals blocks — weight, blood pressure, pulse, body fat, mood, sleep, activity, rate-pressure product, seasonal variation, historical comparison — even though the briefing prompt is fed all of them. A verdict-first briefing that restated a blood-pressure average or a resting pulse (figures every BP-tracking user has) therefore tripped the gate on every run: one ungrounded number strips the entire briefing while the rest of the insight refreshes, so the surface reads as "every other text updated, only the briefing is gone". Admit every figure the server pre-computes for those blocks into the allow-set. The anti-fabrication guarantee is unchanged — only server-computed numbers are ever added, so a genuinely invented value is still flagged and stripped. --- .../ai/__tests__/briefing-grounding.test.ts | 66 ++++++++++++ src/lib/ai/briefing-grounding.ts | 101 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/lib/ai/__tests__/briefing-grounding.test.ts b/src/lib/ai/__tests__/briefing-grounding.test.ts index 39156af55..50e2e5f0e 100644 --- a/src/lib/ai/__tests__/briefing-grounding.test.ts +++ b/src/lib/ai/__tests__/briefing-grounding.test.ts @@ -196,3 +196,69 @@ describe("W8 aggregate-block grounding extension", () => { expect(found.map((f) => f.value)).toContain(250); }); }); + +// v1.25.13 — the allow-set previously omitted the CORE vitals blocks (weight, +// blood pressure, pulse, mood, sleep, …) even though the briefing prompt is fed +// all of them. A verdict-first briefing that restated a blood-pressure average +// or a resting pulse — figures every BP-tracking user has — tripped the gate and +// got the WHOLE dailyBriefing stripped to null while the rest of the insight +// refreshed. These lock in that those figures are now grounded. +describe("core-vitals grounding extension", () => { + const signals = [signal({})]; + + it("flags blood-pressure + pulse figures when only signals are passed", () => { + const found = findUngroundedBriefingNumbers( + { + paragraph: + "Your blood pressure has averaged 128/82 with a resting pulse near 68.", + }, + signals, + ); + // Without the features block these are ungrounded (the pre-fix strip). + // (82 is omitted from the assertion — it collides with the default weight + // signal's `latest: 82`, so it grounds coincidentally; 128 + 68 do not.) + expect(found.map((f) => f.value)).toEqual( + expect.arrayContaining([128, 68]), + ); + }); + + it("admits blood-pressure + pulse averages once the features block is passed", () => { + const found = findUngroundedBriefingNumbers( + { + paragraph: + "Your blood pressure has averaged 128/82 with a resting pulse near 68.", + }, + signals, + { + bloodPressure: { avgSys30: 128, avgDia30: 82 }, + pulse: { avg30: 68 }, + } as never, + ); + expect(found).toEqual([]); + }); + + it("admits weight, mood and sleep means from the features block", () => { + const found = findUngroundedBriefingNumbers( + { + paragraph: + "Weight sits at 84.5 kg, your mood averaged 3.8 and you slept 7.2 hours.", + }, + signals, + { + weight: { latest: 84.5, bmi: 26.1 }, + mood: { avg30: 3.8 }, + sleep: { avg7: 7.2 }, + } as never, + ); + expect(found).toEqual([]); + }); + + it("still flags a genuinely fabricated vital absent from every block", () => { + const found = findUngroundedBriefingNumbers( + { paragraph: "Your systolic spiked to 195 overnight." }, + signals, + { bloodPressure: { avgSys30: 128, avgDia30: 82 } } as never, + ); + expect(found.map((f) => f.value)).toContain(195); + }); +}); diff --git a/src/lib/ai/briefing-grounding.ts b/src/lib/ai/briefing-grounding.ts index aa020deef..99c2419d1 100644 --- a/src/lib/ai/briefing-grounding.ts +++ b/src/lib/ai/briefing-grounding.ts @@ -176,6 +176,107 @@ function authoritativeValues( push(pain.avg30); push(pain.slope30); } + + // v1.25.13 — the core vitals blocks (weight, blood pressure, pulse, body + // fat, mood, sleep, activity, RPP, seasonal, historical) are ALL fed to the + // briefing prompt, but the allow-set only admitted signalsOfDay + the W8/ + // clinical aggregates — so a verdict-first briefing that restated a blood- + // pressure average or a resting pulse (values every BP-tracking user has) + // tripped the gate and got the WHOLE dailyBriefing stripped to null while + // the rest of the insight refreshed. Admit every figure the server actually + // pre-computed for these blocks; the anti-fabrication guarantee is intact + // (only server-computed numbers are ever added to the set). + const weight = features.weight; + if (weight) { + push(weight.latest); + push(weight.avg7); + push(weight.avg30); + push(weight.avg90); + push(weight.allTimeAvg); + push(weight.allTimeMin); + push(weight.allTimeMax); + push(weight.slope30); + push(weight.bmi); + } + const bp = features.bloodPressure; + if (bp) { + push(bp.avgSys30); + push(bp.avgDia30); + push(bp.avgSys90); + push(bp.avgDia90); + push(bp.allTimeAvgSys); + push(bp.allTimeAvgDia); + push(bp.allTimeMinSys); + push(bp.allTimeMaxSys); + push(bp.allTimeMinDia); + push(bp.allTimeMaxDia); + push(bp.slopeSys30); + push(bp.slopeDia30); + push(bp.sdSys30); + push(bp.sdDia30); + push(bp.pulsePressure30); + push(bp.pctInTarget); + } + const pulse = features.pulse; + if (pulse) { + push(pulse.avg7); + push(pulse.avg30); + push(pulse.avg90); + push(pulse.allTimeAvg); + push(pulse.allTimeMin); + push(pulse.allTimeMax); + push(pulse.slope30); + } + const bodyFat = features.bodyFat; + if (bodyFat) { + push(bodyFat.latest); + push(bodyFat.avg30); + push(bodyFat.slope30); + } + const mood = features.mood; + if (mood) { + push(mood.avg7); + push(mood.avg30); + push(mood.latest); + } + const sleep = features.sleep; + if (sleep) { + push(sleep.avg7); + push(sleep.avg30); + push(sleep.latest); + } + const activity = features.activity; + if (activity) { + push(activity.avg7); + push(activity.avg30); + push(activity.latest); + } + const rpp = features.ratePressureProduct; + if (rpp) { + push(rpp.rpp7); + push(rpp.rpp30); + } + const seasonal = features.seasonalVariation; + if (seasonal) { + push(seasonal.winterAvgSys); + push(seasonal.summerAvgSys); + push(seasonal.delta); + } + const hist = features.historicalComparison; + if (hist) { + for (const block of [ + hist.weight, + hist.systolic, + hist.diastolic, + hist.pulse, + ]) { + if (block) { + push(block.current7dAvg); + push(block.previous30dAvg); + push(block.change); + } + } + } } return values;