A misbehaving Withings upstream that returns a multi-MB error body
could bloat any AuditLog.details row that captured the thrown
message. Apply the cap inside the constructor so every downstream
audit, notification, and pg-boss retry path inherits the bound
without touching call sites.
Closes the v1.4.42 L-1 carry-over. A regression test asserts the
1024-char cap for a 4000-char upstreamError while preserving the
"Withings <verb> error: <status>" prefix the legacy regex parser
depends on.
Mocks `readBestGranularityRollups` with a controllable promise so the
peak in-flight count is observable at the helper boundary. Asserts
peak <= 4 across a 15-type fan-out and re-checks after each drained
resolver so a future regression that floods the pool on completion
would also surface. Mirrors the v1.4.40 W-POOL route-level pin in
`src/app/api/analytics/__tests__/route.test.ts`.
`computeAvg30LastYearMap` in the slim summaries slice ran an unbounded
`Promise.all` over every type the user had data for. On a 15-type
tenant the burst held 15+ Prisma slots simultaneously, and with the
slim and thick `/api/analytics` slices firing in parallel on dashboard
mount the combined fan-out drowned the pg.Pool max=20 raised in
v1.4.40 W-POOL — landing a ~9s server-side wait on every cold mount
since v1.4.40 W-WMY-WIRE.
Cap the per-type fan-out with `p-limit(4)`, mirroring the W-POOL
discipline v1.4.40 applied to the thick route's
`ANALYTICS_TYPE_FETCH_CONCURRENCY`. Same total work, at most 4 slots
held at any moment, leaving headroom for the concurrent thick slice.
The v1.4.42 senior-dev QA flagged that `dedupeWorkoutBatch` reads the
source ladder from the canonical `DEFAULT_WORKOUT_SOURCE_PRIORITY`
constant unconditionally, while the read-time picker resolves the
per-user `sourcePriorityJson` from the User row. A customised user
who promoted MANUAL above APPLE_HEALTH in Settings -> Sources can
still have their preferred Manual row dropped at write-time before
the read-time picker ever sees it.
Scope-narrow today (most paired users keep the Apple-first default)
but the moment a user customises, the divergence silently overrides
them. The architecturally correct fix (v1.4.43 candidate) is to
accept the small ingest-path cost of one indexed
`User.sourcePriorityJson` lookup and thread the user's ladder into
the picker. Until that lands, this docstring is the load-bearing
operator notice.
The W6 swap from `isWithingsRefreshReauthFailure(message)` to the typed
`classifyError(err)` + `classificationToFailureKind()` chain left two
byte-identical 14-LOC catch-blocks in `sync.ts` — one in
`getValidToken`, one in `syncUserMeasurements`. Extract the shared
recording shape into `recordWithingsSyncFailure(userId, err)` so the
two call sites collapse to a single `await`. Sync-activity / sync-
sleep migration to typed errors (W6 deferred to v1.4.43) gets a single
one-line catch block as a bonus when it lands.
No behaviour change — the 140 withings / integrations unit tests
still pin every failure path.
The W2 `returnAllZodIssues` helper duplicated the meta/headers handling
from `apiError`: same `{headers, ...rest}` destructure, same `metaKeys`
length check, same `...(headers ? {headers} : {})` spread on the
constructor. Six lines that drift the moment one helper gains a feature
(e.g. an `errorId` autoinject for Sentry) the other doesn't.
Extract the shared body into `buildJsonErrorResponse(body, status,
meta)`. Both `apiError` and `returnAllZodIssues` collapse to one-line
constructions of the resource-specific body shape. Wire shape is
byte-identical — the 7-case `api-response-zod` suite and every
downstream `apiError` consumer still see the same envelopes.
The v1.4.42 W5 helper shipped under the same identifier
`pickCanonicalWorkoutRows` as the v1.4.30 read-time picker in
`src/lib/measurements/`. Same name in two modules with different
signatures (write-time takes no user-priority, read-time does) sets
up the next contributor to auto-complete the name in a new route
and silently invoke the wrong picker — no user-priority resolution
on a read, missing the 90-second window on a write.
Rename to `dedupeWorkoutBatch` so the write-time contract is
self-describing. The read-time picker keeps its established name.
One file's export + 12-case test suite + the batch-route caller
update — no algorithm change.
The W3-QUERYKEY-LONGTAIL `medicationIntakeList` factory entry packed
the params as an object at index 4: `["medications", id, "intake",
"list", {sortBy, sortDir, limit, offset, status}]`. TanStack hashes
queryKeys via stable JSON, so the object form happens to work today,
but two sibling factory entries (`chartData`, `adminAuditLogFiltered`)
decompose params into a flat tuple precisely to avoid the deep-equal
trap — a future refactor that reorders the object keys or adds a
transient field would break the hash silently while every existing
caller looks correct.
Flatten the entry to match — the cache key now reads `["medications",
id, "intake", "list", sortBy, sortDir, limit, offset, status]`. The
unit-test walker pins the full tuple shape; the `intake-history-list-v2`
component test seed routes through the factory itself instead of a
hand-built bare literal so the params shape stays in lockstep with the
component's read.
W4-ARCH-HYGIENE's 7-way dedup landed in `src/lib/tz/resolver.ts` but
left two more BERLIN_DAY_FORMATTER declarations alive:
- `src/lib/insights/bucket-series.ts` needed the numeric `{year,
month, day}` parts for `Date.UTC` arithmetic, so it kept its own
formatter and a local `toBerlinYmd` helper.
- `src/lib/analytics/bp-in-target.ts` used a separate en-CA formatter
to skip the part-reassembly when producing the YYYY-MM-DD string.
Export a sibling `toBerlinYmd(date)` helper from the resolver that
returns the formatter's raw string parts so `bucket-series.ts` can
parse them into numbers without instantiating a second formatter.
Switch `bp-in-target.ts` to the existing `toBerlinDayKey` import —
the en-CA shortcut is a microscopic speedup not worth the duplicate
formatter and the divergent contract.
Net: one shared formatter for every Berlin-anchored cache key in the
tree. The 272 insights / analytics / tz unit tests still pass.
W6 widened FailureKind from `transient | reauth_required` to
`+ persistent`. Both formatter branches in `formatAdminAlertPayload`
got the new arm wedged in as a nested ternary, which the style guide
explicitly forbids ("prefer switch statements or if/else chains for
multiple conditions").
Collapse both stacks to a single `FAILURE_KIND_COPY` table lookup keyed
off the union. The table reads as the contract; adding a future fourth
kind is a one-row edit instead of two more arms in two stacks. No
behaviour change — the 28 integration-status tests still pin both the
reason label and the action line verbatim.
Every `z.infer<>` source-of-truth type the residual flagged had no
external consumer:
- `MetricSource`, `AIRecommendation`, `AIRecommendationRationale`,
`AICitation`, `AIWarning`, `StoryboardAnnotation` in
`src/lib/ai/schema.ts`
- `InsightFinding` in `src/lib/ai/types.ts`
- `CoachChatRequest` in `src/lib/ai/coach/types.ts`
- `ApplyProfileInput` in `src/lib/auth/profile-update.ts`
- `CreateSideEffectInput`, `ListSideEffectsInput` in
`src/lib/medications/side-effects/validators.ts`
- `AnalyticsSlicePayload` in `src/lib/queries/use-analytics-query.ts`
- `WithingsWebhookAuthOutcome` in `src/lib/withings/webhook-handler.ts`
Plus three stragglers — `ThresholdOverride` (the persisted shape is the
sibling `ThresholdOverridesJson`), `ChartRangePreset` + the now-orphaned
`CHART_RANGE_PRESETS` constant it was derived from (no chart reads the
preset list), and the `WebPushChannelConfig` + `ApnsChannelConfig` +
`ChannelConfig` triplet in `src/lib/notifications/types.ts` (the
dispatcher imports the two real channel configs by name; the empty
records and the union added nothing).
`src/lib/validations/**` keeps its `z.infer<>` types — that's the
external API surface the iOS client matches against. Muted via
`ignoreIssues` so the gate can enforce while the contract surface
stays self-documenting.
Every shadcn UI re-export the simplifier residual flagged
(`AlertDialogMedia/Overlay/Portal`, `AvatarBadge/Group/GroupCount`,
`badgeVariants`, `buttonVariants`, the `Card*` / `Dialog*` /
`DropdownMenu*` / `Select*` / `Sheet*` / `Table*` / `tabsListVariants`
tier) is part of the upstream `new-york-v4` registry — confirmed
against `https://ui.shadcn.com/r/styles/new-york-v4/*.json`. Keeping
them in tree preserves library-shape parity so the next shadcn refresh
applies cleanly. Mute the whole `src/components/ui/**` surface for
the `exports` + `types` issue tiers via `ignoreIssues`.
`ignoreExportsUsedInFile: true` covers the schema-source-of-truth
pattern across `src/lib/ai/**` + `src/lib/validations/**`: the schemas
are exported for the inferred `z.infer<>` type but only referenced
internally for composition. Knip's default behaviour flagged them as
unused exports; the option teaches it that same-file references count.
Two genuine dead helpers go: `requireAdmin` in `src/lib/auth/session.ts`
shadowed the real implementation in `src/lib/api-handler.ts` and had
no callers, and `readDailyMeans` in `src/lib/rollups/measurement-read.ts`
was an unused convenience wrapper around `readRollupBuckets`.
`tokenKind`, `withBackgroundEventSafe`, and `isRollupFresh` had no
callers anywhere in the tree, so the symbol + JSDoc + index re-export
are removed. `PROGRESS_TICK_RECORDS`, `MAINTAINED_LOCALES`, and
`SUB_PAGE_METRIC` lose the `export` keyword — every reader lives in the
same module, so the constants stay local. `isRollupFresh` was deferred
in v1.4.41 pending the persistent-rollup reader landing on develop; the
read-path now carries its own per-tier freshness probe and never
imported the helper.
No behaviour change. Knip drops from 35 unused exports to 29.
W1-KNIP's reconcile callouts identified two re-exports that no caller
ever consumed:
- `src/lib/tz/resolver.ts:51` re-exported `listSupportedTimezones` from
`./format`, but both call-sites (`components/settings/timezone-picker.tsx`
and `components/admin/general-settings-section.tsx`) import directly
from `@/lib/tz/format`.
- `src/components/medications/glp1-medication-card.tsx:595-596`
re-exported `describeInjectionSite` "so the parent doesn't need to
import it", but every real consumer (`therapy-timeline.tsx`,
`injection-site-picker.tsx`, the unit test) imports the source from
`@/lib/medications/injection-sites` directly.
Drops both re-exports and the now-orphaned `describeInjectionSite`
import in `glp1-medication-card.tsx`. Clears the last knip findings so
the gate flip in the next commit lands clean.
The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.
New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).
Lockstep updates:
- `eslint-plugins/healthlog/queryKey-factory.js` extends
`GUARDED_DIRECTORIES` to cover `src/components/{settings,
medications,admin}` + `src/hooks`, plus the three guarded files
under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
`guardedRoots` and pins the shape of every new factory entry.
Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
Withings sometimes replies HTTP 200 with a non-zero body status, empty
measuregrps, or a transient 503. Pre-v1.4.42 the client surfaced all of
those as identical `Error("Withings <verb> error: <status>")` throws,
and the sync layer used a message regex to bucket reauth vs transient.
That made rate-limit (601) and contract-mismatch (293/294) responses
silently retry forever without ever reaching an admin alert.
Adds `src/lib/withings/response-classifier.ts` — a pure
`(httpStatus, body) -> {success | transient | reauth_required |
persistent}` function plus a `WithingsApiError` subclass carrying the
verdict. The four client entrypoints (exchangeCode, refreshAccessToken,
fetchMeasurements, subscribeWebhook) and both v2-sync paths
(sync-activity, sync-sleep) now throw the typed error; sync.ts reads
err.classification directly via classifyError() and falls back to the
legacy regex when the prototype is lost on a pg-boss retry.
Extends FailureKind with `persistent`. Persistent failures audit with
kind="persistent" and surface a distinct admin-alert label
("persistent error" / "investigate the upstream contract") but keep
state=error_transient so the next sync still runs once the upstream
side fixes the mismatch.
Three test branches pinned (37 new tests, 103 -> 140 passing in
src/lib/withings + src/lib/integrations).
iOS HealthKit observer queue routinely carries the same logical workout
from two paired sensors (Apple Watch + Withings ScanWatch) in a single
ingest batch. Without write-time dedup both rows persist (their
externalIds differ, so the (userId, source, externalId) unique index
never fires) and the read-time picker has to carry the duplicate
forward on every dashboard render.
Add pickCanonicalWorkoutRows() at src/lib/workouts/canonical-rows.ts as
a pure pre-createMany pass invoked by POST /api/workouts/batch. Groups
by (userId, activityType, startedAt +-90 s); prefers the canonical
source ladder (APPLE_HEALTH > WITHINGS > MANUAL > IMPORT) shared with
the read-time helper; breaks ties on calories > earliest createdAt >
input order. Losers surface to the iOS client as duplicate so the sync
cursor advances past them identically to the externalId case.
pr-detection-worker.ts ran its warm-up count and best-measurement scan
across every Measurement row including soft-deleted ones. A user who
deleted a row that happened to be their PR would not get the next-best
row promoted on the next detection pass. Add `deletedAt: null` to both
where clauses to align with the v1.4.40 soft-delete completeness sweep.
Also document the offhost-backup DR-intent at the matching findMany —
that path deliberately includes soft-deleted rows because the offhost
upload is a disaster-recovery snapshot, not a user-facing export.
src/lib/doctor-report-data.ts was checked in as raw bytes (file(1)
reported "data", git diff reported "Binary files differ") because the
control-character regex carried literal NUL + US + DEL bytes inside the
character class. Rewrite the class as `[\x00-\x1F\x7F]` so the file is
UTF-8 clean and future diffs are readable. Same runtime behaviour — the
existing 18-case test suite still passes.
Each of the seven src/lib/insights/*-status.ts files carried an identical
20-LOC block defining BERLIN_DAY_FORMATTER plus toBerlinDayKey. Extract
the helper into src/lib/tz/resolver.ts (next to dayKeyForScheduledFor and
friends) and import it everywhere. Same runtime behaviour, 100-LOC drop.
Historic pattern was apiError(parsed.error.issues[0].message, 422) which
dropped every issue past the first. Mobile callers debugging a contract
needed one round-trip per wrong field. The new helper returns every
issue under details.issues, additive to the existing envelope so any
client that only reads `error` keeps working.
Sanitises issues to {path, code, message} — issue.params can carry the
raw rejected user input for some Zod codes and must not leak.
Bring blood-pressure, weight, general, pulse, mood, and
medication-compliance into parity with bmi-status. On a 20 s
provider stall every route now persists a sentinel auditLog row
keyed to today's Berlin day so the next mount short-circuits at
the cache lookup instead of re-racing the same provider call on
every cold visit. Eliminates the ~14 s warm response on every
dashboard open after a one-off provider hiccup.
The seven routes share one persist path through
`persistTimeoutStubAndReturn`; ~120 LOC of duplicate
`auditLog.create` calls collapse into the helper.
medication-compliance returns a richer `{summary, medications}`
envelope rather than the standard `{text}` shape, so it calls
the helper for the persist side-effect and maps `summary` ←
`text` on the way out. Its cache-read also gains a stub-row
recogniser so the second-mount short-circuit fires on the
helper's `text` + `timeout: true` payload.
Tests cover both arms per route: the persist-on-timeout path
asserts a single auditLog.create with `timeout: true`, model
`timeout-stub`, and today's Berlin dateKey; the
short-circuit-on-stub path asserts a subsequent mount returns
the cached stub text without invoking the provider.
Pull the 40-line auditLog.create block out of bmi-status into a
dedicated `persist-timeout-stub.ts` helper so every InsightStatus
route can share one persist path instead of copy-pasting the
sentinel-row shape. The bmi route now calls the helper in a
6-line return; behaviour and persisted body are byte-equivalent
to the inline form.
Sets up the next commit, which wires the six remaining sibling
routes (blood-pressure, weight, general, pulse, mood,
medication-compliance) to the same helper and closes the
last bare-fallback shape on the dashboard hot path.
Narrow each helper's visibility to its actual reach instead of leaving
it exported "just in case":
- Drop the `export` keyword on file-local helpers / constants:
`getTrendSentiment`, `isPRWithin30Days`, `buildCoachMetricSourceType`,
`countPassedSchedules`, `getGithubConfig`, `isComparisonBaseline`,
`DASHBOARD_LAYOUT_VERSION`, `INTL_LOCALE_MAP`, `SIDE_EFFECT_NOTES_MAX`,
`MEDICATION_CATEGORIES`, `openApiBase`, `ALL_MOOD_GRANULARITIES`.
- Remove never-used dead code:
`CodexOAuthNotConfiguredError` (declared as a backwards-compat shim
no caller ever consumed), `editMessageReplyMarkup` (Telegram method
nothing on the bot dispatch tree calls), and the stale
`DISPLAY_TIMEZONE` re-export from `src/lib/format.ts`.
Knip drops from 48 -> 35 unused exports. Pure narrowing / deletion;
no behaviour change.
`recomputeMoodBucketsForEntry`, `recomputeMedicationComplianceForDay`,
and `recomputeMedicationComplianceForEvent` all declared an optional
`tx?` Prisma transaction client but no call site has ever passed one —
every caller (routes, jobs, worker, tests) invokes them outside a
`$transaction` block. The dead surface area kept dragging a
`PrismaLike` / `PrismaTxOrClient` alias plus a top-level `Prisma`
import along with it. With the param gone the helpers simply close
over the singleton `prisma` client; the alias and the
type-only import follow.
No call-site changes needed — the param was optional everywhere.
Both `/api/medications/intake?scope=today` and `/api/dashboard/summary`
carried their own copy of the v1.4.39 projection + idempotent
`createMany` + per-`(med, day)` compliance recompute. The duplicate
implementations had already drifted twice; fold them into one tested
location at `src/lib/medications/scheduling/project-today-intakes.ts`
so future changes (new IntakeSource literal, new schedule cadence,
audit metadata) only land once.
The helper stays read-shape-neutral — the two routes return different
projections of the resulting events, so the caller still owns its
typed re-read. Returns `{ projected, backfilled }` for the existing
`annotate` telemetry.
Net: ~200 lines collapsed into 145 (helper) + two short call sites.
The insights page kept `CorrelationResult` + `DataSummary` type imports
left over from an earlier refactor, and the slim summaries slice still
referenced `RollupGranularity`, `aggregateWmyBuckets`, and
`RollupBucketRow` after the v1.4.37.2 GROUP BY rewrite folded that work
into the narrow aggregate. Pure deletions — both helpers stay exported
for the test suite that still exercises them.
Mirror the bmi-status (v1.4.37) timeout-stub persist into the two
sibling endpoints. On a 20 s provider stall the route now writes a
sentinel auditLog row keyed to today's Berlin day so the next mount
short-circuits at the cache lookup instead of re-racing the same
provider call for the rest of the day.
This eliminates the ~14 s warm response on every iOS dashboard open
when the provider has stalled once that day. Response envelope
unchanged; iOS v0.5.4 contract preserved.
Tests cover both arms: the persist-on-timeout path (one create with
timeout:true, model:"timeout-stub", today's dateKey) and the
short-circuit-on-stub path (subsequent mount returns cached text,
provider never invoked).
The `sum_value IS NULL` UNION arm in `enqueueBootTimeRollupBackfill`
was added in v1.4.39 W-SUM to converge DAY rollup rows that pre-dated
the writer change. Production data has since converged, so the arm is
a permanent no-op seq scan over `measurement_rollups`. Per-day missing
coverage remains the sole discovery anchor.
Drops the legacy text-anchor unit test. The read-side `mean * count`
fallback in `/api/dashboard/summary` retains legacy-row readability
and stays covered by its dedicated route test.
Pre-v1.4.41 prompts lived in two homes — `src/lib/ai/prompts/` (per-
metric system prompts + safety contracts) and `src/lib/insights/prompt
*.ts` (user-prompt builder + comparison snapshot + the `compactSections`
helper). A new reader could not tell from filenames which file to
extend; both directories looked equally authoritative. Audit rec #3
flagged the split as the only undiscoverable seam in the AI layer.
Move both files into `src/lib/ai/prompts/`:
- `lib/insights/prompt.ts` → `lib/ai/prompts/insight-system-prompt.ts`
- `lib/insights/prompt-compact.ts` → `lib/ai/prompts/compact-sections.ts`
- co-located tests follow
Three importers updated: `app/api/insights/generate/route.ts`,
`lib/ai/coach/snapshot.ts`, the generate route test (the
`vi.mock('@/lib/insights/prompt')` path moved too). Prompts now have
ONE home and ONE mental model.
Routes the registration-status, auth invalidation, notifications
preferences, and Settings about-section api-version reads through
the centralised queryKeys factory. Adds factory entries for the
remaining settings/admin surfaces a follow-up will migrate, and
extends the walker test to fail on any future bare-literal
queryKey in src/app/auth, src/app/notifications, and the
about-section. Also swaps the dashboard tile Suspense fallback
from null to a layout-stable card placeholder so a future RSC
hoist of any tile slot can't trigger CLS.
Closes SB-7 follow-up. POST /api/auth/check-user accepts
{ identifier: string } (username or email, case-insensitive) and returns
{ branch, hasPasskey, hasPassword } where branch is one of:
- not_found no account
- passkey_only passkey present, no password
- email_fallback password present (passkey optional)
- exists neither credential (recovery path)
The booleans let the iOS client render a "Sign in with Passkey"
affordance alongside the password field without a second round-trip.
Five-case test pin covers every branch plus the 422 identifier-required
guard. The endpoint mirrors the envelope shape of /api/auth/passkey/
login-options and adds no new enumeration surface beyond that route.
v1.4.41 W-DELETED-2 — the doctor-report-data aggregator (shared by
/api/doctor-report JSON + /api/doctor-report/pdf server render) and the
section-availability probe now filter deletedAt: null so deleted rows
never appear in the rendered PDF or trigger an empty section toggle.
`z.string().max()` counts JavaScript string units (UTF-16 code
units), not bytes. The audit-table guarantee for SB-10 consent
artefacts is byte-bounded — a UTF-8 artefact full of multi-byte code
points (CJK, emoji) would otherwise slip past the 64 KB row budget.
Replace the `.max()` with a `.refine()` that uses
`Buffer.byteLength(value, "utf8")`.
Closes security M1 from the v1.4.40 QA round.
The compare-toggle component still declared three bare-literal
`["user", "dashboardWidgets"]` queryKeys for read, onMutate
cancel/set, onError rollback and onSettled invalidation. Migrate every
call site to the central `queryKeys.dashboardWidgets()` factory so a
future rename or shape evolution only touches one file. Extend the
W-RSC factory-bypass walker to cover `src/components/comparison/`
so any future regression in this directory gets caught by CI.
Closes UX H1 from the v1.4.40 QA round.
- src/lib/medications/compliance-rollups.ts → src/lib/rollups/medication-compliance-rollups.ts
- co-located test follows the source file
10 importers updated covering the dashboard summary, every intake CRUD
route (single + bulk + import + Telegram webhook), the ingest endpoint,
the admin backups restore path, and the reminder worker job.
Closes the rollup umbrella restructure — every persistent-rollup
producer + reader now lives under src/lib/rollups/. Pure relocation
across all five batches: zero behaviour or exported-surface change.
- src/lib/mood/rollups.ts → src/lib/rollups/mood-rollups.ts
- co-located test follows the source file
17 importers updated from @/lib/mood/rollups to @/lib/rollups/mood-rollups,
covering the mood-entries CRUD routes, the moodlog webhook, the import
restore path, the insights comprehensive + targets routes, the analytics
+ features paths, and the reminder worker + moodlog sync job consumers.
Pure relocation.
- src/lib/measurements/rollup-read-wmy.ts → src/lib/rollups/measurement-read-wmy.ts
- src/lib/measurements/rollup-read-cumulative.ts → src/lib/rollups/measurement-read-cumulative.ts
- co-located tests follow the source files
- cumulative module's sibling import of apple-health-mapping promoted to
the @/lib alias since that mapping stays in src/lib/measurements/
Importers updated: 2 sites for WMY (health-score + summaries-slice),
zero external sites for cumulative (only co-located test reads it).
Pure relocation.
Creates the new src/lib/rollups/ aggregator directory and relocates
the main measurement rollup module:
- src/lib/measurements/rollups.ts → src/lib/rollups/measurement-rollups.ts
- src/lib/measurements/__tests__/rollups.test.ts →
src/lib/rollups/__tests__/measurement-rollups.test.ts
Every importer of @/lib/measurements/rollups is rewritten to
@/lib/rollups/measurement-rollups. The orphan sibling reference inside
src/lib/measurements/rollup-read.ts (still in its old home until the
next batch) is promoted to the @/lib alias so the chain keeps
typechecking through the in-flight move.
Pure relocation — no behaviour change, no exported-surface change. The
umbrella exists so the rollup contract is discoverable from a single
directory before the iOS sprint locks the boundary.
The compliance-rollups module and the cadence scheduling module each
carried a private wallClockInTz definition. The cadence variant
returned the full {year,month,day,hour,minute,second,weekday} tuple;
the compliance variant returned only {year,month,day}. Promote one
canonical helper that returns every part — the compliance caller
destructures the three fields it needs — so the cross-tz ±3 h guard
stays in lock-step instead of drifting between sibling modules.
The mood-rollup and measurement-rollup writers carried byte-identical
copies of the helper that could drift independently. Promote one
canonical export under src/lib/tz/ so both writers — and any future
rollup table — import the same UTC-anchored truncation.
The runtime never reads the format namespace — every locale-aware
formatter routes through Intl.DateTimeFormat directly via
src/lib/format-locale.ts. The keys were only verified by their own
ordering smoke test, which is also removed.
The glucose UI only converts canonical mg/dL into mmol/L for display
via convertGlucose; the reverse helper had no caller outside its own
test. The Berlin-only day-key helper was already superseded by
userDayKey(date, tz) in v1.4.38 for every read path that consumed it;
its remaining presence as a tested module was dead weight.
Pins the cross-consumer routing contract the v1.4.40 wire-in relies
on. Both computeSummariesSlice (year-ago baseline) and
computeUserHealthScoreFastPath (weight long-window) now call
readBestGranularityRollups so the floor table is shared state. The
new tests prove:
- 90 d -> DAY (every coarser floor too high),
- 365 d -> MONTH (clears 181 d floor, short of 731 d YEAR floor),
- 1095 d -> YEAR (clears the 731 d YEAR floor).
Plus a parity assertion: a MONTH bucket consolidating 30 measurements
with mean 80 returns the same count-weighted mean as the two DAY
buckets (count 10 / mean 82 + count 20 / mean 79) that compose it.
Locks the linearly-composable contract count / mean carries across
granularities — any future floor adjustment forces both consumers to
re-pin in lock-step rather than silently diverging.
The bucket-fresh read paths probe coverage per-type via
coverage.get(type) or rely on isFullyCovered for the all-or-nothing
fast path; the typesMissingCoverage helper had no production caller.
The cumulative-batch reader was only exercised by its own test —
the analytics A2 path took the chunked per-type findMany route in
production. Strip both and their tests.
Wires readBestGranularityRollups into the WEIGHT-covered branch of
computeUserHealthScoreFastPath so the trailing-year weight baseline is
served from the WEEK / MONTH / YEAR rollup tier instead of falling
through to a live measurements walk. The 365-day window routes through
MONTH by default (~12 buckets) and falls back to WEEK -> DAY on per-
tier coverage miss. count / mean compose linearly across MONTH buckets
so the derived mean is mathematically equivalent to the per-row
average over the same window.
The read fires in parallel with the canonical DAY-bucket read for the
30-day weight series so wall-clock cost stays flat. Score shape is
unchanged; the weightLongWindow annotate carries the resolved
granularity + bucket count + mean so operators can verify the rollup
tier serves production traffic. The live-fallback branch deliberately
omits the long-window probe because the raw weight findMany already
does the work — guards against double-reading on cold mounts.
Neither function had any production caller. The chart components in
src/components/charts/* implement a local movingAverageByPoints rather
than reaching for the lib helper, and weeklyAverages was never imported
outside its own test file. detectAnomalies stays — it is reached
transitively through summarize() which the analytics, insights, and
measurements-series routes all consume.