While the mood rollup tier is still empty the feature block falls back to
raw entries, and that fallback read one year where the rollup read five,
so the same block described a different history until the tier caught up.
About a dozen hand-rolled spreads each picked population or sample
variance without saying so, and the two surfaces a person compares
picked differently: the series API reported the population SD of a
blood-pressure series, the MCP tools and the AI payload the sample SD of
the same readings, about 22 % apart at three readings.
`lib/stats/spread` holds `variance` and `standardDeviation` with an
explicit `population | sample` kind and `READING_SPREAD`, the kind every
surface uses to report the spread of a person's readings. The series
route and the feature payload both report it; the spreads that feed a
verdict (the normal-swing checks, the changepoint threshold) and the
clinical glucose SD stay the sample estimate, now said out loud. A guard
fails on a new hand-rolled spread and on the two surfaces drifting apart.
The feature extraction, the signals of the day, the status cards, the graded
series, the Coach snapshot, the mood correlations and the report's trend arrow
each still averaged pulse over its readings, so a dense workout hour decided
the day and a workout day the window. They now read a pulse day as the mean of
its hours' means and a window as the mean of its days, through the day-mean
helper. Counts, extremes and spreads stay over every reading; every other type
is unchanged.
The daily briefing serialised the aggregated feature set as computed:
weight, grip strength and the weight target in kilograms, waist in
centimetres, glucose and the signals of the day in mg/dL (the glucose
signal carried no unit at all), workout distances in kilometres. A
reader on pounds or mmol/L got a briefing in units they do not use.
One step now converts the feature set after all canonical arithmetic,
on both the nightly and the on-demand path: absolute values through the
full transform, differences and slopes through the factor alone, every
converted block labelled, workout distances under unit-neutral keys.
The prompt, the stored fingerprint and the grounding check all read the
converted set, so a unit switch moves the fingerprint and the grounding
check compares the reply against the figures the model was shown. The
weight target sentence, the comparison block and the GLP-1 plateau
context quote weight in the reader's unit; the comparison block also
labelled sleep as hours while carrying minutes.
A unit switch now also re-warms a cached briefing alongside the status
notes and narratives.
Refs #1067
Building the coarse buckets from the DAY tier read every day of the window
into the process: a five-year yearly read moved thousands of rows per type,
and the Coach's history bands read several such windows per request. The
per-day source pick and the fold now run in Postgres (DISTINCT ON the day,
ordered by the ladder rank and then the source name, the same order the
in-process pick uses), so a coarse read returns one row per bucket. The
long-range chart picks its tier from the span of the window's days and reads
that tier once. The Coach snapshot hands its already-loaded source order to
the history bands instead of each band reading it again.
The all-time weight, blood-pressure and pulse figures the insight features
carry averaged every raw row, so a device that samples densely outweighed the
readings the source ladder prefers on the same days. They now keep one source
per day, like the dashboard's all-time figures.
The period-over-period range delta averaged readings. For step-like totals
the current window still holds the last day and a half as many small
samples, so the average collapsed; it now compares average daily totals. For
sleep it averaged stage segments and in-bed envelopes; it now compares
average nights of time asleep. The API descriptions say which.
The rollup writer keeps one row per bucket and source. Collapsing the stored
WEEK / MONTH / YEAR rows picked one source for the whole bucket, so a month
of manual blood-pressure readings plus one synced reading reported the synced
day alone, and a step month that switched devices lost every day the losing
device carried. The live weekly and monthly chart fold made the same pick.
Every coarse bucket is now folded from the canonical DAY buckets: the source
ladder picks one source per day, then the week, month or year counts every
one of its days. Spread, slope and r² are composed from the summed day
accumulators. The live fold picks per local day before folding into weeks
and months. Ties between sources without a ladder rank break alphabetically
on the SQL paths too, matching the rollup reader.
The weight tile's arrow, 7-day delta and comparison caption were wired
as "up is bad", so a person below their own target who was gaining saw
every step toward it coloured as a setback. Movement is now judged
against the target they entered: below the band a gain is progress,
above it a loss is, and inside it holding steady is, with a move either
way shown neutral rather than orange. Without a stored target the tile
reads exactly as before; the height-derived band is not treated as a
target because nobody chose it.
The direction is resolved once on the server and published on the
dashboard snapshot as tiles.weightTrend, so the web tile and the native
client colour the same reading the same way. The personal-record
direction reads the same placement helper. The feature payload behind
the briefing, the overview analysis and the Coach now carries the
target band and which way is progress, so a narrative no longer calls a
gain a regression when gaining is the goal. The insights vitals grid
drops weight from its fixed "up is bad" set.
Refs #1006
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.
Fold the ECG silo into the insights narrative without crossing the
non-diagnostic line — the app reports the recording device's own verdict
and the existence/count of recordings, never an interpretation of the trace.
- Feature snapshot: a bounded, device-verdict-only ECG descriptor (recording
count, device-verdict distribution, latest recorded-days-ago + average HR)
flows into the nightly briefing prompt. The waveform never enters the
payload; the grounding gate admits the pre-computed counts so a
device-attributed sentence stays grounded. A prompt rule (EN + DE)
constrains any mention to the device's attribution and forbids reading
morphology, measuring intervals, or emitting a verdict of the app's own.
- Pulse cross-link: a device-attributed pointer from the resting-HR / pulse
page into the ECG viewer, un-mounting when no recordings exist.
- Daily rail: an ecg_new_recording item surfaces once when a recording landed
within the last day — calm, info-level, deep-linking the viewer. Gated on
the insights module; the DTO carries no waveform.
- i18n across all six locales; non-diagnostic disclaimer copy per language.
The insights alert cards computed medication compliance with the naive
`schedules.length × days` denominator, so a fully-adherent weekly
injectable read ~14% and fired a false low-compliance alert (and a paused
medication mis-fired the same way). Route the alert path through the
canonical `calculateCompliance` engine like every other compliance
surface — pause eras, schedule revisions and dose windows honoured.
Document confirm: committing a numeric observation against an existing
biomarker silently replaced the stated unit with the catalog's, with no
conversion — a stated "6.1 mmol/L" against a mg/dL marker committed
"6.1 mg/dL". Reject on a unit mismatch (fail closed) so the user
reconciles on the review screen, the same posture as the unit-required
guard.
Wire grip strength, waist (with waist-to-height) and pain into the
briefing feature set and the grounding allow-set so the clinical-depth
signals reach the daily narrative; the mental-health screeners stay
excluded by construction.
Biomarker detail: fall back to a generic description for catalog-less
markers, and keep the heading + description with a retry on the error
state. Collapse the medication External-API and Lifecycle groups by
default. Re-enable the insights scroll-to-top regression and assert the
shipped main-content reset.
Align the EN haemoglobin/haematocrit display names to British spelling.
Drop the redundant `(userId, deletedAt)` index on inbound documents — a
strict prefix of the new three-column index. Trim the dead
MedicalDisclaimer variants to the one live `dataPosture`. Move the
crisis-resources card off the retired amber tint to a calm
destructive-subtle border. Add a registry invariant asserting every
biomarker signal key resolves to a real catalog slug.
The daily briefing's always-on feature payload carried only the core vitals,
mood, sleep, steps and medication compliance, leaving four captured domains
invisible to it. Add four blocks to the aggregated features:
- Labs: recent flagged biomarkers (out-of-range or trending vs the prior
reading). Retired markers are excluded; qualitative results stay neutral with
no fabricated verdict.
- Preventive care: due and overdue items off the server-authoritative next-due
date, so an overdue screening can prompt an early nudge.
- Workouts: a provider-agnostic count, duration and distance aggregate over the
trailing windows (the briefing previously saw activity only as step counts).
- Glucose: the present-and-trend block the other vitals carry, where glucose
previously reached the briefing only as a single signal-of-the-day row.
The three extra reads are scoped to the wide briefing window; the tight
snapshot window that already carries its own context skips them. Every block is
omitted when its domain is empty rather than emitting zeros, and user-entered
labels are sanitised before they reach the prompt.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Eleven touch-disjoint waves landed on develop plus a six-axis QA
reconcile round before tagging. See CHANGELOG.md for the user-facing
prose; this commit message documents the wave + report inventory for
release-management traceability.
Waves shipped (each with a phase report under .planning/):
- phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the
analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the
v1.4.39 empirical-trace finding #1 (Prisma pool starvation).
- phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3
integration contracts close audit Critical Finding #3 (soft-delete
invisibility) across dashboard, analytics, summaries, rollup tiers,
and the Coach snapshot path.
- phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three
/api/insights/{features,targets,cards} routes plus the
compliance-rollup hook gap closure for bulk-projection mints; the
post-QA reconcile patch closes the six remaining measurement.findMany
sites in the cluster that still leaked tombstoned rows.
- phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation
under src/lib/rollups/* with zero orphan re-exports; lint, typecheck,
knip, and 93/93 rollup tests green post-restructure.
- phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the
queryKey factory-bypass walker test (extended to comparison/ in the
reconcile pass).
- phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17
paired-section tests pinning the 9 SB-3 disclosure requirements.
- phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association
endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity
assertions guarding against split rotation.
- phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER
time-sensitive APNs payload (priority 10 + interruption-level)
scoped via a parameterised test that pins all 6 other event-types
do not bypass Focus. AP-2 .p8 install gates real delivery.
- phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with
append-only invariant pinned in test; revoke-then-regrant leaves
both rows; idempotent DELETE returns 200 + receipt: null.
- phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers
(week/month/year) wired through summaries-slice and
health-score-fast-path with a linear-composability parity test
pinning the cross-consumer routing contract.
- phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed
across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation
eliminates a sibling-drift risk; knip whitelist trimmed to three
files pending follow-up.
QA reconcile patches applied per the v1.4.40 QA round (six reports
under .planning/round-v1440-QA-*-findings.md):
- senior C1 — dashboard-suspense-boundaries regex aligned with the
post-W-INFRA Thread 2 userTimezone-local lift.
- senior H1 — soft-delete filter added to the six remaining
insights measurement.findMany sites.
- specialist H1 — Promise.allSettled around bulk-projection
compliance recompute on both /api/medications/intake and
/api/dashboard/summary.
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets
factory; walker test extended to src/components/comparison/.
- security M1 — consent artefact 64 KB cap enforced via
Buffer.byteLength (UTF-8 bytes, not UTF-16 code units).
Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip
--include files,dependencies,binaries,unlisted, pnpm test --run
(4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over
the v1.4.39 -> v1.4.40 span.
User-facing prose: see CHANGELOG.md.
Carries the v1.4.36 perf carry-over for the full /api/analytics
route (correlations + healthScore + bp_in_target on the rollup-
coverage probe, 111s cold worst-case down to ~1.5-3s on power-user
accounts), consolidates Apple Health step samples into a one-row-
per-day list view with chevron drill-down and a nightly drain
(~50x table compression once it converges), restores
IntakeHistoryListV2 to the pre-v1.4.28 contract, converges every
Coach affordance behind a single feature gate with an invariant
test pin, lifts the medication-detail Mounjaro card to byte-
equivalent parity with Ramipril, brings the Insights overview
hero row to height parity, hardens getClientIp behind a new
TRUST_CF_CONNECTING_IP env flag, ships the Arztbericht hero card
on Settings -> Export, adds the Medikamenteneinnahme quick-add
overlay to the dashboard, drops the timezone override toggle for
a silent browser-zone auto-seed, and fixes the v1.4.36 red CI
workflows. Documentation refreshed across all three repos with
five new architecture diagrams in the Dracula palette. Unit suite
4354 -> 4486 passing; typecheck + lint clean.
Heavy-aggregate skip on the rollup-fresh + fully-covered path,
daily chart fetches opt into rollup buckets via source=rollup,
Insights extractFeatures swap drops the prompt payload from
double-digit MB to the low hundreds of kB on power-user accounts,
page-level isLoading gate replaced with per-section early-skeleton
paint, trends-row chart slot pinned for CLS, IntakeHistoryListV2
restored on the medication detail page, About card folded into the
Admin Console with an auto-checking update badge, cumulative metric
tiles read the day's running sum, IP-whois fallback surfaces city
+ country when the ASN lookup misses. Migration 0068 adds an
additive users.insights_exclude_metrics column. Unit suite 4354
passing; typecheck + lint clean.