Commit Graph
1265 Commits
Author SHA1 Message Date
Marc-André Bombeck baa1ba018d test(medications): pin the today list, the compliance buckets and the status update before they move
One walk through GET and POST /api/medications/intake: today's list with its projection and every status a row reads as, the compliance buckets cold and cached, a take in place, a take that re-binds to another slot, a pin and a pin that is no slot, a skip, both snoozes, the not-found answers, the record-only rule both ways, the injection-site refusal and an allowed site, the schema refusals, and a delegate marking an open dose and refused a snooze and a correction.
2026-10-23 14:32:52 +02:00
Marc-André Bombeck af53d3d6f0 test(medications): pin the per-medication dose write and history before they move
One walk through POST and GET /api/medications/{id}/intake: band, standalone, named-slot, pinned and backfilled doses, skips onto slots and onto a standalone dose, the dedup window, both idempotency forms and their replays, the delegated refusal on both write paths, the injection-site and pin refusals, a one-shot dose, the schema refusals, and the history list in its filters. The parity step can now rewrite an id carried inside a longer string, and a refusal's unawaited audit row is waited for on the real timer so the pinned clock does not move.
2026-10-23 14:21:24 +02:00
Marc-André Bombeck e8510ae167 test(medications): pin the medication read, edit and delete before they move
One walk through GET, PUT and DELETE /api/medications/{id}: the read with stock, composition and courses, every edit branch the handler tells apart (schedule replace and echo, the grace bridge, pause and resume, the category rules, the record-only rule and both tracking transitions, the as-needed and one-shot refusals, the course refusals, a composition change), the refusals, and delegated access at each level. The parity step can now wait for a write the route does not await and read state outside the medication tables.
2026-10-23 13:59:41 +02:00
Marc-André Bombeck d24c544e98 test(analytics): pin the analytics response before moving its builder
A parity walk over GET /api/analytics on an account with a full record
(record-fixture.ts, reusable by the other read routes): an unknown slice,
the summaries slice and the default body cold and warm, the modules off, a
second language with a personal blood-pressure target, an empty account and
an anonymous call. Background work a request leaves running is settled
before the next one. The blood-pressure fast path's rollup row counts are
left out of the pinned wide event: they follow the rollup tier's coverage,
not the code, while the answer they fed stays pinned in the body.
2026-10-23 13:49:24 +02:00
Marc-André Bombeck 905d4b167c test(insights): write the status goldens with sorted keys
The goldens held their fields in the order the code assigned them. They are now written with every object's keys sorted, so the text depends only on content and diffs and compresses well; the parsed content of every golden is unchanged. They carry no clock reading or generated id: the clock is pinned and the ids are fixed.
2026-10-23 13:42:58 +02:00
Marc-André Bombeck 3f8c2a98dd fix(measurements): answer the day drill-down without the sealed note
The dayKey drill-down of GET /api/measurements answered the stored rows as
they were, notesEncrypted ciphertext included and the note itself unread.
It now selects the contract's columns by name and shapes each row like the
plain page: the note decrypted onto notes, the ciphertext never on the wire.

