feat(coach): adherence storyline, changepoint engine, signal-trust, n-of-1 substrate

Add the medication-class to target-vital map and the adherence-to-vital
storyline so the Coach can surface, association-only, when an adherence dip
lines up with a drift in the vital that medication targets. Never causal,
never a dose-change suggestion; dose questions defer to the prescriber.

Add an in-repo CUSUM / binary-segmentation changepoint detector over DAY
rollups with a high firing bar (persistence plus within-segment-spread
magnitude) so the Coach can name a dated, sustained level shift instead of a
vague "lately", and stays silent when no break clears the bar.

Surface which source recovery is read from when two sources materially
diverge, with the honest reason, instead of silently averaging.

Lay the n-of-1 experiment substrate: a nullable encrypted outcome column on
coach_plans (migration 0197), the review_due / reviewed lifecycle statuses
wired into the encrypted-column registry and rotation script, and a daily
worker that reads the experiment result back in the user's own before/after
numbers — association-only, honest-null, no cheerlead on an adverse trend,
and routing a worsened vital to the clinician. The user-visible read-back
stays behind COACH_EXPERIMENT_VERDICT until its review cases clear.

Extend the outbound dose screen to catch experiment-shaped dose changes
(halve / skip / stop a medication to test it). Add evaluation cases and unit
tests for each new surface.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 15:21:04 +02:00
1 parent 3b74b108a1
commit 7ad32f7ab1
24 files changed
+2067 -4

No files matched your search

+2 -1
View File
@@ -319,11 +319,12 @@ async function main() {
);
// ───── CoachPlan (Bytes columns) ─────
// "ifCueEncrypted" "thenActionEncrypted" "targetEncrypted"
// "ifCueEncrypted" "thenActionEncrypted" "targetEncrypted" "outcomeEncrypted"
for (const field of [
"ifCueEncrypted",
"thenActionEncrypted",
"targetEncrypted",
"outcomeEncrypted",
]) {
results.push(await rotateBytesColumn("CoachPlan", field, prisma.coachPlan));
}