Dependabot treats `init`, `analyze` and `upload-sarif` as separate
dependencies, so it raised `init` to 4.37.7 in one PR and left `analyze` on
4.37.6 in the same workflow. The action refuses that pairing outright:
"Loaded a configuration file for version '4.37.7', but running version
'4.37.6'", and the CodeQL job failed on the very PR doing the bump.
All five pinned references move together here: init and analyze in
codeql.yml, and the three upload-sarif steps in scorecard.yml and trivy.yml.
The commit SHA is the one the v4.37.7 tag dereferences to, which is also the
SHA both Dependabot PRs pin.
The scenario purges every sleep row so last night can be the arrival that
flips the day. That left an account with no sleep source at all, which now
reads `final` on purpose, so the provisional assertion could not be reached.
Seeding a night from three days ago restores what the scenario means: a
record that does track sleep, waiting for last night's. Written through the
same ingest route as the fresh night and swept by the same purge.
The hero card on the start page told anyone without a sleep source, every
morning, that last night's sleep was not in yet. The freshness rule only
asked whether last night's sleep had landed, so a record that has never had
a night in it, or whose owner has stopped wearing the tracker, read as
permanently waiting. Not everyone owns one, and people put them away.
Waiting now requires that sleep is actually reaching the record: a night
within the last seven days. A flat battery or a few nights without the
tracker keeps the note, since tonight's sleep really is still expected. A
week of silence retires it.
The same rule settles the day itself. It used to park on `provisional`
forever for those records, which the daily push line reads too, so the
notification carried the same untrue "last night's sleep is still missing"
aside.
Two existing tests pinned the old behaviour by name ("is provisional when
sleep has never been recorded", "stays provisional when sleep never
arrives"). They described the defect rather than a contract worth keeping,
so they now assert the opposite, and a third covers the boundary between an
ordinary gap and a source that has gone quiet.
The preventive-care card kept postponing in the overflow kebab, three taps
from the thing a person actually wants when a due date does not fit: open
the menu, find the row, open the sheet. The medication card has never done
that — taken and skipped sit side by side on the card, one tap each — and
the checkup card should read the same way.
Postpone is a second button in the action row now, outline against the
filled primary, at its natural width beside it. The dense list branch gets
the same action as an icon button, so neither view offers what the other
withholds. The kebab keeps edit, measurements and delete.
The sheet behind the button is unchanged in what it offers (quick offsets,
a free date, the honest-skip zone), but the offsets are computed on the
calendar now rather than by adding 86 400 000 ms per day. Across a DST
shift the old arithmetic moved the wall clock over midnight and wrote the
neighbouring date, so a chip labelled "+7 days" could fill in the sixth or
the eighth.
The schemas stay: the insight result still parses recommendations
through them. Only the exported aliases went unused once the card that
consumed them was removed, and the dead-code gate says so.
The production dead-code check has been documented rather than enforced
since PR #691, because its list mixed real debt with false positives.
Triaged against the shipped tree, six of them are genuinely unreachable
and go now.
Five died when their host was retired and the leaves were left behind:
the recommendation card with its confidence meter and feedback control,
the grid that mounted them, and the suggested-prompts chip strip that
left the hero band in v1.18.7. The sixth, the comparison toggle, has had
two regression tests pinning its absence since v1.4.22, because the
preference it once set is a Settings concern now.
Deleting them freed sixteen translation keys in every locale, a pinned
entry in the success-affordance register, and two entries in the Coach
gate fixtures. Each of those was found by a guard rather than by
reading, which is the point of having them.
The adapter under signals/adapters stays: knip reports it as unreachable
because only a test imports it, but that test is the registry invariant
it exists to prove, and deleting it would remove a check rather than
dead weight.
Two findings from the deferred-audit list, both live since July.
A FHIR lab quantity stamped the UCUM system unconditionally while the
code was only added when the analyte map resolved a symbol for exactly
that unit. A receiver was told the unit came from UCUM and then handed
no UCUM code, which is a coding claim the bundle cannot back. System
and code now travel together, on the value and on both reference-range
bounds.
The doctor-report export accepted a selection that admits nothing. The
panel disables its own submit in that state, so only a hand-built
request reached it, and it rendered an empty artefact while spending a
rate-limit slot. This route has no documents-only mode that would make
an empty leaf list a legitimate scope, so it refuses with 422. The
share-link route, which does have that mode, is untouched.
The two halves of a resolved dose were split across routes: the single
intake route reset the medication's snooze but never closed the pending
dose-due Web Push reminder, while the bulk route closed the reminder but
left the snooze standing. Each path was missing exactly what the other
had. A dose drained from a device's offline queue lands on these routes,
so a synced take could keep ringing as snoozed or leave a stale web
badge behind.
The bulk route now nulls snoozedUntil once per affected medication when
a non-skipped entry actually lands, and the by-id route dispatches the
same fire-and-forget web clear the other intake paths have carried since
v1.18.4.
The two new measurement-reminder routes were admitted in the frozen
fixture but never got their integration drivers, so the matrix count
stayed at 75 and the executable proof was missing. Both routes now
carry a full manageContract driver: the skip driver asserts the stamped
column, exactly one SKIPPED ledger row, and that lastSatisfiedAt stays
untouched; the snooze driver asserts the snoozed-until write under the
same invariant. All three inventory pins move to 77.
Apple Health exports write ECG CSVs in two shapes. The importer only
understood the paired Lead,Voltage form; the single-column form (Lead
and Unit as metadata rows, a blank separator, then one bare reading per
line) failed every row as malformed and the whole recording was skipped.
The parser now treats a blank line after Lead and Unit metadata as the
start of the waveform, reads one bounded numeric sample per row, and
converts the declared unit to integer microvolts before the existing
amplitude and safe-integer bounds. The paired layout parses exactly as
before, and an unknown unit fails with a fixed message.
Refs #794
The rewritten notification-status description ran to two sentences and
130 characters — the card-description guard is right, the slot holds one
meta sentence. Folded to one em-dash sentence in every locale. The new
documents_retokenised job fact joins the outcome allowlist with its
reason.
The German bundle's 109 spaced en-dash pauses join the em-dash norm (the
two date-range placeholders stay ranges); the three straight-quote
stragglers and three mixed-quote strings take German quotes; the eleven
Bitte-Infinitiv validation errors speak du-imperative like the rest of
the app; the two wirklich confirms become bare questions; the
notification-status description trades its semicolon telegram style for
two sentences in every locale; the Withings setup action shortens to the
sibling single word. Twelve arbitrary text-[10px]/[11px] sites move onto
the text-2xs/text-xs tokens; chart tooltips and the vendor calendar stay
as they are.
The knip blind spot (ignoreExportsUsedInFile) left eight symbols carrying
an export nothing outside their file consumes — the modifier goes, the
symbols stay. The score series module goes whole: its only importer was
its own guard test, whose composite- and config-guarding suites survive
in place.
DocumentContentIndex.tokenizerVersion gains the consumer that was the
point of storing it: the content-index job now re-tokenises rows whose
version trails CONTENT_TOKENIZER_VERSION, from the decrypted index text
along the same path the rotation script uses, keyset-paginated and
fault-isolated per row — a future tokenizer bump migrates the index
instead of silently splitting it. ImportJob.uploadSha256 turned out to
already have its dedupe consumer (both kick-off routes short-circuit on
same-bytes non-failed jobs); its comment now names that instead of
pretending to be write-only. The remaining write-only columns carry
their honest annotations in the shape the audit sweep parses.
The entries gained a soft-delete this release; both backup ends follow in
the same one. A portable export omits tombstoned readings so a restore
cannot resurrect them; a disaster-recovery payload carries the tombstone
and the restore writes it back, so the account returns exactly as it
stood — undo affordance included.
Four small debts from the audit tail. The FHIR bundle's missing
Immunization resource gets the same written-decision paragraph the visits
exclusion carries — deliberate follow-up, not oversight. The service
worker's offline allowlist gains /api/daily/digest (both copies, policy
module and sw.js, plus the test) so the Today rail renders on an offline
reopen. The two Telegram plumbing tables that link to an account by a
bare userId string are frozen as a closed NO_FK_BY_DESIGN set in the wipe
guard — a third such table now fails the test instead of passing rule 1
silently. And deploy.md documents the manual digest-pin bump end to end:
imagetools inspect, the env update, and the three-probe /api/version
verification with the too-fast-deploy warning.
The restore dialog asked for its typed confirmation on nothing but a
filename and a date; the counts only surfaced in the audit row after the
rows were already replaced. A read-only summary endpoint now decrypts and
schema-validates the stored file through the same helpers the restore
itself runs and answers the summarizeBackup counts, and the dialog shows
them — per section, with an other-records line so a future summary key
can never silently vanish from the preview. A failed preview states its
own absence and never blocks the restore.
Refs A3-8. The per-item PHQ-9 / GAD-7 / WHO-5 / SCI answers have been
stored encrypted since the assessments shipped, and nothing ever read
them back — the clinical detail (item 9 included) was invisible. A new
by-id read decrypts them for whoever holds the mind read grant (the same
grant that already serves the mood diary's prose and the item-9 flag),
degrades to an honest items-unavailable state on any decrypt or shape
problem, and the history rows gain an expandable breakdown rendering the
official instrument texts the check-in flow already carries — nothing
retranslated, no alarm styling on item 9 beyond what the house already
does.
Rides along: the guard inventories learn the branch's new routes (frozen
counts moved with reasons), the reminder-engine test harnesses carry the
ledger delegate, and the custom-metric restore toast keys sit in their
right namespace in all six locales.
The hero card composition was fixed: score panel first, the worth-a-look
rail beneath. A reminder-driven person asked for the reverse. The stored
dashboard layout gains an optional hero field ('score' default,
'reminders' promotes the rail into the hero slot), validated as a closed
enum, preserve-when-absent on write, omitted from the blob at the default
so untouched accounts serialize byte-identically. The choice sits in
Settings beside the other hero controls; the promoted rail degrades to
the calm all-clear line when nothing is due.
Refs #778. Two honesty gaps from the first-run report: the web onboarding
presented Apple Health like a web connector although the connection only
completes in the iPhone app, and the initial backfill gave no sign of
progress, so a working first sync looked broken.
The onboarding source step now says plainly that Apple Health is set up in
the iPhone app after onboarding, without dead-ending the flow. The
integration card gains a server-authoritative progress summary — rows
accepted, oldest reading reached, and an honest flowing/waiting line
derived from the last accepted batch. Nothing the server does not know is
shown: no percentage, no ETA, no invented throttle state; a quiet stretch
reads as waiting for the phone, not as an error.
The reminder cadences were the oldest entry on the coverage-pending
register: carried in name, restored never — an account came back silent
until every reminder was rebuilt by hand. Both models are two-ended now,
in the house shape (both ends in one file, reminders-backup.ts): every
column travels, nextDueAt verbatim rather than recomputed, the completion
ledger beside it, tombstones in disaster recovery only, and a portable
export omits a tombstoned reminder together with its ledger rows.
Encounter.reminderId and VaccinationRecord.reminderId stop being dropped
on restore: both remap against the restored reminders and fall to NULL
with a skip report only when the file genuinely lacks the row. The wipe
plan names the ledger child-before-parent; the classification guard,
completeness guards and the integration round trip all demand the new
coverage (proven failing with the section removed).
Refs #223
Document search does whole-token equality over a blind index, so
'Blutdruck' never found the discharge letter that says 'blood pressure'
and 'hypertension' missed every German Arztbrief. A curated German/English
medical synonym list (vitals, common labs, imaging, specialties, document
words — static, no vendor names) now expands the QUERY tokens before they
are hashed: the stored index, its tokenizer version and the schema are
untouched, the expansion happens ahead of the one-way hash, and recall
simply widens. Typed tokens always survive the expansion cap.
Refs A3-11. Deleting a logged custom-metric value was the one hard delete
left on an entry surface — everywhere else a deletion tombstones and the
toast offers Undo. The DELETE verb now stamps deletedAt, every read
(list, latest-value, counts, revive check) filters the tombstone, and the
new entries/restore endpoint clears it scoped to owner and parent metric —
a foreign, live or mismatched id is a silent no-op, never an existence
leak. The history table's delete toast carries the Undo action.
The documents view already round-trips its filters through the query
string; the measurements and mood lists kept theirs in memory, so any
navigation dropped them. The documents pattern is extracted into a shared
hook (use-url-filter-sync) and both lists adopt it: type/source/day-bounds
and the measurement value range survive reload, back and sharing, defaults
stay out of the URL, invalid facets drop leniently. The insights readings
subpage keeps its locked type in memory — that URL belongs to the insights
route. /measurements no longer strips ?type=, so the deep links the
Vorsorge cards mint now persist as filter state.
Refs #223. A due Vorsorge item could only be done, deleted or ignored;
Telegram's postpone moved the date with no trace. Both decisions are
first-class now, and both are honest:
- POST /{id}/skip (no body): the interval restarts from the skip instant
through the recurrence engine, lastSkippedAt is stamped, skipCount
increments, any snooze clears. lastSatisfiedAt is NEVER touched — done
and skipped stay distinguishable forever, enforced in the primitive and
pinned by tests.
- POST /{id}/snooze ({until: YYYY-MM-DD}): the server resolves the day to
the notifyHour in the profile timezone and moves snoozedUntil and
nextDueAt to the same instant, so every due-state consumer follows
without knowing snooze exists. At least tomorrow, at most five years.
- GET /{id}/history: the new append-only completion ledger
(MeasurementReminderEvent), one row per satisfy from every engine path
and per skip, onTime derived at write time against the due instant that
was current when the event landed.
Two anchor fixes close the backdated-event gaps: the auto-resolve floor is
now the later of last satisfy and last skip (an old reading synced late no
longer resolves the cycle the user just skipped), and a backdated real
result re-anchors the next due at the later of its own instant and the
skip decision.
Screenings stay skippable and snoozable — the satisfy-side 409 guards
claimed fulfilment; a skip claims the opposite. Appointment reminders 404
on all three routes, in the lookup, like every by-id sibling. /checkups
gains the postpone sheet (quick chips, date field, the skip zone with its
own confirm) in both view branches, with muted meta lines and no card
colour change. The MCP preventive-care tool carries the resolved fields;
the OpenAPI contract publishes all three paths and the widened DTO.
Refs #611. The nap separation itself shipped in v1.34.4; what remained of
the report was the tooltip footer labelling a single night's total with the
orphaned 'Average per night' key (the headline card that owned it was
dropped in v1.22.0, so hovering one column claimed an average that was that
night's sum). The footer now names the main session's sleep, and on a day
with an inferred nap adds the whole-day total beside it — the Main sleep /
Naps / Total triple the report asked for. Percentages keep dividing by the
night alone.
Ten operations the spec owed: the seven mood-entries routes (list, create,
read, edit, delete, restore, bulk-delete) whose /bulk twin was already
published, GET /api/mood/linked-context (a cross-section read through the
mood URL that grant-scope reviewers need visible), and POST /api/import
(the spec documented polling an import job with no way to create one).
Every schema mirrors the handler's own Zod source; the envelope, refusal
and error semantics are read off the routes, not invented. Request-body
ids are bound instances now — a bare .meta() call clones in Zod 4 and
never reaches components.schemas.
Migration 0334, all additive. MeasurementReminder gains snoozedUntil,
lastSkippedAt and skipCount; the new MeasurementReminderEvent table is the
append-only completion ledger (kind SATISFIED|SKIPPED, occurredAt, write-time
onTime, source). CustomMetricEntry gains the deletedAt tombstone for undo
parity with every other entry surface. Consumers land in the sibling commits
of this branch.
Refs #223
- The record-settings contract, slug-list, and section-frame tests
follow the slugs that left the registry (layout children resolve
their headings from the layout namespace, like production).
- The orphaned settings.sections.<module>.title/description keys leave
all six locales; the reverse-coverage guard flagged them the moment
their nav entries stopped existing.
- The operator-cap ledger read fails open on a read error — the
reservation layer still meters against the same ledger, and a
transient blip must not take the fallback chain down.
- The reaper queue joins the subsystem surface; its healed count and
the sweep's nag_dismissed join the job-fact vocabulary.
- The intake-event contract fixture carries the full row the resource
now declares; the workouts empty-state test asserts the
source-neutral cue; the persister poll gates on rendered content
first.
- The tiered AI context's band reader now falls back to a live grouped
aggregate when a rollup band is empty (the module doc promised
replace-with-fallback from day one; only the replace half existed), so
a coverage-miss or never-folded YEAR partition no longer hands the
model empty history. Far-from-UTC users get the day-grain band keyed
on their own calendar (the same ±3h guard the correlation readers
carry).
- The slim analytics slice splices measurements older than the 5-year
fold window into its all-time count/min/max/mean: the rollup tier
only covers the window, so "all time" silently truncated for long
histories while the live fallback told the truth. A type whose only
rows are pre-fold no longer reads as never logged.
- account-sharing polls the offline persister's own IndexedDB store for
the flushed snapshot instead of sleeping past the debounce.
- The record-session-fence cases poll for the observable halves of their
setups (the switch round-trip landing, the exit switch being issued);
the one remaining fixed window is a documented absence-assertion grace
with nothing to poll on.
- The documents suite's two remaining sleeps are named for what they
are: a focus-dance grace the outer document cannot observe, and the
long-press gesture duration itself.
- The inventory-date fixtures derive their printed expiry from now
instead of a fixed 2027 date that would silently flip the fixture to
expired when the calendar catches up.
- The restore summary counts practitioners, encounters, the three link
tables, and vaccinations — restored in full since 08-01 but absent
from the report, so a file carrying visits or an Impfpass read as if
those sections were empty.
- A portable export that cannot open a field's ciphertext still nulls it
fail-soft, but now names the loss in the file's own decryptFailures
manifest — an export with a lost emergency note is no longer
byte-identical to one where the note was never written.
- A timezone change enqueues the medication-compliance re-fold: the
rollup keys its rows by the local day minted under the OLD zone, and
the coverage probe compares counts, not keys, so nothing else could
notice the stale bucketing.
- The measurement-reminder tick claims its slot in the record-event
ledger BEFORE any provider egress (the documented claim-first pattern
from reminder-dedup.ts); a crash between dispatch and the nextDueAt
advance can no longer double-send.
- mood-entries/bulk classifies a lost P2002 race as the duplicate it is
(with the winning row id) instead of a skip carrying a raw Prisma
message — the row IS stored, the racing twin wrote it. The workouts
batch already re-derives per-entry status from RETURNING.
- The vault list's content-index and thumbnail probes ride the same
Promise.all as the link loads: four independent grouped queries, no
serialized round-trips on a 4s-polled list.