A route test walks the measurement reads over rows with a sealed note
(the page, the drill-down, the day-grouped list, an aggregate, the detail)
and fails on any sealed field; it failed on the drill-down before this
change. The parity walk drops its contract exemption for the drill-down.
2026-10-23 13:41:11 +02:00
Marc-André Bombeck 7b9737c7b1 test(kit): refuse a sealed field in any response the route kit drives
callRoute now fails a call whose JSON body carries a key ending in
Encrypted, or bytes serialised as an index-keyed object or a Buffer, on
every route it drives and regardless of the contract exemption. A sealed
column is decrypted or dropped before a response is built; one that leaks
is a defect, whatever the contract says. The kit's own test proves the
check finds both shapes and leaves an hour map alone.
2026-10-23 13:41:11 +02:00
Marc-André Bombeck 0e0e661cf0 Merge branch 'feat/v146-server-insights' into feat/v146-server 2026-10-23 12:48:08 +02:00
Marc-André Bombeck f942389a4b Merge branch 'feat/v146-server-meds' into feat/v146-server 2026-10-23 12:41:47 +02:00
Marc-André Bombeck 069e07b510 Merge branch 'release/v1.46.0' into feat/v146-server
The size baselines take the lower value of the two sides per entry, and every ratchet baseline is written back to what the tree measures now (file and function sizes, version stamps, source reads, import cycles, route body parsing).
2026-10-23 12:39:42 +02:00
Marc-André Bombeck 05b9697848 Merge branch 'feat/v146-server-status' into feat/v146-server 2026-10-23 11:47:13 +02:00
Marc-André Bombeck 8d48733c6a test(measurements): pin the measurement list and create before moving them into services
A parity walk over GET and POST /api/measurements: every create arm (single, combined, named sleep stage) with its duplicate and refusal, a bridge token, a delegated write, and every list mode. Sealed bytes are recorded by length, since each write carries a fresh nonce.
2026-10-23 11:47:09 +02:00
Marc-André Bombeck 8bb58d8760 test(medications): pin the medication list and create before the create moves 2026-10-23 11:44:26 +02:00
Marc-André Bombeck 2bfc5aee4b test(dashboard): pin the widgets layout route before moving its logic
Record the parity walk for GET, PUT and DELETE /api/dashboard/widgets on the
current code: the first and the cached read, a full save with ids outside the
catalogue, a partial save that keeps stored fields and the per-chart range,
the schema, empty-body and base-token refusals, a save on the current token,
the recovery-off availability probe with and without an HRV series, a
delegate's read and refused writes, the reset, and an anonymous call.
2026-10-23 11:39:48 +02:00
Marc-André Bombeck dd52a8d92a test(workouts): pin the batch ingest before moving it into a service
A parity walk over POST /api/workouts/batch: a mixed first batch with every per-entry refusal and the cross-source twin, the same batch with an enrichment, a corrected hand entry, a kept track, both whole-batch refusals and a narrow bridge token. Responses, wide events, queue tails and stored workouts with their series and tracks are pinned.
2026-10-23 11:34:11 +02:00
Marc-André Bombeck 4c9dc2be5b test(medications): pin the dose correction and undo before they move 2026-10-23 11:33:51 +02:00
Marc-André Bombeck 3df80389ab test(kit): record route parity snapshots, starting with the batch ingest
tests/kit/parity.ts pins what a route answers, stores and reports for a
fixed walk of cases, so the code behind it can move under a byte-for-byte
comparison: the response, the rows read back, the request's wide event and
the queue and arrival tails it asked for, with generated ids, server
timestamps and request ids replaced by stable placeholders.

The first walk covers POST /api/measurements/batch: a mixed first sync with
every per-entry refusal, the same sync again, a background day total, the
manual and HealthKit mirror merge, a backfill, both whole-batch refusals and
a narrow bridge token. The clock is pinned, so the snapshot holds on any day.
2026-10-23 11:19:46 +02:00
Marc-André Bombeck 78ac2c5aeb refactor(sharing): read and judge grants from a leaf module
lib/sharing/grants.ts held both the grant transitions and the predicates
every reader asks (state, level, scope, the live grant for a pair). The
transitions reach into the managed-profile lifecycle and the OAuth connect
target, and both of those read a live grant, so grants, lifecycle and
connect-target formed an import cycle.

The predicates, findActiveGrant and touchGrantUsage move verbatim to
lib/sharing/grant-access.ts, which imports only the database client and
the scope vocabulary. Importers and the mocks that replaced those
functions point at the new module. Runtime import cycles fall from 7 to 6.
2026-10-23 11:02:10 +02:00
Marc-André Bombeck 4308682c96 test(insights): pin every status card's prompts, results and stored notes in golden files 2026-10-23 10:52:49 +02:00
Marc-André Bombeck 0182f91092 Revert "feat(topics): compute topic overviews and topic pages on the server"
This reverts commit cc04e0944d.
2026-10-23 10:46:55 +02:00
Marc-André Bombeck 9de64cae3e Merge branch 'release/v1.46.0' into feat/v146-questionnaires
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
2026-10-23 10:20:34 +02:00
Marc-André Bombeck cc04e0944d feat(topics): compute topic overviews and topic pages on the server
GET /api/topics answers one entry per topic the record holds data for:
a headline value, a rule-based sentence and a 30-day sparkline.
GET /api/topics/{topic} answers a topic's signals with their usual
ranges, side-by-side series for a chosen window, the relationships
between its signals, 90-day trends, what it holds nothing for yet, and
its blocks (training load, lab markers with their trend, screener
trends, the cycle day).

