The canonical rotation registry (src/lib/crypto/encrypted-columns.ts) is the
source of truth its guard test cross-checks; add EcgRecording.waveformEncrypted
there (the script already references it) and drop the redundant parallel guard.
Update the sleep/rhythm route test to the v1.19.0 rolling 5-night balance
(nightsCounted 5, debt 300) instead of the old 14-night cumulative sum.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Walk every per-domain route tree that serves a toggleable module and
classify each route.ts as module-gated, delegated (cycle/coach),
builder-gated (doctor-report/FHIR), or explicitly exempt with a reason.
A new ungated module route trips the guard by name.
Close four route-level gate gaps the inventory surfaced: the mood
analysis reads (/api/mood/insights, /api/mood/analytics) and the sleep
view reads (/api/sleep/night, /api/sleep/rhythm) now gate on their
module instead of staying reachable over a Bearer token while the module
is off. Pin the gate to enabled in the three pre-existing route suites.
WHOOP's v2 API returns only per-stage sleep duration totals — no onset
timestamps, no hypnogram endpoint. The mapper stamped all five stage
totals on the one sleep-END instant, so the hypnogram reconstructed
every stage as a span touching the night's right edge: bars stacked
right-aligned with no clock times whenever WHOOP was the sole source.
Lay the asleep stages back-to-back from sleep onset in a fixed
physiological order (AWAKE lead, then CORE → DEEP → REM), emitting one
timed row per segment with measuredAt at that segment's end and a
distinct indexed externalId. IN_BED stays a single envelope row over the
whole window. The existing reader and renderer are untouched — they now
see distinct per-segment instants and place each block at its own time.
The order is synthesised, not measured, so every reconstructed row is
flagged and the night DTO carries a per-session `reconstructed` boolean
(GET /api/sleep/night, regenerated OpenAPI). The client renders the
hypnogram but labels it an approximate layout and never recomputes;
real-series sources (Apple Health, Withings, Fitbit) stay false.
Compute cumulative sleep debt and the MCTQ chronotype server-side off the
canonical night reconstruction the Sleep Score reads, and render them on
the sleep page: a sleep-debt headline with a calm partial state under the
night threshold, and a chronotype card whose social-jetlag detail and
corrected mid-sleep sit behind an advanced disclosure. Both hold a learning
state on thin data and never assert a total or a band.
Serve the same values to the dashboard summary and a new GET /api/sleep/rhythm
read so the native client renders identical figures. Free vs work nights
default to weekend in the user's timezone until day-type tracking exists.
Replace the divergent UTC-day night reconstructor in the Sleep Score with a
thin adapter over reconstructSleepNights, the shared engine the dashboard,
hypnogram, and doctor surfaces already read. The old path bucketed rows by UTC
calendar day and never deduplicated writers, so a multi-source night (WHOOP +
Apple Health for the same sleep) summed both writers and roughly doubled the
total. The adapter inherits session-clustering, local-wake-day keying, and
multi-source writer dedup, then derives the score-only fields the canonical
night does not carry: REM and Deep minutes from the stage map, the stage-
breakdown flag from any granular stage, and the midpoint from the asleep span.
Select source and deviceType in the Sleep Score read so the writer-dedup
ladder fires, and thread the user source priority through so the chosen writer
matches every other sleep surface. Round emitted asleep and in-bed minutes to
whole numbers, and round the per-stage map in the sleep-night DTO, matching the
integer contract the native client decodes.
A multi-source user's sleep score and the readiness score that folds it drop
once the double-count is removed, reflecting the true single-writer night.
`GET /api/sleep/night` summed second-precision stage rows into asleep /
awake totals and serialised them unrounded (e.g. 433.4999), which also
violated the OpenAPI integer contract. Round the present minute values;
`inBedMinutes` / `awakeMinutes` stay null when the night never saw that
stage.
Sleep nights collapse per writer instead of per source: several
HealthKit apps land behind the same source value, so a phone's coarse
awake and in-bed blocks could blend into or fully constitute a night
whose stages another writer had recorded. The winning writer per
session is the one with the most distinct granular stages, then the
user's source ladder, and its rows are kept whole. The device type is
threaded through every sleep read including the stage stacked bar, and
the sync overlap for late-arriving nights widens from one day to seven
so a night scored after the cursor passed is fetched on the next tick —
assembly is read-time, so affected nights heal without manual repair.
Mood notes expand to their full text in the list on desktop and touch,
replacing the hover-only tooltip, and the mood CSV export carries a
note column.
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
GET /api/sleep/night?date=YYYY-MM-DD returns one reconstructed night for
the phase-progression view: the canonical source's stage segments (each
with an absolute start/end span), the per-stage breakdown, the asleep/
in-bed/awake totals, the mid-sleep awakenings count, and same-wake-day
naps surfaced separately. Session-clustered, wake-day-keyed, one
canonical source per night so two sources never overlay. date omitted
returns the most recent night. A view over existing per-stage rows — no
schema, no new measurement type. OpenAPI regenerated.