A per-field registry of the answers the server resolves for the record on
screen (the blood-pressure target band, intakeActionable and courseStatus,
the AI capability, the module surfaces) and the ingredient shapes that
rebuild them in the browser: getBpTargets() on the viewer's age, a dose
offered from active and trackIntake, course state from endsOn against the
clock, the raw disableCoach column, server resolvers imported into client
code. A match in client code fails unless the file is allowlisted with the
reason it is not a rebuild; the switches that edit a stored value are the
allowlisted readers.
The walk carries a floor, every pattern is checked against a sample of the
shape it catches, a stale allowlist entry fails, and the total match count
must be non-zero. Putting back each of the three rebuilds this release
removed fails the guard.
- /api/auth/me and the AuthUser type described disableCoach as the gate
every Coach surface checks below a flags.coach short-circuit. Neither has
been true since the AI capability resolution: it is the account's stored
opt-out, and surfaces read ai.capabilities.coach or modules.coach.
- /api/mcp/tokens claimed /api/tokens mints medication:ingest; that mint was
removed, and the medication pair now comes from the per-medication
endpoint toggle.
- /api/meta/capabilities listed the token kinds as wildcard, medication
ingest or MCP, missing measurements:write, workouts:write and
documents:write. Comment only: the served scopeMintable answer for the
FHIR face is unchanged and still right.
A PDF longer than the pages a scan renders was read from its first ten
pages, and the cut was recorded only in the wide event. The person checking
the extracted rows had no way to know that values on later pages were never
read.
The rasterizer now returns the PDF's page count, the extract response
carries pageCoverage { read, total } when the cap applied, and the review
screen says how many of the pages were read.
A segment whose stage label was unknown (SLEEP_STAGE_TYPE_UNSPECIFIED, or a
label added upstream later) was skipped without a trace, so the night total
read short and nothing said why. The mapper now counts those segments and
keeps their labels, and the sleep sync puts both on its wide event.
The grid change wrapped the heading in a cell div, which separated it
from the description paragraph the settings-score spec reads as its
following sibling. The heading is the grid item itself now.
An unknown or archived tag key, or a factor the catalog no longer carries,
was dropped on the way in while the entry still reported "inserted" (bulk)
or 201 (create, edit). A client had no way to tell that part of what it sent
never landed.
Each write now names those keys: the bulk result per entry, the create and
edit responses on the entry, as droppedTagKeys and droppedFactorKeys. Both
are present only when non-empty, so a response where everything landed is
unchanged. An archived tag an edit preserved is on the entry and is not
listed. The web shows a short notice after saving when a picked tag was left
off.
Settings → Coach hid its preference and nudge cards on the raw disableCoach
column, which /me publishes for the signed-in person. Inside a managed or
shared record that answered for the wrong person: a guardian with the Coach
on saw the tuning for a record that has it off, and the other way round.
The cards now read the module map the server resolves for the record on
screen, the same map the navigation and the settings entry already follow.
The three-button footer fits a phone row in English, so the English
case never saw Cancel leave the sheet. A German case at every width
fails against the old footer (checked) and passes against the fix.
The intake pickers chose their default from every active medication, so a
course that had ended but was not archived could be the preselected dose.
The default now reads the server's intakeActionable answer; the medications
page dialog still lists ended courses for back-filling a past dose, they are
just never the default.
Both pickers also graded "due" against the fixed 120 / 240-minute defaults
while the cards beside them used the account's own late and missed
thresholds. They read the same thresholds now.
The blood-pressure chart rebuilt its target zones in the browser from the
signed-in account's date of birth. A band the person had set themselves never
reached the chart, and inside a shared or managed record the zones followed
the viewer's age instead of the record's.
The targets payload already resolves for the record on screen. Its blood-
pressure card now honours the user's own band too (labelled Custom, like the
weight card), and the chart reads its zones from that card, so the chart and
the reference panel under it name one band.
The lab list panned sideways on a phone while the overflow guard was
green, because /labs was not on its hand-kept list. The new sweep reads
the routes from src/app: static routes are visited as is, every dynamic
route needs a resolver to real paths, and routes that are not signed-in
surfaces are listed with their reason. A route added without either
fails the bookkeeping test by name.
It runs on an account of its own, filled once from globalSetup with one
or two of everything a route renders, in German, so the narrow-column
and long-string cases are present. Two lab tests assert at the elements
that broke: the marker name keeps a readable width, and the values
list's actions stay on screen. The dialog spec gains a 360px width,
where the labs sheet's three-button footer overflowed in English too.
Three cards put a full sentence into the SettingsCardHeader status slot
(mood reminder, low-stock warning, the app-log refresh button), which
does not wrap: the row grew to 1000px and /settings/notifications
panned sideways at every width. The sentence moves into the card body,
and the slot is capped at the row so a long status wraps instead.
Also on a phone: the provider-chain add control wraps, the report
picker's standard-template action drops under its sentence instead of
running past the card, and the dashboard layout rows put their switches
and arrows on a second line so the tile name is no longer cut to five
characters.
With three or four icon actions beside the title, the description was
left a ~130px column that wrapped five or six times. Below sm it now
spans the row under the title and actions; from sm up the actions span
both rows exactly as before.
A one-word German title wider than the column beside the actions ran
underneath them; the heading hyphenates by the page language and breaks
as a last resort. Notifications, whose two actions are text links,
moves them under the description on a phone (stackActionsOnPhone).
A reference window of 0-130 padded the axis to -13, a tick that reads
like a value for a concentration that cannot be negative. The pad only
goes below zero when the data already does.
The compact list was a five-column table at every width. Below the fixed
columns (badge, 12rem range bar, 72px trend, chevron) the name column
collapsed to nothing, the badge sat over the value and the row panned
sideways off the card. Below lg the same cells now fold into two lines
per row; the desktop subgrid is unchanged.
The values table needed ~510px, so on a phone its edit and delete
buttons sat in a sideways-scrolling strip and the range bar labels were
clipped. Below lg each reading is a stacked row with its actions beside
it.
The add sheet's three footer buttons did not fit a phone row in German
and pushed Cancel off the left edge; the secondary save takes a row of
its own there, and the footer wraps instead of overflowing on desktop.
The scan review's 'links to existing marker' chip truncates instead of
running past its row.
Every queue bound with createAndWork is now declared in
src/lib/jobs/queue-runtime.ts as short (cannot come near the default
fifteen minutes, with the reason), budgeted (stops itself at three
quarters of the default and resumes) or long (an explicit expiry, a
stop on the job's budget, and a lock or claim per identity; or a
declared one-shot with no retries).
The guard reads every binding, every boss.send and every schedule tuple
in the tree: each names a queue in the table, every entry is bound, a
long queue's sends and schedules all carry its expiry, its handler
stops on jobBudget / jobDeadline where declared and holds lockedPass or
the named marker, and a short or budgeted queue sets no expiry of its
own. Floors pin the counts (112 bindings, 64 sends, 74 schedules), and
planted long queues missing each protection in turn are caught. Run
against the tree before the previous commit it reports 59 findings;
dropping lockedPass and the cron expiry from pr-detection reports both.
pg-boss gives a job fifteen minutes unless the send says otherwise,
then fails it and retries it while the handler keeps running. Several
passes can legitimately need longer and had no protection at all: the
seven nightly status passes and the pre-generation (a provider call per
account), the period-narrative warm, the record-detection cron (every
account's history, every thirty minutes), the key rotation (every
encrypted column), the content-index backfill (up to 200
transcriptions) and the on-host backup.
Each of them now carries an explicit expiry on every send and schedule,
stops between units once three quarters of it is spent (jobBudget), and
runs under a session advisory lock keyed on queue and identity
(lockedPass), so the retry pg-boss starts after the expiry does nothing
beside a run that is still going. The lock is the one the full-history
imports already held, moved into job-lock.ts.
Where a pass stops early it says so (stopped_early) and the next run
continues: the status passes and the narrative warm leave the rest to
the next night, the pre-generation and the narratives walk the stalest
accounts first, the on-host backup now backs up the oldest copies
first, the key rotation reports a partial run in its audit row and
skips rotated rows when run again, and the dose-history import fails
the delivery so its retry resumes from the committed chunk. The
off-host backup, which already stopped on its budget, gets a four-hour
expiry and the lock.
The narrative, the workout distance and the elevation chart converted
through the unrounded escape hatch, and the trend caption scaled its
noise floor off the raw factor. They now take the rounded helpers, so a
narrated pound or mmol/L figure reads at the grain every screen shows
it, and the caption's floors are declared canonically (the distance
floor in metres). The trends row joins the chart-feeding surfaces that
may hand a raw scale to a chart.
Refs #1067
The unit-preference guard covered the dashboard, the insights pages and
the target surfaces, and the class kept turning up beside them. It now
also walks src/components, src/app, src/lib/insights and the doctor
report and export modules for the glucose units anywhere in code and
for kg, km and °C in display strings, with comments stripped. Each file
that legitimately carries one is listed with its exact line budget and
the reason: the conversion and the pickers, input parsing, display-kind
tags, canonical wire and registry units, the SI policy for clinician
artefacts, and four prompt texts still to move. The sweep asserts a
match floor, so a broken walk cannot pass as a clean tree.
Refs #1067
The cycle-phase table printed the bundle's mg/dL label beside raw mg/dL
values for every reader. Weight, temperature and now glucose rows take
their unit and conversion from the one reading resolver; every other
row keeps its bundle label.
Refs #1067
Two more surfaces carried a fixed unit. The insights trends row labelled
weight in kg and body temperature in °C for every reader, and plotted
walking distance as raw metres under a km label; its trend caption
said the same. A single transformed series there now takes unit, scale
and offset from the preference, and the caption describes the converted
series against a converted noise floor.
The illness day sheet and timeline showed and took a fever in °C. The
fever is now seeded, typed and shown in the reader's temperature unit
and stored in °C as before.
Refs #1067
The workout list, the detail header, the stat tiles, the sport-average
line and the elevation chart printed kilometres, minutes per kilometre
and metres for every reader. They now read distance, pace and elevation
through the unit registry (km or mi, /km or /mi, m or ft). The splits
table keeps its kilometre rows, which are cut server-side, and says so.
A pace that rounded to sixty seconds now carries into the minute.
Refs #1067
The dense-read guard walked the worker and insight directories only,
and the same class kept turning up beside them: the targets page, the
cycle insights, the per-kind series and the doctor report all lived in
app/api or a helper next to it.
The findMany check now also covers app/api, import, export, the targets
and the doctor report. It reads the type from the where object rather
than the first type key in the call, accepts a list of sparse literals,
and counts a key-bounded where (an id, externalId or measuredAt list,
or an exact instant) as bounded. Every remaining unbounded read is
listed with its reason.
A second check covers every raw statement in the tree that reads from
measurements: it must GROUP BY, use DISTINCT or LIMIT, or select
aggregates only, or be listed. Both checks carry floors and run their
matchers over a planted offender. Restoring the old targets builder
fails the findMany check on the year of glucose and the heart-rate
month; dropping the GROUP BY from the resting-pulse fold fails the SQL
check.
The fasting-glucose pillar served its observed and reference labels as
"Fasting median 99 mg/dL" and "70-99 mg/dL fasting" whatever the
account's unit, and clients print those labels as they come. They are
now written in the record's glucose unit. The scored value, its unit
and the band edges stay canonical, so the score does not move and the
web, which converts from the value, cannot convert twice. The schema
now says which fields are canonical and which are display text.
Refs #1067
The sleep list capped its read to the last year only when no `from`
was sent. An explicit early `from` walked every per-stage row back to
that date, on a multi-source account several rows per night for years.
The read window is now at most a year ending at `to` (or now). `from`
narrows it but never widens it, older nights are reached by moving
`to` back, and `meta.windowFrom` names the instant a cut window
started, so a client can tell a cut from an empty history.
The settings threshold editor took the identity path for the glucose
thresholds, so an account on mmol/L was shown a mg/dL default and asked
for mg/dL figures against 40-400 bounds, while the insights sheet for
the same threshold already spoke mmol/L. The shared target adapter now
owns the glucose unit as it owns pounds: fields, inward-rounded bounds,
the default hint and the canonical write all go through it, and the
insights panel and its sheet use the same branch instead of their own
conversion.
Refs #1067
The report's dense exception covered pulse and glucose. Heart-rate
variability and blood oxygen were read raw for any window, and a watch
or ring writing them every minute puts six figures of readings into a
long report that draws them as one sparkline.
Their report sections are a statistics row and a series, the same as
pulse, so they take the same rule: beyond 90 days per local day, and
inside 90 days per local day once the window holds more than 10 000 of
their readings. One grouped count decides each type on its own. Count,
minimum, maximum and the latest reading stay exact; the mean is the
readings' mean. Glucose keeps its own rule because its clinical panel
needs the per-reading counts.
The bucket query now takes a list of types instead of two flags.
The period narrative pinned kilograms and mg/dL on its change list, and
its personal-range lines carried no unit at all. Every delta and band
edge is now stated in the reader's units (computed canonically, converted
at the end, percent left unit-free), and each band transition carries
its unit into the prompt. The sleep channel, a per-night total in
minutes, was labelled hours and is now converted to them.
Refs #1067
The route test mocked Prisma and asserted a raw glucose findMany. The
glucose and resting-pulse inputs now come from their SQL readers, so the
test mocks those, feeds the pinned response through the in-memory fold,
and asserts that neither stream is read as rows.
The per-metric status note quoted glucose in mg/dL, body composition in
kg, temperatures in °C and walking distance in metres, whatever the
reader had chosen; the deterministic floor without a provider did the
same. The generator now converts the series, the reference band and the
signal into the reader's units before they reach the prompt or the
floor, and prints the guideline band table in that unit while still
classifying on the canonical value.
A changed glucose unit or metric/imperial preference (own account or a
managed record) now re-warms the stored notes and narratives through
the hash-gated refill, so today's text does not keep the old unit.
Refs #1067
The dense rule in /api/measurements/series covered pulse and glucose
only. Heart-rate variability and blood oxygen read every raw row for
any window up to 3650 days, and a watch or ring that writes them every
minute makes that the same unbounded read pulse had.
Both kinds now fold per local day beyond 90 days and per local hour
inside 90 days once the window holds more than 10 000 readings. A
bucket keeps the point shape of its kind: only pulse carries
valueMin / valueMax. Statistics stay computed over every reading, and
a sparse series inside 90 days is unchanged. The OpenAPI description
names the four kinds.
GET /api/cycle/insights read a year of raw readings for its outcome
channels, glucose and heart-rate variability included. On a sensor
account that is over 100 000 objects per request.
The crosstab uses each local day's sum and count after picking one
source and device per day, so the route now reads readSourceDayAggregates
like the coach's cycle snapshot does, and each row carries its reading
count into metricDayMap.
The integration test runs the route over the fold and over every raw
reading handed in as a one-reading group, and compares the answers.
Metric/imperial and the glucose unit were two separate lookups, so every
server-side text producer had to remember both and most remembered
neither. display-transform.ts now carries a glucose branch keyed by the
glucose preference (getReadingTransform), the metre-valued workout
quantities (distance, elevation, pace), and a resolver for the two
stored columns together. loadUnitPreferences reads them in one query and
useUnitDisplay hands the glucose unit out beside the metric/imperial
branch.
Refs #1067
The targets page read a year of raw glucose readings to keep the newest
one per meal context, and thirty days of raw heart rate to derive one
resting figure per day. A sensor reporting every five minutes puts
105 000 glucose readings in that year and a watch recording once a
minute puts 43 000 heart-rate readings in the month, and the page is
read on every visit from the web and the app.
Glucose now reads one row per context (DISTINCT ON) for the newest
value and one row per context and local day for the thirty-day mean
and the consistency strip; the strip takes day sums directly through
rollupConsistencyFromDays. Heart rate reaches the resting-pulse card
through readRestingPulseProxy, the SQL fold the insights page already
uses, merged per day with the native resting rows exactly as before.
The cards are unchanged: the integration test rebuilds them from the
raw readings through the in-memory folds and compares.
The clinician view printed bare numbers for every measurement group and
the per-context glucose rows in raw mg/dL, so a recipient read glucose
without a unit and in the wrong one for an account on mmol/L. The PDF's
measurement table had the same gap for the raw glucose series.
Both artefacts now take the unit and the displayed number from one helper
(doctor-report/stat-display.ts): SI for mass, length and temperature as
before, hours for sleep, and the record owner's unit for glucose.
Refs #1067