repair-intake-anomalies.ts reports duplicate live rows sharing a slot anchor and intake rows whose taken time sits implausibly far from their slot. Dry-run by default; --fix tombstones duplicate losers with the same precedence the nightly dedup applies and recomputes the affected compliance rollup days; implausible rows are only listed unless --tombstone-implausible is passed explicitly. Runbook under docs/ops.
3.4 KiB
Medication intake repair
scripts/repair-intake-anomalies.ts repairs two known historic defects in
the medication intake ledger (medication_intake_events):
- Duplicate dose-slot rows. More than one live row on the same exact
(user, medication, scheduled_for)tuple — cross-source duplicates the pre-v1.15.19 write path could mint (e.g. a pending REMINDER row plus a taken API row for the same slot). The duplicate inflates the per-day scheduled count and paints a phantom entry in the history view. - Implausible
taken_at. Live rows whosetaken_atlands more than 7 days beforescheduled_foror more than 1 day in the future — typically the residue of a mis-attributed edit before the v1.15.19taken_atvalidation landed.
When to use it
- After upgrading to v1.15.19+, when a user reports a compliance rate above 100 %, duplicate entries in the medication history, or a dose shown as taken days away from its slot.
- The compliance rollup counts DISTINCT slots since v1.15.19, so the percentages self-correct on recompute even without running this script. Running it additionally clears the phantom rows from the history view.
- The
intake-slot-dedupworker already collapses canonical-slot duplicates at boot and on a daily cron; this script is the manual, immediately-observable fallback and the only path that surfaces the implausible-taken_atrows.
Run a dry-run first
Always. The default mode only reports and never mutates:
# inside the app container (or any checkout with DATABASE_URL set)
pnpm dlx tsx scripts/repair-intake-anomalies.ts
# scoped to one account
pnpm dlx tsx scripts/repair-intake-anomalies.ts --user <userId>
Use pnpm dlx tsx, not bare pnpm tsx — the production standalone image
strips tsx. DATABASE_URL must point at the target database.
The dry-run prints every duplicate group (which row would be kept, which
would be tombstoned) and a table of implausible rows (id, medication,
scheduledFor, takenAt, source, timestamps).
Applying the fix
pnpm dlx tsx scripts/repair-intake-anomalies.ts --fix
--fix does three things:
-
Duplicate groups are collapsed to one winner per slot. Precedence: an actioned row (taken or skipped) beats a pending one; between two taken rows the earlier
taken_atwins; ties break deterministically oncreated_at, thenid. Losers are soft-deleted (deleted_atset,sync_versionincremented) — the same tombstone shape the delete route writes, so connected clients drop the rows on their next delta-sync. Nothing is ever hard-deleted. -
Implausible
taken_atrows are NOT touched. The recorded intent is unknowable, so the operator (or the user) corrects or deletes them in the medication history tab. To soft-delete them wholesale instead, opt in explicitly:pnpm dlx tsx scripts/repair-intake-anomalies.ts --fix --tombstone-implausible--tombstone-implausibleis refused without--fix. -
Affected compliance rollups are recomputed for every touched
(user, medication, day)through the shared rollup helper, so the scheduled/taken counts and the rate self-correct immediately.
The script is idempotent: a second --fix run finds zero duplicate groups
and changes nothing. Exit code is 0 on success (including a clean
zero-findings run), non-zero on bad arguments or a fatal error.