Symptom
Per-medication compliance percentage reported on the medication card, on the Health Score on the dashboard, in the AI Coach prompt features, and on every insight that reads compliance7.rate / compliance30.rate is wrong for any medication whose schedule is not "every day". The bug compounds with multi-dose-per-day schedules that are also restricted by daysOfWeek (e.g. metformin weekdays-only).
Marc reported it as "I feel the compliance level is wrongly calculated, especially for medications taken not once a day but once a week".
Root cause
src/lib/analytics/compliance.ts:131-206 (calculateCompliance) computes:
totalExpected = schedules.length * effectiveDays
The ScheduleWindow interface at src/lib/analytics/compliance.ts:11-14 only exposes windowStart / windowEnd. The MedicationSchedule.daysOfWeek column (prisma/schema.prisma:832) and intervalWeeks recurrence are never read by this function.
Numeric examples
Once-per-week Ozempic, Mondays only, 30-day window:
- Truth: 4–5 Mondays → expected = 4 or 5
- Code:
1 schedule × 30 days = 30 expected
- User takes every Monday (4 doses):
min(100, round(4/30 × 100)) = 13 % instead of 100 %
- User misses one Monday (3 of 4): 10 % instead of 75 %
Every weekly medication looks ~85 % non-compliant.
3×/day metformin, weekdays only, 30-day window:
- Truth: 3 doses × 22 weekdays = 66 expected
- Code:
3 × 30 = 90 expected
- User takes all 66 scheduled: 73 % instead of 100 %
A 27 % undercount that hits anyone with a weekday-only schedule.
3×/day metformin, every day (no weekday restriction):
- Truth: 21 expected per week, user takes 18 → 86 %
- Code:
3 × 7 = 21 expected → 86 % ✓ correct
The pure multi-dose-per-day path is fine — the bug only fires when daysOfWeek or intervalWeeks is involved.
Blast radius (8 production call sites)
/api/medications/[id]/compliance — per-medication card
src/lib/analytics/health-score-fast-path.ts:326,339 — Health Score on the dashboard
src/lib/insights/features.ts:925-927 — AI Coach prompt context
src/lib/medications/medication-compliance-status.ts:187,193 — status card
src/lib/medications/blood-pressure-status.ts:309,315 — BP-status compliance gate
src/app/api/insights/targets/route.ts:893,899 — insight targets
Fix
The cadence-aware correct path already exists at src/lib/medications/scheduling/compliance.ts:129-162 (complianceChips). It uses expandScheduleSlots (src/lib/medications/scheduling/cadence.ts:167-220) which honours daysOfWeek + intervalWeeks + DST + timezone correctly. It is already used on /api/medications/[id]/cadence but the rest of the surface still calls the legacy aggregator.
The fix is to replace every calculateCompliance(events, med.schedules, days, createdAt) call with complianceChips(med.schedules, mappedEvents, new Date(), days, med.createdAt, userTz), adapt the result-shape (the chips path returns slightly different fields — compliance7.streak, .missed), then delete the legacy calculateCompliance function (keep classifyIntakeTiming which is independent).
Pair with a parameterised test matrix covering weekly / bi-weekly / weekday-only / 3×/day combinations to prevent regression.
Release target
v1.5.1. Rationale: shifting Health-Score numerics the same week as the iOS launch (every user with weekly meds will see their Score move 10–20 points upward) creates UAT noise that masks iOS-handoff issues. Land the fix the week after iOS goes live; pair with a one-time "Health Score updated for weekly medications" toast so the change is transparent.
Acceptance criteria
- A weekly Ozempic schedule with 4 taken intakes on 4 scheduled Mondays in a 30-day window reports
compliance30.rate === 100
- A weekday-only 3×/day metformin schedule with all 66 weekday doses taken in 30 days reports
compliance30.rate === 100
- A daily 3×/day metformin schedule with 18 of 21 doses taken in 7 days still reports
compliance7.rate === 86 (no regression on the path that works today)
- Parameterised test matrix in
src/lib/analytics/__tests__/ pins the contract for: weekly Mondays, bi-weekly, weekday-only, weekend-only, 1×/day, 2×/day, 3×/day, and 4×/day combinations
Symptom
Per-medication compliance percentage reported on the medication card, on the Health Score on the dashboard, in the AI Coach prompt features, and on every insight that reads
compliance7.rate/compliance30.rateis wrong for any medication whose schedule is not "every day". The bug compounds with multi-dose-per-day schedules that are also restricted bydaysOfWeek(e.g. metformin weekdays-only).Marc reported it as "I feel the compliance level is wrongly calculated, especially for medications taken not once a day but once a week".
Root cause
src/lib/analytics/compliance.ts:131-206(calculateCompliance) computes:The
ScheduleWindowinterface atsrc/lib/analytics/compliance.ts:11-14only exposeswindowStart/windowEnd. TheMedicationSchedule.daysOfWeekcolumn (prisma/schema.prisma:832) andintervalWeeksrecurrence are never read by this function.Numeric examples
Once-per-week Ozempic, Mondays only, 30-day window:
1 schedule × 30 days = 30expectedmin(100, round(4/30 × 100))= 13 % instead of 100 %Every weekly medication looks ~85 % non-compliant.
3×/day metformin, weekdays only, 30-day window:
3 × 30 = 90expectedA 27 % undercount that hits anyone with a weekday-only schedule.
3×/day metformin, every day (no weekday restriction):
3 × 7 = 21expected → 86 % ✓ correctThe pure multi-dose-per-day path is fine — the bug only fires when
daysOfWeekorintervalWeeksis involved.Blast radius (8 production call sites)
/api/medications/[id]/compliance— per-medication cardsrc/lib/analytics/health-score-fast-path.ts:326,339— Health Score on the dashboardsrc/lib/insights/features.ts:925-927— AI Coach prompt contextsrc/lib/medications/medication-compliance-status.ts:187,193— status cardsrc/lib/medications/blood-pressure-status.ts:309,315— BP-status compliance gatesrc/app/api/insights/targets/route.ts:893,899— insight targetsFix
The cadence-aware correct path already exists at
src/lib/medications/scheduling/compliance.ts:129-162(complianceChips). It usesexpandScheduleSlots(src/lib/medications/scheduling/cadence.ts:167-220) which honoursdaysOfWeek+intervalWeeks+ DST + timezone correctly. It is already used on/api/medications/[id]/cadencebut the rest of the surface still calls the legacy aggregator.The fix is to replace every
calculateCompliance(events, med.schedules, days, createdAt)call withcomplianceChips(med.schedules, mappedEvents, new Date(), days, med.createdAt, userTz), adapt the result-shape (the chips path returns slightly different fields —compliance7.streak,.missed), then delete the legacycalculateCompliancefunction (keepclassifyIntakeTimingwhich is independent).Pair with a parameterised test matrix covering weekly / bi-weekly / weekday-only / 3×/day combinations to prevent regression.
Release target
v1.5.1. Rationale: shifting Health-Score numerics the same week as the iOS launch (every user with weekly meds will see their Score move 10–20 points upward) creates UAT noise that masks iOS-handoff issues. Land the fix the week after iOS goes live; pair with a one-time "Health Score updated for weekly medications" toast so the change is transparent.
Acceptance criteria
compliance30.rate === 100compliance30.rate === 100compliance7.rate === 86(no regression on the path that works today)src/lib/analytics/__tests__/pins the contract for: weekly Mondays, bi-weekly, weekday-only, weekend-only, 1×/day, 2×/day, 3×/day, and 4×/day combinations