Six suites pinned the old rule as the rule. Each inversion, and why it is
one:
`breadth.test.ts` — the case table asserted a refusal reason per set; it
asserts a tier and a `physiological` flag now. Every row in it except the
empty set used to be a refusal, and each is a number somebody gets to see.
Adds the tier boundaries (one pillar, two areas, physiological-absent), a
case proving the same pillars produce the same number at `partial` and at
`full`, and a block whose only job is the counter-case: the empty verdict
still carries a reason and no tier, and a composite whose selected pillars
all came back short still refuses with `no_usable_data`. A rule that has
stopped refusing anything is a deleted rule, not a graded one.
`composite.test.ts` — "requires three domains and one measured
physiological domain" asserted the refusal that no longer happens; it now
asserts the score and the basis block on the same fixture. The coverage
case keeps its fixture and its arithmetic and flips its outcome: two of
three used to be a refusal and is a scored account with a real coverage
fraction. Adds the one-area case and the zero-pillar counter-case, and the
`SCORE_VERSION` floor moves 2 -> 3 because widening who gets a score is
exactly the kind of change that must not ship without a version bump.
`record.test.ts` — "records nothing when the composite did not resolve"
used a two-pillar fixture to prove a below-the-floor day stored nothing.
That day has a number now and the table has to keep it, or an account that
starts narrow and widens later has a hole where its early days were. The
assertion is now that a one-pillar day builds a draft AND reaches the
database; the absence case survives on the fixture that genuinely produces
no score.
`composition-source.test.ts` — three recipes asserted as refused at read
time. Two of them score. What the block proves is unchanged — the reader
publishes the shared rule's verdict on the reader's own resolved set, so a
second copy of the rule would still surface here — but the agreement is
over a tier rather than over a refusal.
`route.test.ts` (score config) — the two too-narrow refusals become
acceptances, because a write that refuses what the read is willing to serve
is the two layers disagreeing. The empty selection keeps its 422 and gains
the assertions the old cases carried: errorCode, reason, and the sentence.
The errorCode driver loses the two selections that no longer 422.
`score-refusal-consumer-guard.test.ts` — its non-zero control anchored on
`three_domains_required`; it anchors on `no_pillars_selected`.
New: `health-score-basis-additive-contract.test.ts` pins that `scoreBasis`
is published, is optional on all three carriers, and points at the one
shared component — a required field on a cached surface would invalidate
every cached payload with nothing failing anywhere. It also pins that the
walk does not confuse the composite's basis with the older per-pillar
`scoreBasis` that blood pressure uses for its axis, since they share a key.
Fixtures: the notice-key assertions derive the method half from
`SCORE_VERSION` rather than restating 3. Three fixtures that named
`three_domains_required` now name `no_usable_data`, a reason the server can
still emit. The settings e2e drove its refusal through a two-pillar
selection that now succeeds; it deselects every pillar instead.
Registering `ko` in the `Locale` union left seven locale-keyed tables
without an entry, so the build could not type-check and the safety
screens, the citation-coverage grader and the reference-range parser
read nothing at all for a Korean reader.
Three failures were not missing keys but wrong assumptions:
- The shared fold decomposes with NFD and strips U+0300-U+036F. Latin
accents become plain letters, but Hangul decomposes into jamo, which
are not in that range and survived — so folded Korean text was a jamo
sequence no precomposed table entry could ever match. It recomposes to
NFC now; every Latin, Polish and German fold is byte-identical.
- `normaliseLabKey` kept `[a-z0-9]`, which reduced every Korean analyte
name to the empty string, so the derived index held none of them and a
Korean cholesterol result reached a FHIR export uncoded.
- The reference-range parser had the hyphen and the dashes but not the
tilde a Korean lab actually prints (`3.5~5.0`).
The Korean safety banks carry no `\b` next to a Hangul token — `\b` is
an ASCII `\w` boundary and Hangul is not `\w`, so `/\b증량/` matches
nothing — and the clause order is the mirror of the Latin banks, since
Korean closes with the verb. The dose patterns require a target-marking
particle for the same reason the Latin banks require "to"/"by": without
it the permitted restatement "7.5mg을 복용 중이고, 체중은 내려가고
있어요" tripped the lowering pattern.
The Coach and Insights prompts route `ko` down the reviewed English body
plus a Korean reply directive; there is no `safety-contracts.ko.yaml`, so
the native builder has no Korean ground rules to compose from.
`GET /api/insights/derived` accepts `windowDays`, bounded to 1..90, and
threads it to the dispatcher. Omitting it changes nothing: every engine
keeps its own default, so a caller that asks for nothing reads exactly
what it read before.
Ninety is where the tier this route promises runs out. The rollup router
opens its WEEK floor at 91 days, and the baseline engines cannot compose
a spread from WEEK buckets, so a 91-day request falls through to a raw
`measurement.findMany` with no row cap — for a densely sampled type that
is a full history scan, and the analytics-read budget allows 120 of them
a minute per account. The trailing `series` is capped at 30 points
regardless, so past the ceiling a wider window would only broaden the
mean behind the value. Out of range is a 422 rather than a clamp.
An unhonoured window is visible rather than silent. `provenance.windowDays`
is the window the engine actually used and `coverage.historyDays` the days
that actually backed it, so a three-week record answering a thirty-day
request reports both numbers instead of implying coverage it does not
have. `HEALTH_SCORE` composes fixed per-pillar windows and reports the
widest; a new suite freezes that it is the only metric that does, so an
arm added later cannot quietly drop the parameter.
Recovery keeps its canonical resolution under the wider window. A worn
band stamps the wake morning and the computed proxy the night before, and
only `resolveCanonicalRecovery` pairs them; a test seeds forty such
nights, asks for sixty days, and holds the answer to forty nights with a
flat trend — a bypass leaves eighty rows and a false climb.
Contract: model `WellnessScoreValue` field for field and publish it. The
envelope's `value` has to stay an open record because one route serves
eighteen differently-shaped metrics, so nothing can `$ref` the shape and
it went out undocumented — `series`, `daysInWindow` and `asOf` were on
the wire with no way to find them short of inspecting a live response. It
is registered in the forced-components slot instead, held against the
runtime interface by a test, and the fields it does not cover are named
in the schema comment so the remaining gap is known rather than invisible.
Eighteen modules carried a convenience re-export of a symbol defined
elsewhere. In each case the callers the comment describes now import the
concrete module directly, so the line had no importer left. Where the
whole re-export block went, the comment explaining it went with it —
doctor-report-pdf-core, trend-card and medication-extract-prompt each
claimed to be a single import surface that nothing was using.
Definitions are untouched; imports that existed only to feed a removed
re-export go too.
The barrel mirrored every export under src/lib/insights/derived. Of the
124 lines, 101 had no importer: consumers and per-metric tests reach for
the concrete module (./sleep-score, ./trajectory, …) directly. Keep the
cross-cutting surface — registry, dispatcher, baseline loader, and the
value shapes outside consumers read — and say in the header that the
barrel is not a mirror, so the next addition needs a consumer first.
Definitions are untouched; only the re-export lines go.
The normative-claim bank held English and German words. Insight
recommendations are generated in the reader's locale, so for French,
Spanish, Italian and Polish every recommendation scored non-normative:
the annotation reported zero normative claims and zero uncited ones, and
the admin quality dashboard read that as full coverage rather than as no
measurement at all. A check that cannot fail is indistinguishable from a
check that passes.
The bank now has one arm per shipped locale and the caller passes the
generation locale; the reader's arm and the English one both run, because
a fallback provider answers a non-English reader in English. Matching
folds both sides through the shared label fold and anchors the leading
word boundary, so inflections still hit while the everyday Italian
"soprattutto" no longer counts as a comparative.
These words are the model's prose, not app labels, so they are not in the
message bundles and cannot be derived from them. The structural test
refuses an empty arm instead: a seventh language fails the suite rather
than quietly scoring nothing. An empty arm also used to compile to an
alternation that matched every string, which would have flipped the same
blindness to the opposite extreme.
Four surfaces assembled the correlation-discovery matrix and three carried a
different channel set. The Coach `get_correlations` tool had neither the
environmental-exposure channels nor the custom-metric ones, so asking it
whether air pressure relates to your sleep met a matrix the pair had never
been in. Its own file header said the opposite - "the SAME full-matrix scan
the route runs" - and had said it since the weather channels arrived in
v1.25. The period narrative was missing those two families and the
compliance / symptom pair on top.
`src/lib/insights/discovery-matrix.ts` is now the one place a matrix is
built. v1.30.3 hoisted the fetch discipline into a shared module for exactly
this reason and left the assembly duplicated behind; this finishes it. The
route, the per-metric card, the Coach tool and the period narrative all call
`assembleDiscoveryMatrix` and get the same families. What still differs is
declared as an option with its reason at the declaration: the measurement
read mode (a read-cost choice, left per surface), the extra metric types the
narrative's delta beat needs, and the RATED mood factors.
Mood factors stay on the narrative alone. That capture kind is frozen, the
two surfaces that persist a discovered pair write its channel key into a
plaintext column, and a custom factor's key is an opaque id - so admitting
them elsewhere is a decision about what to ask, not a parity fix.
Restore the per-metric filter on the assessment card's relations. It was
written in place of the pattern-dismissal filter rather than chained with it,
so a weight card's prompt had been receiving every surviving pair in the
record while the function name, the docstring and the early-return guard all
still said one metric.
`discovery-matrix-guard.test.ts` freezes who may fetch a channel, who may
construct one, and who may scan. Each matcher asserts a non-zero match count,
so a matcher that has stopped matching fails rather than agreeing with the
allowlist.
The statistics are untouched - same Pearson, same exact p-value, same
Benjamini-Hochberg, same floors. The Coach's family is larger now, which is
the point: it is corrected over the same family the insights page is.
A rollup band outlives the rows it was folded from. The read-swap falls back to
live SQL only when a band is EMPTY, so a bucket that is merely wrong is served
as current; `ensureUserRollupsFresh` repairs the DAY tier over the trailing 90
days only, and keys off `Measurement.updatedAt`, which a hard delete does not
bump. The WHOOP body sync hard-deletes a WEIGHT row with no invalidation, so a
MONTH bucket keeps reporting a value the record no longer holds.
The block was handing the model two irreconcilable accounts of the same record
and marking neither: the coarse tail said one thing, the all-time extremes read
live beside it said another, and `asOf` stamped the whole thing current because
it is derived from raw reading age and knows nothing about rollup age.
Reconcile them where they are assembled. A bucket mean is an average of real
rows, so it cannot fall outside those rows' own all-time extremes — when it
does, the band is describing readings the live read cannot see. That is an
invariant rather than a heuristic, so the disputed tail is dropped and
`asOf.coarseHistoryWithheld` records that it went. Narrating a ghost is worse
than narrating less.
`currentForTodayClaims` stays keyed to raw recency, which is what it means. The
surviving readings really are from today, and suppressing a true present-tense
statement to punish a stale history band would trade one dishonesty for another.
The bucket on disk is still wrong, and a wide event now says so. The deeper fix
is to make the tier notice — compare a bucket's `computedAt` against the type's
in-window MAX(`updatedAt`), or invalidate on the hard-delete path — which is a
larger change than this repair.
The six measurement routes carried eight copy-pasted rollup-recompute +
status-invalidate blocks — the documented two-ended failure class
waiting for the next surface to remember one leg and forget the other.
All of them now call afterMeasurementMutation (the helper the deletion
fix introduced), and the two write surfaces that bypassed the hook
entirely join it: the nightly Recovery/Stress/Strain score rows and the
PHQ-9/GAD-7 assessment projection now recompute their (type, day)
bucket on the tick that writes them.
- The daily Coach sweep now charges each ignored day against
surfaceCount and auto-dismisses at three — the cap the schema comment
promised since v1.22 but nothing enforced.
- The welcome gate compares the stored disclaimer acknowledgment
version against the current one, so revised disclaimer text
re-prompts; /api/auth/me carries the version for the client.
- slope90 and anomalyCount leave the analytics wire: slope90 had no
reader anywhere (computed on every request, in SQL twice), and the
slim path could never compute anomalyCount (it always reported 0).
anomalyCount stays as an in-process field on summarize() for the
insights feature builder, which is its only real consumer.
The biomarker catalog mints name and unit from labResult.analyte —
model-transcribed free text out of an uploaded document. Both rode into
the status generator raw, the name in system-prompt position, so a
hostile PDF could smuggle instructions past the fence the coach
labs-snapshot already applies to the same fields. Sanitize both at the
snapshot and the system-prompt site.
Both paths read identical fixtures — per-(day, source) DAY rollup rows
derived from the same raw rows the fallback reads — and must produce
equivalent daily series: the plain daily-mean case, the multi-source
day (the all-source weighted mean, never a ladder-collapsed single
source), and the pre-sum_value mean-times-count compose. A far-from-UTC
profile timezone must force the raw path so day keys stay profile-tz
keyed; the near-UTC midnight band's single-day attribution shift is
pinned explicitly so it stays a documented decision rather than drift.
Coverage misses and empty in-window bucket reads fall back per channel;
sleep and the cumulative types never enter the rollup query.
Every assertion family was broken on purpose and observed failing with
the site named before restoring green; the record sits in the test's
doc comment. The route test mocks the new coverage probe and bucket
read as empty so every channel rides the raw path unchanged there.
The discovery scan read every raw measurement in the 180-day window for
all nine measurement channels and bucketed them into daily means in JS,
paying tens of thousands of rows on every cache miss (a CGM glucose
channel alone can exceed the read cap). The DAY rollup tier already
stores those per-day aggregates, so eligible spot channels now read one
pre-aggregated row per day per source and compose the all-source
count-weighted mean — the exact reduction toDailyMeans applies.
Read-swap replaces the raw read per channel, with fallback-on-miss:
structural ineligibility (SLEEP_DURATION's per-night reconstruction,
the cumulative types' device-type collapse axis the rollup rows do not
carry), a far-from-UTC profile timezone (the v1.4.38 W-A near-UTC
guard), a coverage-probe miss, or an empty in-window bucket read all
route that channel to the existing raw path. The SWR cache cell stays
in front; the swap only lowers the cost of a cache miss.
The /insights overview crept a kilobyte over its ceiling: the ECG
teaser imported its overview cap from the classification module, which
dragged that module's label map and helpers into the eager page, and
the Health Score card pulled the whole metric-provenance map for its
one citation. The cap now lives in a value-only module and the single
provenance entry is split out, so the overview imports the bare number
and citation it renders. Both routes are back under budget. The
aggregate ceiling rises to hold the six-locale copy the new encounter,
mood-context, mood-prognosis and practitioner surfaces added, which the
whole-build sum counts independently and no single route downloads.
The switcher and the banner named a shared record's owner by their greeting
name — the value they typed at onboarding for "how should HealthLog address
you". When an owner has set a full name, that is what a delegate should see:
entering somebody's record, you read whose it is by who they are.
- the account-access resolver publishes the owner's `fullName` alongside the
greeting name and the username
- `accountLabel` prefers fullName, then displayName, then username. Optional
on the argument so the grant-view and actor shapes that never carried it
keep the old two-field behaviour untouched
- the closed presentation schema carries `fullName` and its parity matcher
compares it, so the two published views of the active record cannot name
the same owner differently
- the OpenAPI contract regenerates with the additive field
This deliberately discloses the owner's full name to every delegate, read-only
included — it was patient-identity data behind a MANAGE-gated export until now.
That is the intended behaviour, documented at `accountLabel` so it is not
mistaken for a leak and tightened back. The resolver, the schema parity, the
label fallback and a read-only delegate's banner are each pinned by a test.
The five-strip cap belonged on the overview teaser — a glance on /insights —
not on the surface a user opens to read their history. It was living in the
shared EcgSection, which the dedicated /insights/ecg page reuses, so an
account with many recordings could only ever reach the newest five there and
the older strips became unreachable with no pagination behind them.
The cap is a prop now. The teaser passes ECG_OVERVIEW_LIMIT; the page passes
nothing and lists every recording. The constant lives in the React-free
classification module so the overview page can pass it without pulling the
dynamically-loaded ECG component into its bundle.
The ECG surface opened with three lines of muted text saying what HealthLog
does not do, before a single strip. Two of them said the same thing — one
from the page's explainer slot, one hand-rolled beneath it — and the third
repeated it under the list. The device attribution on the recording itself
already carries that claim where it means something, so the three go and the
page opens on the recordings.
- the device's verdict rides the timestamp as a tag instead of taking a line
of its own beneath it. It qualifies the recording; it is not a second fact
- a recording is a route now, `/insights/ecg/[id]`, with the back link ABOVE
the heading exactly like a workout detail. The detail used to swap itself
into the list card with its back button under the page title, and the
recording had no address at all: nothing to deep link, nothing for the
browser's own Back to return to. It reads by id, so a cold link works
- the list paints the five most recent strips. A watch that records every
morning turns an unbounded list into a scroll, not a history
`ecg-classification.ts` holds the verdict label map the list, the detail and
the cross-link card each used to carry a copy of.
The snapshot gains the day's two readings with the age of the second
one, through the same reader the page uses, so the page and the
assistant cannot disagree about what today's comparison is. The
freshness rule is the shared one: a comparison about the day before
yesterday carries the date it is about and an instruction not to state
it as today. Absence is a present field rather than a missing key —
a model told "there is none yet" says so, and a model told nothing
estimates one. The wording rule travels with the values, because a model
handed a number and no instruction writes the sentence its training
suggests, and about a forecast that sentence is a causal one.
The contract publishes the resolved value: the expected figure, the
band, the count, the deviation against the band, the age, and the
contributions that carried weight. Nothing is left for a client to
re-derive — two clients deriving the same figure about somebody's health
record is how they start disagreeing about it. The description carries
the observational wording and the display rule: the count goes wherever
the value goes.
The end-to-end proof runs against real Postgres and reads the table
rather than a return value: nothing written a day short of the
threshold, a banded and counted row on the day it crosses, the
provisional label dropped at sixty days, the rows cleared when the
module goes off, the rows cleared when entries are deleted back below
the threshold, and an older pass's row replaced rather than left beside
a fresh one. A delegate holding only the mind section is refused, and
the assertion is on the response bytes.
Nine visit routes and five practitioner routes, all classified under the
existing profile domain and each reviewed into the frozen sharing
literal with the sentence that argues for it. An address book of a
person's own doctors is record content, not account configuration: it
names no credential, no integration and no notification channel, which
is the fence that classification turns on. The eight sharing domains are
unchanged. Link and unlink stay bare, matching document link and unlink,
and the pair is revisited as one decision later.
Nothing is a required field. A visit saves with a date and nothing else.
Every id a body may carry is re-narrowed to the caller before anything
is written, and an unknown link id never blocks a save.
A booked visit mints exactly one reminder on the existing engine with
the new origin, and the appointment half is the part worth reading
twice. Both cadence columns stay null, which is the schema's one-shot,
so the first due instant is stamped directly and the never-nag property
is structural rather than guarded: after it fires, the cadence helper
has no occurrence to return. Rescheduling re-anchors that row and never
mints a second, which the tests assert by counting rows rather than by
checking the date moved.
Filing a visit as done closes the checkup it was filed against, on the
transition rather than on every later edit. Writing the test first found
that: satisfying is forward-only, so re-saving a completed visit was
pushing the checkup's next due date out again as if the person had been
a second time.
An appointment must appear on no preventive-care surface, and that is
four separate reads, one of them in the assistant tooling. Each carries
the exclusion, a guard proves all four by source text with a non-zero
match count, and the reminder mapper refuses such a row outright — so a
read site that loses its filter fails loudly instead of showing an
appointment as a checkup. The published origin enum is untouched.
- one comparison per context value, reusing the Welch test, the day floors and
the Benjamini-Hochberg family the tag board already runs; a wide sweep
filtered on a raw p < 0.05 would surface one-in-twenty noise as a finding
- two entries on one day count as one day
- every statement names how many days it compared, on the same surface as the
claim, and says it shows a connection rather than a cause
- the assistant snapshot carries the same rows with that constraint written
out beside them
The dead-code sweep found the window type exported and unread. It was
unread because the surface that offers the choice carried its own copy
of the same three numbers: the chart could not import the calculator's,
since that module reaches the database client two hops down and a client
component pulling it in would ship Prisma to the browser.
Both now come from the dimension table, which is pure by construction
and which the chart already imported. One declaration of 7 / 30 / 90,
read by the calculator that averages over them and by the control that
offers them, so the two cannot drift.
Three small things the audit named together.
The mood reads spread the whole row, so adding the wire keys shipped
every level-A value twice — once as the column name and once as the key
the write path accepts. A client would have picked one and been stuck
with it. The column spelling is stripped now, and the create response's
key set is asserted so it stays stripped. The published contract is
unaffected either way; these routes are not in it.
The dimension series carried its own copy of the day-key subtraction
that the freshness module already exports. One copy now.
And the reason a stale dimension ships with a notice instead of being
dropped the way the status surfaces drop one is written at the builder:
a one-line card has nothing left to say about a value it cannot narrate,
while an open conversation can say when the value is from — and silence
there would leave no way to tell an unanswered dimension from one nobody
has looked at in a week.
The snapshot stamps every metric block with an asOf built from the
newest reading in it, which for mood is the newest ENTRY on the 1-5
axis. Somebody who taps a face each morning and opens the sliders once
a week therefore carries a block stamped as current and a stress
reading six days old, and a reader taking the block stamp as covering
the dimensions states last week's answer as today's.
The block now names the precedence in one line beside the dimensions,
and each dimension keeps its own age. Present only when there are
dimensions to explain.
- the level-A blocks ride the mood aggregate the coach and the briefing
already share, so the ages are fixed once in the data preparation
rather than once per sentence
- every block carries its newest value, the day it belongs to, how old
it is and whether that age still backs a present-tense claim, through
the same freshness rule the vitals surfaces use
- a stale dimension carries the instruction in words as well as in a
flag, because that is the sentence this exists to prevent
- the 1-5 scale line is untouched; the new axis describes itself, with
the anchors resolved from the bundle the person read
- the trend page and the assistant run the same calculator, so a number
in a sentence cannot disagree with the number on the chart
- one engine: the dimension series is a sibling calculator of the mood
aggregate hub and rides the same bounded read, so no second summary of
mood can disagree with the first
- 7 / 30 / 90-day means per dimension, plus the daily series behind them
- a dimension nobody answered is named as not recorded rather than drawn
flat through the middle of the scale, and a zero counts as an answer
- each line keeps the orientation the person answered in; the legend
says which way the inverse one runs
- the card rides the existing module-gated aggregate read and query key
A lab runs its own method on its own device, so the window a physician
evaluates a value against is the one printed next to it on that report,
not a catalog band averaged over every lab. Reading a report pulled the
value and dropped that range: the extraction asked for it, the commit
handed it to the biomarker resolver, and the resolver only uses it when
it mints a NEW marker. For every analyte the user already had — the
normal case after a first panel — the printed range was read and then
thrown away.
Add `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText`
to `LabResult` and carry the range from the report to the reading. The
text is stored whether or not numbers can be read out of it, because
"bis 5,0" and "negativ" state a window too; a range that could not be
parsed is kept verbatim, never replaced with invented bounds.
`resolveEffectiveReferenceRange` is the one place the precedence lives:
the source window wins for that single reading, and the catalog stays
the net for readings without one. A printed string with no derivable
bound does not displace the catalog. Every surface that judges a
reading routes through it — the API verdict, the doctor report, the
insights blocks and per-marker assessment, the coach snapshot, the MCP
read and the FHIR `referenceRange` — so a value cannot read in range on
one surface and out on another.
The API keeps `referenceLow` / `referenceHigh` as the window in force,
so a client that reads only those two renders the right range and the
right verdict with no change of its own; the catalog band, the source
window, its origin and a divergence flag ride alongside for clients
that want to show both.
Read side: the chart draws the source window as a second, visually
distinct band with the reading it came from named beneath it, the
reading rows name the printed range and the catalog band where the two
disagree, and the doctor-report PDF prints the report's own string with
a footnote naming the saved range. Entry side: one optional field on
the lab form, folded away until asked for, and the printed range is
editable on the scan review screen.
The range parser covers the notations German and English reports
actually print — two-sided, one-sided, comma or dot decimals, thousands
separators, trailing units — reads units without ever converting them,
and refuses to guess on anything ambiguous.
Two things about the card that shows what changed since the last panel.
The comparison had no expiry. Someone whose newest panel is years old was
still reading "since your last panel" about a draw from another year — not
stale UI state but a stale sentence, and one the app kept repeating on the
overview every day.
The window is a year, applied to the data rather than to a per-user
dismissal. A year because routine panels are commonly drawn annually, so a
shorter window would hide a comparison that is genuinely the newest thing
somebody has for months out of every year, while past a year the wording
cannot be defended at all. Doing it on the data means nothing to
acknowledge, nothing to persist, no housekeeping asked of anyone, and one
answer for every client through the existing absent contract — the card
already un-mounts on it, and so does anything else reading that route.
Verified in both directions over the real endpoint.
The delta on the right was muted at the smaller size, the same typography
as the meta line under the analyte, so it optically hung off that lower
line and the row read as bottom-aligned even though it measures centred to
the pixel at every width. A delta is the number the row exists to report,
so it takes the content tier, and the cell states its own centring rather
than inheriting it.
The existing cases pinned their clock at the same time: fixed fixture days
against a relative window is a test that passes until a date in the future
and then fails for reasons unrelated to the code.
The hero line said a pulse was well above its 30-day average today. Nothing
had measured a pulse for five days. The day-signals card flagged the same
reading, and the baseline-drift card said the value was above the usual range,
present tense, on a day that held no reading at all.
One class, and it sits in the data preparation rather than in any of the
sentences. Each surface was handed "the freshest stored value" with no notion
of when it was taken, so each of them assumed now. The age was reachable — the
aggregates carry `newestDaysAgo` inside their coverage object — but it read as
a statistics footnote rather than as the fact that governs the tense of every
sentence about the metric, and none of the three consulted it.
A reading is material for a claim about today when it comes from today or from
yesterday. One day rather than zero: a vital taken at 23:40 and read at 00:10
is the same reading it was half an hour earlier. Two is not, because by then a
whole day has passed with nothing in it and the sentence would be describing a
gap.
The rule now lives beside the data. The vital deviations carry the age of the
reading they were computed from, and the two deterministic consumers apply it:
the day-signals flag counts only current vitals toward its count, and the
baseline-drift card surfaces only current deviations. Stale vitals stay in the
list with their age, so the anatomy view still shows them and nothing silently
disappears; the dated level shifts were never today claims and are untouched.
For the narrated surfaces the same fact is stamped onto every metric block of
the Coach snapshot before anything reads it, so the age travels with the
numbers into the hero, the briefing, the Coach reply and the retrieval tools
instead of each of them guessing. A block narrated off a stale series also
lands in the wide event, which is where it should have been visible all along.
The baseline-drift card reported a pulse in the hundreds of millions against a
usual range in the tens of thousands. Neither figure is a heart rate, and both
came from the same place.
The application declares a plausibility domain per metric, and every
interactive, import, MCP and Telegram write validates against it. The
derivation layer did not ask. So a single stored value outside that domain — a
provider unit-decode slip, or a row from a writer that never had the gate — was
folded into the day mean, carried by the median into the band, and then held
against itself as "currently". The two numbers in the sentence were wrong by
different amounts, which is why the line read as nonsense rather than as an
exaggeration.
Both readers now refuse a value the application has already declared
impossible: the per-day means behind the band, and the latest-day mean the
deviation reads. A day left with no plausible reading contributes nothing
rather than a fabricated point, and the vital drops out of the comparison — the
coverage floor already knows how to report an absent day. The rollup branch
applies the same rule to a bucket mean, since the rollup writer averages
whatever is stored. Everything downstream of `readDayMeanSeries` inherits it,
which is every consumer of the usual range: the band strip, the drift card, the
changepoint detector, readiness, trajectory, the coach read.
The card also printed raw doubles. Each figure now renders at the metric's own
decimal precision through the shared formatter, so a mean reads as a number
rather than as a float and follows the reader's number convention.
Left open, and recorded for a separate pass: the provider sync writers do not
enforce the domain on the way in, so the impossible rows keep landing.
An owner can now narrow a grant to the parts of their record they mean to
share, and can offer the third level. Both decisions are made once, on the
invitation, and neither widens afterwards.
The invite schema takes `scope`: absent or null is the entire record, which
is what every stored grant means and what an owner who does not narrow still
gets. An empty array, a key outside the vocabulary and the whole-record
sentinel are each refused with a 422 naming the field; `inviteGrant` refuses
the same three underneath, and its `invalid_scope` now has an HTTP mapping
instead of falling into the generic refusal.
Offering MANAGE asks for a fresh second factor when the account has one. That
gate resolves through the session cookie and has no Bearer fall-through, so a
token caller is refused first with `sharing.invite.manage_browser_only` rather
than being told it is not authenticated by a route that had just
authenticated it. The consequence is deliberate: management of a health
record is offered from a browser.
The account payload gains `sections` and `level` beside `access` and
`canWrite`, resolved server-side and ordered as the consent screen reads.
`access` was `grant.access === "WRITE" ? "write" : "read"`, which would have
published a grant that can delete entries as read-only; it is a total map
now, so the next level cannot ship silent. The capabilities hook carries both
facts and turns neither into an affordance yet — the edit and delete controls
come back in the release where the routes behind them answer.
The consent surface asks two questions in order rather than laying out a
nineteen-cell grid. The level is three radios, narrowest preselected. What
they can open is two radios, the entire record preselected, and only the
narrow branch expands the eight sections with a sentence each. Both honest
limits are stated above the checkboxes, where the decision is being made: a
section shares everything written in it, and the cross-record surfaces do not
appear for a narrowed grant. Documents carries its own warning, cycle appears
only for an owner who tracks it, and Manage collapses the question to the one
line that answers it.
The Manage sentence says the other person can add, change and remove entries,
including ones the owner entered themselves, and says in the same breath what
it does not reach. The acceptance side gets its own responsibility line rather
than the write one wearing a stronger adjective.
Six locales.
Nine surfaces offered a delegate an action the server declines, and the
switch had never been asked about any of them.
The Today rail's dismiss and the coach check-in's keep / let-go write
through routes that resolve the caller, so both refuse under a switch;
they now ask canManage and are absent at either grant level. A priority
action with no href and no handler is no longer rendered as an inert
button at all — the type carries that rule now.
The Vorsorge dashboard card offered the mark-done that /checkups already
withholds, and the checkups list branch inlined its own ungated copy of
the button the cards branch gates, so which view a browser last chose
decided whether the action appeared. Both bind canManage.
The chart overlay cog persists through a route that resolves the caller
and stores a preference belonging to the person rather than the record.
Gated once in the control so the three chart wrappers cannot drift.
Three query parameters opened sheets past a gated button: ?add=<TYPE> on
measurements, ?new=1 on medications, and ?edit=1 on a medication detail,
the last of which opens a wizard refused at both levels. A deep link is
the same affordance as the control that produces it.
The efficacy retarget dial rewrites a setting the owner chose. The
episode documents card offered a link and an upload the vault gates on
the same endpoint throughout. And the dose ledger built its own success
toast, missing both the Undo suppression and the "saved to" receipt its
two siblings already carry — the one a delegate met on every dose.
The Coach drawer is not mounted inside a shared record, so every button
calling askCoach() opened nothing. The launch provider now publishes no
value there and each entry point's existing null check does the rest;
the documents sheet, the one that rendered without checking, now checks.
The briefing payload states how far off a checkup is as a bare number the
model narrates back: "due in N days", "overdue by N days". It got that
number by dividing the hour gap by twenty-four, which answers a different
question than the one the sentence asks. A checkup booked for nine this
morning came out "overdue by 0 days" by the evening. One due Sunday at
01:00, read late on Friday, came out one day off — Saturday, on a calendar
where it is two. A clock-change day is twenty-three or twenty-five hours
long and rounded whichever way its length pushed it.
The checkup screens had exactly this and it was fixed there, so the counting
already exists. Lift it out as `calendarDaysUntil` and have both the phrase
and the number come off it, rather than leaving the payload with a second
body that can drift again. It counts by differencing two integer day indices
read on the person's profile clock, so the answer is exact rather than
rounded, and `extractFeatures` resolves that zone and hands it down — the
number now agrees with the screen the person is looking at.
Today's checkup moves off the overdue list, where the hour gap had put it as
soon as its appointment time passed. The hour it was booked for may be
behind, but the day it belongs to is not.
The horizon that bounds the read stays an hour cutoff on purpose. It is a
query bound, not a statement to anyone about which day something falls on.
It could never score. The schema records VO2max but cannot tell a
measured test apart from a device estimate, so every fitness row reached
the scorer unproven and was refused - the pillar has never produced a
value on any account. Listing it on the score settings page as a thing
you can choose, with a line admitting it does nothing, was chrome over a
switch that changed no number.
So it leaves the catalogue: no pillar id, no domain, no scorer, no row,
no wire field. A stored selection that still names it reads back fine -
the resolver has always filtered stored ids against the catalogue and
drops what it no longer knows, so nobody's saved recipe throws or is
discarded over it.
That also fixes the page claiming unsaved changes forever. The save
stripped the fitness pillar out of the selection on its way to the
server, so the answer that came back could never match the form that
sent it, and 'All pillars' left the save button lit no matter how often
you pressed it.
Two branches met here. One made `configured` a required field so a caller
cannot stay silent and publish "not authored" for an account that chose its
own composition. The other wrote these cases before that existed, so they
compiled against the old shape and stopped compiling against the new one.
Filling the field in to satisfy the compiler would have thrown away the point
of making it required, so each case now states what it actually is: the two
with a hand-picked selection are authored, and the one whose own comment says
the account counts all eight is the default.
Two sentences about the Health Score stopped being true the moment
someone could choose which pillars count toward it.
The one people read said the score averaged "every pillar that has
enough recent data". For an account that took a pillar out, that
describes ground the number does not cover, and it reads as a
reassurance rather than a description. It now says the average is over
the pillars that count, that a pillar without recent data is still left
out rather than zeroed, and that which pillars count is the reader's own
choice, so the combined number is not itself a clinical standard. All
six locales.
The one nobody reads named the WHO HEARTS technical package flat, as
though a standards body had signed off on whatever selection the account
holds. HEARTS names the risk factors the pillars are drawn from; it
prescribes no average over them. The entry now says what it is cited
for. While checking where that citation surfaces it turned out it
surfaces nowhere: ProvenanceExplainer takes the prop and renders only
the method caption, which v1.22 did on purpose when the info popover
went away. That is written down in the file now, because the map's own
header implies otherwise and the next person would trust it.
The registry entry for HEALTH_SCORE lists all eight pillars with a
minimum of three, which reads like the recipe and like a threshold. It
is neither. The published coverage and confidence are built in
computeComposite over the resolved composition, so a person counting
four pillars is not measured against a denominator of eight, and the
real floor is the breadth rule, which counts distinct domains rather
than inputs. Both facts are now stated at the entry instead of being
inferable only by following the call chain.
Tests: the published denominator is never the eight-pillar catalogue; a
pillar somebody took out of their score is never listed back as missing
coverage; a pillar they kept and have no data for still is; the registry
list stays equal to the score catalogue; the choice sentence is present
in all six locales.
Three things are held down here. The route hands the reader's language to
the strip builder, for a cookie and for a stored preference. The pattern
sentence comes back in German for a German reader, asserted word for
word, through the real chain rather than the engine on its own — the
engine could always write German, it was simply never asked, so an
engine-only test would have stayed green the whole time it was broken.
And a cached strip stays with the language it was written in.
The guard reads the declarations rather than grepping them, so renaming
the argument or moving the fallback into a variable does not slip past
it. Its counterpart runs the same detector against those disguises and
insists each one is caught, because a check that cannot fail reads like
proof and is not.
On a German metric page the strip printed its own summary in German and
the pattern line under it in English. The pattern line is a finished
sentence the server writes, and the route never told the server who was
reading, so it wrote English.
The route now resolves the reader's language the same way the
correlations and analytics routes do and passes it down. The language is
a required argument the whole way through — it used to be optional with
an English fallback, which is why a caller could stay silent and still
get a sentence back. The cycle page's lagged rows had the same gap and
are fixed with it.
The strip's browser cache is keyed by language too, so switching
language no longer re-uses the sentence written for the previous one.