fix(illness): gate fever red-flag runs on calendar-adjacent days

Symptom and vital points are present-days-only, so a sparse febrile
series (e.g. days 10/15/20/25) was counted as one long run and falsely
fired the sustained_fever escalation. Continue a run only when the next
matching point is the calendar day immediately after the previous one,
using the existing dayDiff helper; otherwise start a fresh run of one.
This commit is contained in:
Marc-André Bombeck committed 2026-06-26 09:21:49 +02:00
1 parent a20e140a2f
commit e4c0af0970
2 files changed
+86 -7

No files matched your search

@@ -669,6 +669,70 @@ describe("computeIllnessCorrelation — red-flag escalation", () => {
expect(fever?.days).toBe(3);
expect(fever?.worstValue).toBe(39.0);
});
it("does NOT escalate a sparse febrile series with calendar GAPS (cries-wolf guard)", () => {
// Symptom/vital points are present-days-only (never zero-filled). A user
// who logs a fever on 01-10/01-15/01-20/01-25 has FOUR isolated febrile
// days with multi-day gaps between them — NOT a sustained run. The run
// scan must compare calendar-day deltas, not array adjacency, or it falsely
// fires sustained_fever (which surfaces a "seek medical care" string AND an
// urgent cross-channel push). No two of these days are calendar-adjacent →
// the longest run is 1 → NO escalation.
const rhr: MeasurementType = "RESTING_HEART_RATE";
const out = computeIllnessCorrelation(
input({
series: [
{
type: rhr,
baselineDays: jitteredBaseline(55, 1, 21, "2026-01-02"),
episodeDays: flatBaseline(55, 5, "2026-01-15"),
},
],
dayLogFever: [
{ day: "2026-01-10", feverC: 39.2 },
{ day: "2026-01-15", feverC: 39.0 },
{ day: "2026-01-20", feverC: 38.8 },
{ day: "2026-01-25", feverC: 38.7 },
],
}),
);
expect(out.status).toBe("ok");
if (out.status !== "ok") return;
const fever = out.value.redFlags.find(
(f) => f.reason === "sustained_fever",
);
expect(fever).toBeUndefined();
});
it("DOES escalate a genuinely consecutive 3-calendar-day febrile run", () => {
// The mirror of the gap guard: three truly adjacent calendar days
// (01-10/01-11/01-12) all ≥ 38.5 → a real sustained fever → escalate.
const rhr: MeasurementType = "RESTING_HEART_RATE";
const out = computeIllnessCorrelation(
input({
series: [
{
type: rhr,
baselineDays: jitteredBaseline(55, 1, 21, "2026-01-02"),
episodeDays: flatBaseline(55, 5, "2026-01-15"),
},
],
dayLogFever: [
{ day: "2026-01-10", feverC: 39.2 },
{ day: "2026-01-11", feverC: 38.9 },
{ day: "2026-01-12", feverC: 38.6 },
],
}),
);
expect(out.status).toBe("ok");
if (out.status !== "ok") return;
const fever = out.value.redFlags.find(
(f) => f.reason === "sustained_fever",
);
expect(fever).toBeDefined();
expect(fever?.days).toBe(3);
expect(fever?.worstValue).toBe(39.2);
});
});
describe("isAdverseDeviation", () => {
+22 -7
View File
@@ -968,22 +968,37 @@ function runFlag(
let bestWorst: number | null = null;
let run = 0;
let runWorst: number | null = null;
let prevDay: string | null = null;
for (const p of sorted) {
if (predicate(p.mean)) {
run++;
runWorst =
runWorst === null
? p.mean
: worst === "min"
? Math.min(runWorst, p.mean)
: Math.max(runWorst, p.mean);
// A run is CALENDAR-consecutive days. Symptom/vital points are
// present-days-only (never zero-filled), so a sparse febrile series
// (e.g. days 01/10/15/20) must NOT count as one long run — that fires a
// false sustained_fever escalation. Continue the run only when this
// point is the calendar day immediately after the previous matching one;
// otherwise the current point starts a fresh run of 1.
const adjacent = prevDay !== null && dayDiff(prevDay, p.day) === 1;
if (adjacent) {
run++;
runWorst =
runWorst === null
? p.mean
: worst === "min"
? Math.min(runWorst, p.mean)
: Math.max(runWorst, p.mean);
} else {
run = 1;
runWorst = p.mean;
}
if (run > bestRun) {
bestRun = run;
bestWorst = runWorst;
}
prevDay = p.day;
} else {
run = 0;
runWorst = null;
prevDay = null;
}
}
if (bestRun >= RED_FLAG_RUN_DAYS && bestWorst !== null) {