`classifyIntakeTiming` treated `windowStart === windowEnd` as an overnight
wrap, inflating a degenerate point window into a ~30h on-time band — a dose
taken many hours late still classified `on_time` and fed a perfect-compliance
streak. Widen the point window symmetrically instead, matching the treatment
`resolveLegacyWindowBounds` already applies.
`/api/measurements/batch` never applied the entry-instant plausibility bound
its single-entry twin has carried since v1.17 W1b, so a future-dated row landed
and permanently won 'latest reading' while poisoning the rollup buckets and the
canonical picker. Skip per entry, like the range guard. The past floor stays at
1900 — a historical backfill is a legitimate shape here.
The intake CSV import had neither an upper date bound nor a rate limiter, while
running up to 1000 sequential find + create + consume iterations per call. Add
both, on the bulk twin's bucket shape and budget.
The suite was not timezone-pinned. CI runs UTC, so a whole class of
day-bucketing defect was invisible to the gate while a contributor outside
UTC saw a red suite on unmodified main. Verified: a UTC+14 host fails five
tests on an otherwise green tree. Pin TZ=UTC in both vitest configs; tests
that need a different HOST zone now set and restore it explicitly.
The resting-pulse fixtures passed offset-less ISO literals, which parse as
host-local, so they could not distinguish the Berlin day-key the
implementation uses from a UTC one — swapping the day-key's zone left the
file green. Build each instant explicitly in Berlin and add cases whose
local day and UTC day genuinely differ; that swap now fails two of them.
Delete the 'DST boundary (spring-forward)' compliance case. It passed no
timezone, so the slot builder took the legacy UTC path where the date
carries no transition, and it asserted totalExpected === 7 — which holds in
every zone and therefore could never see the double-emit its own comment
described. Threading a real zone does not rescue it: the assertion stays
true in Berlin, UTC, UTC+14 and UTC-11 alike. The property it claimed is
already pinned properly in the scheduling suite, which walks
buildCadenceTimeline with an explicit zone across both the 23 h
spring-forward and the 25 h fall-back day.
The medication-compliance engine lived in one 2,099-line file with 38
exports feeding eight surfaces (Coach prompt, BP-status gate, dashboard
pillar, per-medication tile, insight targets, doctor report, …). A single
edit intended for one surface could silently shift all of them.
Split along the existing internal seams into `analytics/compliance/`:
parsing, types, adapters, dose-status, occurrences, windows, cycle,
ledger, display. `analytics/compliance.ts` is now a thin barrel that
re-exports every public symbol, so no consumer import path changes. Pure
move — no logic edits; the tally, window ladder, ledger and cycle math
are byte-for-byte the code they were. Tests move alongside under
`compliance/__tests__/` and keep exercising the public barrel.