Every figure comes from reads the metric pages already make: the local
daily values, the reconstructed nights, the mood score, computeLabTrend,
the training load and the screener trends. No model is involved. A
relationship says nothing below 14 paired days and is only called a
pattern at p < 0.05; a lab trend needs three readings spanning a year.

The preview is the viewer's own opt-in: with it off both routes answer
404 topics.preview_off. Topics, signals and blocks follow the record's
module switches and the sections the viewer's grant opens; the routes
read across sections, so a scoped grant is refused like the timeline.
/api/auth/me publishes the caller's opt-ins as previewFeatures.
2026-10-23 08:18:43 +02:00
Marc-André Bombeck 14273051db feat(doctor-report): mark screener totals answered in an unvalidated translation
The report data counts, per screener score type, how many of the
window's results were answered in a translation that has not been
validated (`screenerUnvalidatedTranslations`, read only when a screener
total is in the report). The PDF marks such a row with an asterisk and
says under the vitals table how many of how many read as orientation;
the clinician share view says the same under the row.

To keep the aggregator and the PDF section from growing, the wellness
summary and the per-context glucose rows move into helpers; the size
ratchets only fall.
2026-10-23 08:14:32 +02:00
Marc-André Bombeck 02cc0631fa feat(mental-health): record which wording a check-in was answered in
An administration now stores `wording`: `validated` (the published
validated version of the reader's language, the original for English),
`unvalidated_translation` (HealthLog's own translation) or `original_en`
(the English original, chosen by a reader of another language). The
client sends only `englishOriginal`; the server resolves the rest from
the wording registry, so a client cannot claim a validated wording the
registry does not hold. `locale` keeps the reader's language, which
still selects the crisis resources.

Additive migration 0475: a nullable column with no backfill, since which
text earlier rows showed depended on releases since replaced. Every read
returns the field, the OpenAPI contract carries it and `englishOriginal`,
and the disaster-recovery backup carries and restores it (the parity
goldens gain the column; their pinned timestamps move by one step
because the fill writes one more column).

The PHQ-9 item-9 crisis signpost is answered from the answers alone and
is proven in every wording through the real route.
2026-10-23 08:14:15 +02:00
Marc-André Bombeck 333dfe8746 fix(backup): give every export read a total order
Several export reads sorted only by a column two rows can share (a
creation time, a day, a name), and a few to-many reads (medication
schedules, mood tag links, cycle and illness symptom links) had no order
at all. Tied rows then came back in whatever order they sat on disk, so
two exports of one account could differ, and the parity goldens flapped
once the fixture's child rows shared their pinned timestamps.

Each read now ends its order in the model's primary key: the existing
sort key plus `id`, or the remaining key columns of a compound key
(nutrient `source`, hidden tag `moodTagId`, link `symptomId` /
`moodTagId` / `documentId`). Schedules sort by window start, then id.
The restore re-export reads through the same modules, so it inherits
the order.

The parity goldens are unchanged: they already hold the tie-broken
order, so no regeneration was needed and they still pin the output of
the pre-split exporter.

A new integration test runs both purposes, built and streamed, through
a client that records every findMany and every nested to-many read, and
fails on any whose order does not cover the primary key (keys pinned by
an equality in `where`, or by the foreign key to the parent, count as
covered). A second case writes the same tied rows in forward and
reverse physical order and requires byte-identical files.
2026-10-23 00:28:38 +02:00
Marc-André Bombeck b09a6d2df8 test: fail a test whose code caught and logged a missing mock export
A module mock that leaves out an export the code under test reaches throws
at the call, and where that code catches and logs (a best-effort tail, a
fail-soft read) the test went on green while the path it meant to cover
never ran. The Apple Health import tests mocked the rollup module without
its fold window, so the import tail's rollup fold and cache invalidation
never ran in any of them; four route test files mocked the logging context
or the rollups without what the measurement tail reaches.

Both setup files now watch the console and fail the test in which such a
line was logged. The ECG import test keeps the real rollup module and
stubs only the fold, and the four unit mocks gain what they lacked.
2026-10-22 23:15:43 +02:00
Marc-André Bombeck 2844088251 test(integration): seed the large pulse account a month at a time
The heap test for the pulse day-mean rebuild wrote its million readings in
one INSERT ... SELECT, which outran the 60-second statement timeout on CI's
shared Postgres. The fixture is now written a month per statement, each in
its own transaction with the timeout lifted for that transaction only, and
ANALYZE the same way; the code under test keeps the timeout, and the
production-heap assertions are unchanged. With the timeout at 4 seconds the
old seed is cancelled with 57014 and the new one passes.
2026-10-22 22:56:05 +02:00
Marc-André Bombeck 70c1077121 perf(mood): write a rollup fold in one statement per chunk
The full mood fold upserted every day bucket with a statement of its own,
so the read that refreshed the tier paid one statement per day of mood
history. The dashboard summary in the hot-route budget fixture did exactly
that: 57 to 59 single-row inserts out of its 85 to 87 statements, a count
that moved with the calendar the fixture is dated against, which is why it
crossed its budget of 84 on CI without a new query in the route. The fold
now writes each chunk of up to 500 buckets as one multi-row upsert, and the
route's budget goes to the measured 28.
2026-10-22 21:31:28 +02:00
Marc-André Bombeck 239319a762 fix(mood): count the mood block's ages in calendar days, and fold today in every zone
The Coach's mood block gave its newest and oldest entry ages as elapsed
hours rounded to days. A point from the rollup tier sits at its day
label's UTC midnight, so from noon UTC on this morning's entry read as a
day old; the integration run failed at 19:02 UTC for that reason. The ages
and the span are now whole days between the entries' own day labels and
the reader's today.

Pinning the clock to the edges of the Berlin day showed a second gap: the
open-ended rollup fold stopped at now, and a day label runs up to fourteen
hours ahead of UTC, so between a reader's local midnight and UTC midnight
the fold left their today out and the tier called yesterday the newest
day. The open-ended fold now reaches a day past now.
2026-10-22 21:23:19 +02:00
Marc-André Bombeck 811fcf5509 fix(backup): restore row stamps and carry sealed labels, course notes and marker context as ciphertext
A disaster-recovery restore wrote these rows stamped with the restore's own
time: the medication's updatedAt (moved again by the course window), the
category assignment, custom category labels, courses and composition, the
mood day context and tag links, illness and cycle symptom links, nutrient
days, and a flare's updatedAt after the parent pointer pass. The recovery
file now carries each stamp (additive fields: linkCreatedAt on a mood
entry, symptomLinkCreatedAt on a cycle day log, createdAt on an illness
symptom, categoryUpdatedAt on a medication, updatedAt where missing) and
the restore writes it back. A file without them restores as before.

A custom category label, a course note and a marker's context rode the
recovery file as readable text and were sealed again under the active key.
They now ride as ciphertext (labelEncrypted, noteEncrypted,
contextEncrypted) and come back as the same bytes; a file that carries the
readable value instead is sealed on the way in, as before. The portable
file keeps the readable values. The rotation script and the
encrypted-column writer guard already cover all three columns.

The parity golden copies change for exactly this: the new fields in the
recovery file, the restored stamps in place of <now>, and the three values
coming back under the key they were sealed with. The legacy readable coach
title and custom metric note are still sealed on restore, since the row
holds no ciphertext to carry. The round-trip column audit no longer
excludes the four link stamps.
2026-10-22 19:57:47 +02:00
Marc-André Bombeck 298515d156 ci: fail the mutation floors step, and scale nightly property runs by a factor
The floors step piped scripts/mutation-floors.mjs into tee without
pipefail, so it took tee's exit status and a module under its floor could
never fail the run. It now runs under bash with pipefail, as do the other
steps that pipe into tee, and a guard reads every workflow and fails on a
tee pipe without it. The other piped steps were checked: each either
validates what the pipe produced or pipes a command that cannot fail.

The nightly runs set FC_NUM_RUNS, an absolute count that replaced every
property's own: the database backup round trip, which states twelve cases
for its cost, ran a hundred times against a timeout sized for twelve.
FC_RUNS_FACTOR now multiplies each property's count (five for the unit
suite, four for integration), propertyTimeout grows a test's timeout by
the same factor, and the backup round trip uses it.
2026-10-22 19:04:20 +02:00
Marc-André Bombeck 422adf2986 test(measurements): pin that a stats re-post with a new source version, device or sleep stage still writes
The unchanged-stats shortcut skips the write when every overwritten column matches the stored row. The kit test covered a new value and a new device; it now also re-posts a step total that only names a new HealthKit source version and a sleep row that only changes its stage, and checks each is written with its syncVersion bumped and the tail started.
2026-10-22 19:00:54 +02:00
Marc-André Bombeck 3b2c2c45f3 fix(backup): carry deleted family-history entries in the disaster-recovery file
Every record with a tombstone (allergies, labs, conditions, vaccinations,
life events, measurements and the rest) rides the disaster-recovery file
deleted rows included, so a restore gives back the account as it was. The
family history read only live entries for both files, so a restore lost
its deletions, and it carried the note as readable text that the restore
sealed again under the active key.

The disaster-recovery slice now carries every entry with its deletion and
the note as the ciphertext at rest, and the restore writes both back
verbatim; the portable file keeps the readable live entries. The parity
golden copies change for exactly that: the deleted entry, its note's key
and the shifted ids after it.
2026-10-22 18:59:07 +02:00
Marc-André Bombeck faedcbba93 test(backup): pin seeded timestamps, tombstones, self-references and key ids
The parity harness normalised any timestamp written during the run to
<now>, and the seeded rows' createdAt and updatedAt were themselves written
during the run, so a restore that dropped a row's createdAt still matched
its golden copy. Every run-time timestamp of the seeded account is now
pinned to a fixed historical value, and the seeded snapshot is asserted to
hold none, so a lost createdAt shows as <now> against a historical value.

The account also carries a tombstone of every model that keeps them, a
flare under its condition and a superseded profile fact, so the second
restore pass resolves real pointers. The fixture is sealed under the
legacy key and a second key is active from the export on; sealed values
render as the key id they name and the text they open to, so a value the
restore re-encrypts instead of carrying over differs.

The golden copies are written from the backup code as it stood before the
export and restore moved into per-domain modules (64fa65b46), and the
current code matches them.
2026-10-22 18:55:56 +02:00
Marc-André Bombeck ac31130bc0 fix(cache): mark measurement caches stale after every bulk write
Closed analytics blocks survive a write that says it reached no further
back, so a writer that never invalidated left them on their pre-write
values. The Apple Health and Health Connect imports, the CSV and JSON
imports, the score engines, the WHOOP webhook, the Withings activity, sleep
and ECG legs, the Oura history backfill and the sleep-segment sweep wrote
measurements without touching the read caches; a long-history import
showed the old all-time figures until the entries aged out.

The shared post-mutation tail now marks the caches stale back to the
oldest row it is handed, and the Withings and Oura legs that hand-rolled
the same tail run through it. The two export imports share one tail that
refolds the rollups, marks the caches stale back to the oldest sample
written (all of them when none was) and re-warms the status assessments.
A sweep that tombstones a sleep segment drops every cached block.
2026-10-22 18:37:25 +02:00
Marc-André Bombeck c15c1d18f3 fix(coach): read one lab marker's history in full
With an analyte, the Coach's labs block read the newest 4,000 lab rows of
every marker and matched the asked-for one in memory afterwards. An account
with that many newer readings of other markers lost the marker's older
readings, so its count and first date named a shorter history than the
record holds, or the marker vanished. The match now runs in SQL before the
row cap, on the biomarker's name for a linked row and the row's own name
otherwise, and the history compares units with sameLabUnit, so a reading
stored as "MG/DL" beside "mg/dL" stays in the series.
2026-10-22 18:22:36 +02:00
Marc-André Bombeck 466dfb3785 fix(workouts): resolve duplicate clusters the same way on every surface
The cross-source picker anchors each duplicate cluster on its first start,
so which cluster a workout lands in depends on the workouts around it. The
detail route still read the old fixed five-minute clock slot, and every
other reader handed the picker only the rows of its own range. The detail
of a twin that straddled a slot boundary declared itself canonical while
the list hid it, and runs at 09:58, 10:02 and 10:06 were grouped one way by
a read starting at 09:55 and another by a read starting at 10:00.

Every reader that decides which workout is canonical (list, detail and its
previous session, activity calendar, day view, doctor report, Coach,
briefing and correlation blocks, strain, per-sport context, the activity
insight's history, the batch write) now reads through one windowed reader.
It reads a duplicate window past the end of the range and reaches back
until every sport in the range starts at a workout that is an anchor
whatever came before it, then picks over everything read and keeps the
rows that start inside the range. The per-sport baseline and the insight
history leave out the whole cluster of the current session instead of the
row alone, which left its twin behind as a session of its own. The batch
write clusters its rows together with the stored workouts around them and
drops only a row that loses to another row of the same batch.

A property test checks the windowed read against a whole-history read for
random dense histories and ranges; route tests pin list, detail, deep link,
previous session and the batch write against the database.
2026-10-22 18:21:02 +02:00
Marc-André Bombeck 9c8972bc96 Merge origin/main (v1.44.2) into fix/v145-sheet-motion
next stays on main's 16.4.2 (the RCE and Server Actions advisories) with its two companions; release's newer dev tools and its own additions are kept. The lockfile is release's, re-resolved. pt-BR, which main does not have, takes the one key main added.
2026-10-22 16:43:28 +02:00
Marc-André Bombeck 190a83c513 feat(coach): read lab results over the whole record
The Coach read lab results from the last twelve months only, even with
the lookback set to its widest option, so a slow change in LDL or PSA
across years stayed out of reach. Labs are sparse, so they are no
longer held to the one-year window that bounds the dense readings: they
reach back over the whole record, and a lookback limit the person set
still applies.

The snapshot block keeps the latest value per marker, now over that
wider window, at its old size: it shares the prompt's size cap and is
never shed. get_labs with an analyte adds each matching marker's
history, summarised so it stays bounded: the newest five earlier
readings in full and older ones as one mean / min / max / count per
year, ten years at most, older years counted. With a heavy fixture
(30 markers, quarterly for 20 years) the overview stays at 4.3k
characters and a named LDL read grows from 0.2k to 1.2k characters for
80 readings. The MCP get_labs read and the lab resource follow, since
they run the same tool.

The widest lookback option no longer reads "All": it names what it
reads, twelve months in detail and every lab result, in every locale.

Refs #1263
2026-10-22 10:50:56 +02:00
Marc-André Bombeck 6a2247e422 fix(healthkit): count a type as arrived only when the batch brought something new
A batch that carried a type with duplicates only moved its
lastReceivedAt, lastTrigger and lastBackgroundAt, so a type whose live
delivery had stalled kept reading as freshly arrived for as long as a
replay, a retry or an older client re-sent its old samples.

The arrival ledger now moves those stamps only for a type the batch
brought at least one new or changed row for. An unchanged stats:
re-post and an hour posted again with its stored value count as
duplicates here; the wire keeps answering updated for them. A
duplicates-only type stamps the new last_seen_duplicate_at column on
its existing row and nothing else. The intake bulk route's dose entry
follows the same rule through the shared recorder. lastSyncedAt and
healthKitLastSyncedAt are unchanged.

GET /api/integrations/healthkit serves the stamp additively as
metricFreshness[].lastSeenDuplicateAt, and the Apple Health card names a
re-send that came after the last arrival.

Refs #1259
2026-10-22 10:34:45 +02:00
Marc-André Bombeck 90106d88c8 fix(dashboard): shade blood pressure, pulse and body fat from the own target
The dashboard bands took only the weight target into account. Blood
pressure, resting pulse and body fat were resolved from age and sex
alone, so a target set on /targets left the chart zones and the tile
ranges on the standard band.

buildDashboardBands now reads the stored targets blob and routes every
override through getEffectiveRange, the same resolver the targets page
and the doctor report use. A metric without an override keeps exactly
the band it drew before. The snapshot's targetBands carry the resolved
values, so a native client reading them needs no change; the field
descriptions now say the own target wins.

Refs #1261
2026-10-22 10:23:14 +02:00
Marc-André Bombeck 703c92c776 test(integration): take the clock and the probe budget out of two verdict tests
The grant expiry case gave the grant 400 ms and slept 500: on a loaded
runner accepting, signing in and switching took longer than the grant
lived, and the grant expired before it was accepted. The grant now runs
30 seconds and the case moves the clock the resolver reads to the
instant the grant names, so the row still does not change and nothing
sleeps.

The key check's start-up probe is bounded at ten seconds and two per
query. Under load a probe query ran past two seconds and the check
answered inconclusive where the data says mismatch. The verdict tests
now run the check with time to finish; the cases that test the bounds
pass their own budget or sampler and keep them.
2026-10-22 10:14:03 +02:00
Marc-André Bombeck 0cefac9d68 Merge branch 'release/v1.45.0' into feat/v145-backup
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-10-22 09:59:37 +02:00
Marc-André Bombeck 6dfb7eada6 Merge branch 'release/v1.45.0' into feat/v145-runtime 2026-10-22 09:41:54 +02:00
Marc-André Bombeck 3524d7c289 test(integration): lower the hot-route statement budgets to the measured counts
/api/auth/me 18 to 13, dashboard snapshot 103 to 99, summary 86 to 84,
the 365- and 3650-day pulse series 6 to 5, the day view 48 to 47,
insights health status 60 to 59, correlations 44 to 43 and a 500-row
batch ingest 31 to 28.
2026-10-22 09:34:42 +02:00
Marc-André Bombeck 83661319d2 refactor(cache): keep the closed-block key helpers in a leaf module
Readers that tag a cell as a closed block no longer import the
invalidation module, which several tests replace wholesale. The cache
route test counts the slice's pre-fold cell apart from the route's two
cells, and the provenance test reads the shared pick's ORDER BY over its
grouped rows.
2026-10-22 09:09:44 +02:00
Marc-André Bombeck 4c86b68363 perf(import): skip the update of a re-imported row that is already stored
Re-importing an Apple Health export the account already holds sent one
UPDATE per row the insert skipped, about two million statements for a
large export, nearly all writing back what was stored. One read per chunk
now finds the rows whose stored values equal what the update would write;
they count as updated as before, without the statement. A key the chunk
carries twice is written in order as before. The natural-key rescue moves
into the same module.
2026-10-22 09:09:44 +02:00
Marc-André Bombeck f50b6b6637 perf(series): read a dense window's rows once for buckets and figures
A long-window read of a dense series sent two statements over the same
canonical rows, the day or hour buckets and the strip's figures, so a
ten-year pulse read picked the canonical source per day and scanned the
readings twice. Both aggregates now run in one statement over the window
materialised once, with the same SQL each had (dense-series-read.ts).

An integration test keeps the two old statements as the reference and
requires the same buckets and figures for pulse (two ranked sources),
glucose and oxygen saturation, day and hour grains, in UTC,
Europe/Berlin, Asia/Kolkata and America/New_York, and the same empty
answer for a type without readings.
2026-10-22 09:09:42 +02:00
Marc-André Bombeck 4fc6b0e386 Merge branch 'release/v1.45.0' into feat/v145-backup 2026-10-22 08:12:22 +02:00
Marc-André Bombeck 5a9a6ce78b Merge branch 'release/v1.45.0' into feat/v145-deps 2026-10-22 07:54:27 +02:00