feat(illness): correlation + recovery-gap engine

Add the retrospective correlation engine for illness episodes: a pre-onset
anomaly scan (deviation in robust SDs from the user's own median±MAD
baseline), the nadir / what-dropped across the active span, and the
recovery-gap (physiological return-to-baseline vs the felt-better marker).
CHRONIC_ONGOING is excluded from the gap. Every derived number is gated by
the shared coverage/confidence model — a thin signal asserts nothing rather
than showing a wrong figure.

The baseline window ends strictly before the pre-onset lookback, so an
episode's own days can never contaminate the baseline it is measured against.
Reads are DAY-native (rollup tier with a live-SQL fallback), reusing the
baseline engine's read path so the rollup-vs-live parity carries over.

Surfaces:
- GET /api/illness/episodes/{id}/correlation — per-episode findings (gated
  Derived wire shape; red-flag escalation for sustained low SpO2 / fever).
- GET /api/illness/insights — cross-episode retrospective ("sick N times ·
  typical recovery gap X days"), with the typical-gap withheld below the
  min-sample floor. Retrospective only — never a predictor or diagnoser.

Reliability battery: golden-fixture recovery-gap math, rollup-vs-live parity,
baseline-contamination guard, thin-data / coverage-gate, red-flag escalation.
Born-gated + owner-scoped; server-authoritative DTO (iOS renders). i18n ×6.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 22:16:19 +02:00
1 parent a7b2b61b99
commit 745fa28c8b
16 files changed
+1737

No files matched your search

+11
View File
@@ -133,3 +133,14 @@ export type IllnessDayLogInput = z.infer<typeof illnessDayLogInputSchema>;
/** The single-day read query: `GET .../day-logs?date=YYYY-MM-DD`. */
export const illnessDayLogQuerySchema = z.object({ date: dateString });
/* ── P3 retrospective insight window ─────────────────────────────────── */
/**
* The retrospective-insight window query: `GET /api/illness/insights?
* windowDays=365`. Bounded 30..1095 days; defaults to a trailing year.
* Retrospective only — the engine summarises past episodes, never forecasts.
*/
export const illnessInsightsQuerySchema = z.object({
windowDays: z.coerce.number().int().min(30).max(1095).optional(),
});