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;