feat(illness): fold the logged symptom curve into the recovery gap

The recovery-gap engine reasoned only over passive vitals and ignored
the journal's own per-day symptom series. Add the functional-impact
(symptom-burden) return track: read functionalImpact per episode day
(falling back to the strongest linked symptom severity) and treat it as
one more return against the known-constant baseline (healthy = 0).

The symptom track needs none of the MAD-band / contamination machinery
the passive vitals carry — the band is the constant 0 — so it is more
reliable to fold in than any passive vital. It is adverse by
construction, so it contributes to adverseCoverageDays and the headline
gap median, giving the qualifying-days floor a genuinely
illness-relevant contributor where a weight-only episode had none.

Stability is logged-days-only: a sparse journal that logs impact-0 once
and stops withholds the return rather than fabricating an all-clear from
the absence of further logs. gapDriverType names the dominant return
track, with the symptom curve winning ties as the most illness-specific
signal.
This commit is contained in:
Marc-André Bombeck committed 2026-06-21 21:06:06 +02:00
1 parent 02ce177ac4
commit fc76c39aea
4 files changed
+435 -4

No files matched your search

+4 -2
View File
@@ -181,11 +181,12 @@ const illnessVitalReturn = z
type: z.string(),
returnedDay: z.string().nullable(),
gapDays: z.number().nullable(),
adverse: z.boolean(),
})
.meta({
id: "IllnessVitalReturn",
description:
"A vital's physiological return: the first day it re-entered its band AND held, and the signed gap (days) from the felt-better marker (positive = the body lagged the feeling).",
"A physiological / symptom return: the first day the metric re-entered its band AND held, the signed gap (days) from the felt-better marker (positive = the body lagged the feeling), and whether the move was illness-adverse (only adverse returns name the driver). `type` is a MeasurementType or `FUNCTIONAL_IMPACT` for the user-logged symptom-burden track (functionalImpact, baseline = constant 0).",
});
const illnessRedFlag = z
@@ -210,12 +211,13 @@ const illnessCorrelationValue = z
recoveryGapDays: z.number().nullable(),
adverseCoverageDays: z.number().int(),
feltBetterDay: z.string().nullable(),
gapDriverType: z.string().nullable(),
redFlags: z.array(illnessRedFlag),
})
.meta({
id: "IllnessCorrelationValue",
description:
"The retrospective correlation findings for one episode: pre-onset anomaly scan, nadir, per-vital physiological returns, the headline recovery-gap (median of per-vital gaps), and any red flags.",
"The retrospective correlation findings for one episode: pre-onset anomaly scan, nadir, per-vital physiological returns (incl. the user-logged symptom-burden track, `type:FUNCTIONAL_IMPACT`), the headline recovery-gap (median of per-track gaps), the driving metric (`gapDriverType`), and any red flags.",
});
const illnessCorrelationResponse = z