feat(illness): Rest Mode — annotate score/recovery/streaks during an active episode + FHIR/doctor-report

An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.

- rest-mode.ts: one server-authoritative resolver (module-gated, fail-soft)
  for "is the account in Rest Mode + which episodes back it". iOS mirrors the
  resolved context, never recomputes the suppression.
- health-score: /api/analytics attaches a value-free Rest Mode annotation;
  computeHealthScore stays pure (score never adjusted).
- recovery-score: computeRecoveryScore carries the Rest Mode context as of the
  scored day; the persisted blend is unchanged.
- coach-nudge: pause cadence-nudges during an active episode (new
  skippedDuringIllness counter, gate sits before the trigger search).
- achievements: bridgeFrozenStreakGaps freezes (does not break) every
  day-streak across illness days; never invents a streak, a non-illness gap
  still breaks.
- coincident-deviation: reframe a fired flag as illness-explained; vital
  numbers untouched. Surfaced as a neutral line on the insights card.
- coach snapshot: illness history + active illness as load-bearing context so
  the coach knows about Rest Mode (no decrypted note reaches the prompt).
- FHIR: illness episodes map to Condition (generic disease root, label on
  code.text, patient-reported/unconfirmed) + a bounding Encounter.
- doctor-report: module-gated illness section in the aggregator + PDF table
  (retrospective, neutral palette); FHIR Condition section in the bundle.

i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 22:25:50 +02:00
1 parent 8447ad90cc
commit c59cf0b652
37 files changed
+1352 -24

No files matched your search

@@ -18,6 +18,7 @@ vi.mock("@/lib/db", () => ({
medicationIntakeEvent: { findMany: vi.fn() },
moodEntry: { findMany: vi.fn() },
labResult: { findMany: vi.fn() },
illnessEpisode: { findMany: vi.fn() },
user: { findUnique: vi.fn(), update: vi.fn() },
},
}));
@@ -98,6 +99,7 @@ beforeEach(() => {
);
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.labResult.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.illnessEpisode.findMany).mockResolvedValue([] as never);
// First findUnique = aggregator profile select; second = route KVNR select.
vi.mocked(prisma.user.findUnique).mockResolvedValue({
username: "sample",