Measurement upserts in /api/sync/changes carry appleHealthMirror. A new
measurementMirror domain delivers { id, appleHealthMirror } for the live
readings of every token whose setting changed past the cursor, keyed on
(token.mirrorChangedAt, measurement.id). Switching the setting writes the
token row only, so the readings keep their updatedAt and syncVersion and
nothing keyed on them recomputes. The domain is additive inside the v2
cursor and is left out of the retention expiry, since its watermark is a
setting's age, not a deletion's.
A reading sent with a measurements:write token keeps its EXTERNAL source
and now points at the token that carried it (measurements.ingest_token_id).
Every stored row answers a server-resolved appleHealthMirror: true for
WITHINGS, IMPORT and MANUAL, the token's choice for EXTERNAL, false
otherwise. The plain list accepts appleHealthMirror=true so a client can
page exactly those rows.
Migration 0476 adds the columns and links existing EXTERNAL readings only
where one measurement token provably sent them; 0477 builds the index
concurrently. Backups carry the link and a restore keeps it only for a
token the account still holds. A guard holds every EXTERNAL writer to
setting the link.
The custom-metric create and update refusals and the medication layout
refusal are filed through auditLog now, so a delegate's refused request
reaches the owner's activity feed, where it read as the generic change.
Each gets its own sentence in every bundle; the refusal sentences live in
their own function. The audit write guard states its floor, and the
schedule-reader guard names the medication create and edit, which answer
through medicationDetail.
The create, its mirror replay and the edit answered the written row
without the read-model fields the declared resource carries (reminder
state, stock and runway, actionability, course status, composition,
links), while the detail read answered them. They answer the read model
now, built from the row they just wrote (medicationDetail), so a client
reads back exactly what GET /api/medications/{id} would say.
A dose write that converged onto an existing slot answered the slot
upsert's narrow row, without userId, medicationId, updatedAt,
autoRecorded, autoMissed or attributionSource. The upsert reads and
returns the whole event row, the same shape a write that inserted its own
answers. The parity walks drop their contract skips and are re-pinned.
A mirror create re-posting a pair the caller already holds has always
answered the existing medication with 200, as the description says; only
the 201 was declared.
Twelve routes wrote their <surface>.validation-failed row with a bare
prisma.auditLog.create, so they skipped the two decisions auditLog makes:
an owner's own refused request stays on the wide event
(meta.rejected_attempt) instead of becoming a row, and a delegate's names
the delegate as the actor. The bulk medication and mood ingests, the
layout, overlay, device, health-sync, metric and ingest routes all go
through auditLog now, and a structural guard keeps raw writes to the one
writer and the briefing failure marker. The unit tests assert the auditLog
call, and the bulk walks are re-pinned without the owner's refusal row.
The briefing prompt asks for dailyBriefing, trendAnnotations and
storyboardAnnotations beside the insight fields, and the briefing card,
the dashboard snapshot and the today read consume them. insightResultSchema
was a strict object, so it dropped exactly those blocks from a reply that
validated, while a reply that failed validation kept them. It is a loose
object now: the named fields are checked and the rest of the reply stays.
A key id retired from ENCRYPTION_KEYS, or a corrupt row, made decrypt throw
inside the provider builders, and a briefing generation answered 500. An
unreadable key now drops its entry from the chain like a missing one, with a
warning on the event, so the generation answers the no-provider 422
(ai.provider.none). The connection test reads it as no stored key. The
parity walk holds that case to the contract again.
Both ingests store each route point's sample as { t, speedMs?, hr? }, while
computeSplits parsed every sample as a bare instant and the GPX export
skipped anything that was not a string. Splits were therefore null for
every ingested track, and an exported track carried no times. One reader,
routeSampleInstant, takes either shape. The workout detail walk is
re-pinned: its ingested run now answers its first kilometre split.
Work a request starts and does not wait for runs inside the request's
context, so an annotate({ action }) from it landed on the request's event
whenever it ran before the event was emitted. The measurement tail's
status re-warm logged a hand-entered reading as
insights.status.invalidate.skipped on some requests and as
measurement.create on others.
runDetached starts such work so that its action is dropped for the event
it was detached from; meta, warnings and the auth it reads stay as they
were, and an event the work opens for itself keeps its own action.
fireAndForget now takes a function and starts it detached, and every
trackBackgroundTask handle on a request path is built with runDetached,
which a structural guard holds.
The splits on this line took the files over 800 lines from 115 to 96,
under the liveness floor of 100. The floor guards the matcher, so it
moves to 80, below the real count.
A duplicate upload has always answered meta.duplicate (and meta.deleted for
a deleted source key) beside the row, as the description says, but the
envelope schema declared neither. An episode or visit id that is not the
caller's has always answered 404, which no response declared.
The inventory reads a delegated route and its direct imports. With the medication handlers moved into lib/medications, the lifecycle reconciliation a dose write files was two levels from its route and dropped out of the walk. A route's handler-body modules now count at the route's own level, listed in one map whose every entry must be a direct import of its route.
POST /api/medications/intake/bulk keeps authentication, its own bulk bucket, the body with its size and schema refusals, the payload schema and the HTTP mapping. The ingest, one 846-line function, moves to lib/medications/intake-bulk/ in the order it always ran: the ownership read, the Apple mirror gate and the batch pre-reads (ingest.ts), each entry's refusals, site, write bookkeeping and P2002 answer (entry.ts), its slot attribution (entry-attribution.ts) and its slot, replay, probe or standalone write (entry-write.ts), then the audit row, the dose days, the snooze, the tails, the arrival record and the checkpoint. The two identical canonical-slot write blocks of the loop are one function now. The route falls from 1044 to 231 lines; no function in the new modules is over 70.
The parity walk recorded before the move (medication-intake-bulk.parity) is unchanged. The per-entry external-id markers name the module that classifies each entry, the batch-surface and key-scope legs read the moved code, and the intake-tracking allowlist and the dense-read guard follow it.
A '/*' inside a line comment opens a block for every guard that strips
block comments first, and the code after it disappears from that guard's
view. Four server comments and one new one did this; reword them.
The guard stripped block comments before line comments, so a '/*' inside a
line comment opened a block that swallowed the code after it. The delta
feed's intake read sat behind such a comment and was never seen. The
shared stripper handles that order; with it the feed shows up, and it is
listed: it mirrors recorded rows and computes no rate.
GET and POST /api/medications/intake keep authentication, the query and body parsing with their refusals, the 404 and the HTTP mapping. The today list moves to lib/medications/intake-today.ts and the cached compliance buckets to intake-compliance.ts. The status update, one 432-line callback, moves to lib/medications/update-intake-status/ in the order it always ran: the event lookup, the record-only rule, the delegated refusals and the injection site (admission.ts), the take in place or onto another slot (take.ts), then the skip or snooze, the audit row, the rollups and the tails (update-intake-status.ts). The route falls from 634 to 149 lines; no function in the new modules is over 70.
The parity walk recorded before the move (medication-intake-status.parity) is unchanged. The sharing and activity-verb guards read the status-update modules beside the route, and the dense-read guard keeps the moved code in scope.
POST and GET /api/medications/{id}/intake keep authentication, the intake and ownership guards, the body and query parsing with their refusals, the delegated outcome-change refusal and the HTTP mapping. The dose write, one 607-line function, moves to lib/medications/record-intake/ in the order it always ran: the injection-site check and the slot attribution (attribution.ts), the replay and dedup answers and the slot, probe or standalone write (write.ts), then the inventory, the audit row, the rollup, the lifecycle and the tails (record-intake.ts). The history list moves to list-intake-events.ts. The route falls from 767 to 200 lines; no function in the new modules is over 75.
The parity walk recorded before the move (medication-intake.parity) is unchanged. The activity-verb guard now reads the awaited audit actions of a delegable write's handler modules beside the route, for this route and for the medication create that moved before it, and holds each module to its route's imports; the sharing guard, the intake-tracking allowlist and the dense-read guard follow the moved code.
GET /api/measurements/series keeps authentication, the query and its
refusal; readSeries (lib/measurements/series/read-series.ts) picks the
reader in the old order (sleep nights, paired blood pressure, the bucketed
read of a dense kind, the raw rows) and adds the direction, change,
reference lines and stat strip. The kinds, units and point shape live in
kinds.ts. The route falls from 752 to 95 lines.
The parity walk recorded before the move is unchanged. The guards on the
verdict resolver, the spread, the pulse day mean and the unit literals read
the new files, the last two with their scope widened to the series folder.
GET, PUT and DELETE /api/medications/{id} keep authentication, the ownership guard, the body and the HTTP mapping. The read moves to lib/medications/read-medication.ts; the edit, one 886-line callback, moves to lib/medications/update-medication/, split along its stages in the order they always ran: the preflight checks and refusals, the schedule normalisation, the transaction (era archive, tombstones, the row update, the pause era, the course write) and the commit tail with its audit row. The delete moves to delete-medication.ts. The route falls from 1270 to 100 lines; no function in the new modules is over 115.
The parity walk recorded before the move (medication-detail.parity) is unchanged. The sharing guard reads the edit and delete modules for the handler body, the intake-tracking allowlists name the modules that now read the schedule and the open slots, the published-DTO leg reads the read module, and the dense-read guard keeps the moved code in scope.
The backups section now hands its flow to the download, restore and
upload hooks. Each caller entry may name the file that sends the
request; the component still owns the flow and the dialog and must
import that hook and hand it the flow.
The HealthChart split moved its layers, tooltip and day footer into
health-chart/, so the guard read the composer as a date-axis chart with
no doors. A component and the folder of its name are now one entry, a
tooltip action handed whole to the tooltip counts as wired, and the mount
floor counts the dashboard's one builder mount for its seven charts.
Drop two return-type exports nothing imports.
GET keeps authentication, the module gate and the AI capability, and hands
the rest to readWorkoutDetail (lib/workouts/detail.ts): the stored row, the
canonical winner of its cluster, the enrichment reads and the body, as
named stages. DELETE keeps authentication and the HTTP answers; the
ownership check, the synced-source refusal and the transactional removal
with its records, audit, cache and detection tail move to
lib/workouts/delete-manual-workout.ts.
The parity walk recorded before the move is unchanged; the guards that name
the canonical-identity reads and the track readers point at the service.
A track stored before the object form holds one ISO instant per point, and the detail answers it as stored. The route-sample declaration accepts both forms.
WorkoutRouteGeometry.sampleTimestamps was declared as an array of strings. Both track ingests (the workout batch and the Apple Health export) store one object per point, { t, speedMs?, hr? }, and the detail answers them as stored. The response is unchanged; the contract now says what it holds.