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.
The generated-insight narrative layer built its user-facing sentences by
string interpolation and never routed through the i18n bundle, so a
non-English UI rendered English text spliced into localised copy — e.g.
"Higher activity steps tends to go with lower next-day weight" surfacing
mid-German on the Weight insight. Because those strings bypassed t(), the
locale-integrity guard could not catch them.
Route every correlation narrative through the server translator:
- Correlation discovery (daily lagged pairs + labs-vs-outcome) now takes
the reader's locale, resolved from the correlations route the same way
other insight generators resolve it. Sentence templates, the direction
word, and every channel label live under a new insights.correlation.*
namespace, translated in de/en/es/fr/it/pl. The three fixed hypotheses
(BP-compliance, mood-pulse, weight-weekday) and their weekday labels are
localised the same way; the analytics cache key now includes the locale
so a language switch cannot serve another locale's cached text.
- Metric labels resolve to stable localised channel labels; the never-
causal framing ("not a cause" / "an association, never a cause") and the
faint/moderate/high hedging tiers are preserved in every language.
English output is byte-for-byte unchanged. Adds a regression test pinning
German narration and asserting it is not the English string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V8V9qapgHi7fhgtMJ8SkM
Two in-memory key builders joined their parts with a literal NUL
(`${a}\x00${b}`): the sleep writer-bucket key and the correlation
pair key. The embedded NUL made grep classify both files as binary, so a
plain `grep symbol src/**` skipped them silently.
Swap the separator for U+241F (␟, the unit separator). It cannot appear
in a `MeasurementSource` enum literal, a device-type tag, or a
correlation-channel name, so the key semantics are identical — the sleep
key still slices its source prefix unambiguously and the correlation Set
still excludes double-counts. Both keys are ephemeral (one pass, never
persisted), so the byte change is invisible to any stored state.
Register the seven environmental-exposure signals through the signal registry
as a new "environment" kind (correlation-eligible; off Coach/MCP/FHIR in v1),
and fold their daily series into the correlation engine as lagged behaviour
channels. Weather/daylight now pair against mood, sleep, and vitals through
the existing FDR / effect-size / shrinkage gates, so a thin series degrades to
absent rather than a spurious link. All env channels share one ENVIRONMENT
family so two same-day fields never lag-correlate against each other.
Code-review and design follow-ups before the v1.22 cut. No behaviour change
beyond the items below.
Coach / insights:
- Coach SSE route: a non-provider error raised inside the open producer (e.g. a
DB failure persisting the reply) now records on the wide event and forwards to
GlitchTip instead of masquerading as a provider outage; it still surfaces an
error frame since the stream is open.
- Lab ↔ outcome correlations: hedge faint-tier pairs ("a faint hint, if
anything … too small to lean on") so the narrated confidence never outruns the
effect, matching the daily engine.
- Reminder sweep: mint the plan-review reminder and clear the plan's reviewDate
in one transaction so a half-applied write can't duplicate the reminder.
- Fold the two raw provider-fallback runners onto one shared chain walker.
Design / accessibility:
- Lift sub-44px tap targets to the 44px mobile floor (suggested-action card,
Coach bubble affordances), brighten the resting timestamp clock for non-text
contrast, and route the lab chart height through the shared constant.
- Replace ad-hoc arbitrary type sizes with the scale token and static size-11
icon buttons with the named icon-lg variant.
- Settings: add the missing About card-header descriptions, normalise the
low-stock numeric widths, and relax the notification-status rows from nested
cards to a divided list.
Housekeeping:
- Drop three unused exported types (and the now-orphaned source const), re-stamp
v1.21.5 comments to v1.22, and register the live-judge eval harness as a knip
entry so the script-only module resolves.
Add a rolling 14-21 day early-detection pass to the FDR correlation engine
so an emerging shift (illness pulling HRV and sleep down together) surfaces
before the 180-day retrospective scan accrues the n>=20 it needs. The recent
pass re-runs the same engine over the trailing window with a lower paired-day
floor, a tighter Benjamini-Hochberg target, and a no-faint effect bar, then
drops any pair the retrospective scan already established so a pattern is never
reported as both established and emerging.
Extend the curated behaviour x outcome matrix with three clinically meaningful
links: medication adherence -> next-day blood pressure (systolic and diastolic,
the latter previously never a candidate channel), sleep as a behaviour
(poor sleep -> next-day mood / HRV), and a separate point-vs-window labs ->
outcome pass (each draw against the contemporaneous outcome mean) since sparse
lab draws cannot meet a daily-lag floor. Every pass keeps FDR control, the
effect-size floor, and shrinkage; thin data degrades to absent, never a
fabricated link. All associations stay descriptive, never causal.
Render daily medication-compliance rate and illness symptom-severity as
first-class series in the FDR discovery engine, so the adherence-dip →
symptom-flare link (and compliance → vital drift) can finally surface.
Compliance pools every active medication's dose-history ledger into a per-day
taken/(taken+missed) rate, re-keyed to the user's display timezone. Symptom
severity reads the illness day-log functional impact, zero-filling healthy days
only across real episode spans so a user with no episodes yields an empty
series rather than a constant. Both flow through the existing n >= 20, BH-FDR,
effect-size, and shrinkage gates unchanged; a sparse series degrades to absent.
Each channel forms its own metric family (no self-lag tautology) and keeps the
association-not-causation framing. Add discoveryMeasurementTypes() so every
caller drops the non-measurement channels before a Prisma type filter.
Significance alone surfaced trivial drivers: a large-n pair with r=0.16
narrated as a confident "tends to go with" link while explaining ~2.5%
of variance. Add three gates before a pair is ranked as a driver:
- exclude same-metric-family lagged pairs (mood->mood, BP component
self-lag) as serial-autocorrelation tautologies, widening the existing
exact self-pair skip to the whole family.
- shrink each Pearson estimate toward null by n/(n+10) so a thin-data
correlation cannot out-rank a deep one on an inflated point estimate.
- floor the shrunk effect at 0.2 (drop below) and bind 0.2-0.3 to a
hedged "faint hint" phrasing tier, 0.3+ to confident phrasing scaled
by sample depth. The reported r and p stay the honest raw statistics.
Rank by shrunk effect magnitude, q as tie-break.
Rated mood factors (work / sleep-quality / stress, scored per entry) now
participate in the cross-metric correlation surface. Until now their score
series was captured but read by zero analytics.
Two paths, both on the existing FDR-controlled engine:
- A factor x vital crosstab on the mood insights page and the mood
snapshot: for each factor the user scores, split its days low vs high by
the factor's own median, then run the same Welch test + day floors +
Benjamini-Hochberg correction the tag crosstab uses. Surfaces lines like
"sleep runs ~38 min below baseline on the days you rated work low"
(n-gated, q <= 0.10, never fabricated on thin data).
- Factor channels (FACTOR:<key>) fold into the period-narrative discovery
matrix as both lag source and lag target, so the drivers the coach and
narrative reference can cite a factor relationship.
An inverse factor (higher rating = worse day) gets the documented sign-flip
once, at the series boundary, so "up" always reads as a better day and the
low/high split and phrasing stay honest. Strings land in all six locales;
the surface carries the standing "association, not cause" caption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two changes that turn the descriptive relations surfaces into a
statistically-gated discovery view on the mood page.
Mood as an outcome: the FDR-controlled all-pairs correlation discovery
engine treated mood only as a behaviour (a lag source). Promote it to an
outcome channel too, so the scan also surfaces "behaviour today → next-day
mood" relations (e.g. more daylight today → better mood tomorrow) alongside
the existing "mood today → next-day outcome" direction. The self-pair is
skipped and Benjamini-Hochberg already controls the larger pair family this
opens up. The discovery route now feeds the shared mood daily-mean series
into both roles.
Surface the discovery slice: the engine was computed but never rendered.
The mood Insights page now shows the mood-involving pairs that survive the
false-discovery correction, each phrased observationally with its paired-day
count, r, and BH q-value, plus the honest "X of N tested" footer. The
surface self-fetches the correlation route and degrades to nothing when the
operator has disabled it. Observational only, with the standing disclaimer.
Register four new derived metrics for the additive HealthKit signals:
- WRIST_TEMPERATURE_BASELINE, STAIR_ASCENT_SPEED_BASELINE,
STAIR_DESCENT_SPEED_BASELINE — any-user-baseline personal bands reusing
the type-generic median +/- k*MAD engine via a new BASELINE_CAPABLE_TYPES
allowlist. Wrist temperature is a personal-deviation band only (no illness
or cycle inference); stair speeds are personal trend bands (no population
cutoff, geometry-confounded).
- SIX_MINUTE_WALK_BAND — passthrough re-frame placing the device-estimated
distance against the Enright & Sherrill 1998 predicted 6MWD (ATS 2002
test standard), as percent-of-predicted with a green/yellow/red band;
band null when age/height/weight/sex are incomplete.
Fall count and the breathing-disturbance index stay trend-only by design,
documented in the registry: fall count is a zero-inflated discrete safety
event, the breathing index is a regulated screening signal where a derived
band would imply a diagnosis.
Note wrist temperature as a deferred correlation outcome channel held
pending a privacy review (cycle-phase inference risk), mirroring the
medication-compliance omission note.
Add provenance + standards entries, method/caveat copy across all six
locales, and tests for the engine routing, the Enright equation, and the
percent-of-predicted band.
Build the composite tier of the derived-metrics layer on the shared seam:
- Sleep score — a transparent 0–100 composite from sufficiency, efficiency,
consistency, timing and composition sub-scores over the per-stage sleep
rows, reweighting around any sub-score the data cannot support (a legacy
asleep-only night drops composition gracefully rather than fabricating it).
- Wellness/readiness index — extends the health-score null-redistribution
blend with resting-HR, HRV (SDNN), sleep, respiratory-rate and mood
deviation-from-baseline components; returns insufficient below two present
components so it never headlines from one signal.
- Coincident-deviation flag — counts vitals outside their personal band on
the latest day and fires when two or more coincide, listing the
contributing vitals as possible factors, never a cause or a diagnosis.
Each composite cites its standard, carries coverage + confidence +
provenance, and reweights around missing inputs.
Add an FDR-controlled correlation discovery engine behind
GET /api/insights/correlations: it scans a curated behaviour x outcome
matrix, lag-joins each behaviour day to the next day's outcome, runs Pearson
with the exact Student-t p-value, and applies Benjamini-Hochberg control
across every tested pair so only statistically-defensible patterns surface —
each tagged with n, r, p and the adjusted q, framed descriptive not causal.
Add the score-anatomy detail view at /insights/scores/<metric>: a full
score ring, ranked contributor rows each showing its push/pull and weight
share, and the cited-standard provenance explainer — reused by every
composite, mobile-first and accessible.
Regenerate the OpenAPI contract for the new metric values + the correlation
discovery response, and add the i18n keys across all six locales.