Radix Sheet paints its default close-X at absolute top-4 right-4. The
v1.4.20 drawer header parked the settings cog inside the right-edge
button cluster (next to the New chat button), so the close-X overlay
visually swallowed the cog — the icon still rendered but read as
unclickable.
Move the cog to the left side, immediately after the gradient
avatar, so the right edge is reserved for Radix's close-X alone. Add
pr-12 / sm:pr-14 padding to the header so the New chat button can
never slide under the close-X on narrower viewports either.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After an SSE `done` frame the streaming hook keeps `streaming.content`
populated to support the in-flight render path, then fires a TanStack
invalidate that pulls the persisted assistant message into
`conversation.messages`. Until the next `send` reset the message
thread therefore rendered the assistant reply twice — once from the
streaming bubble and once from the persisted message — until the
drawer was closed and reopened (which is when Marc noticed the bug
went away).
Suppress the streaming bubble at render time as soon as the persisted
twin lands. The match keys on `streaming.messageId`, which the SSE
`done` frame fills in once the row is on disk; mid-stream the field
is null so the in-flight bubble stays visible.
Add two regression tests: one that asserts dedup-on-id works and one
that pins the still-streaming render path so the fix doesn't trade a
duplicate-bubble bug for a no-bubble bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Coach prompt only said "ground every number in the
SNAPSHOT" without telling the model that the snapshot now carries a
timeline.recent block of day-level rows with weekday labels.
Without that pointer the model kept defaulting to aggregate-only
phrasing on day-specific questions.
Add a DAY-LEVEL READINGS section to both EN and DE prompts:
- explain the timeline.recent / timeline.weekly shape
- tell the model to answer day-specific or weekday-specific
questions out of timeline.recent and to cite the actual reading
with date + weekday
- when no row exists for the day named, say so plainly and offer
the closest available day rather than the aggregate
- older weekday questions fall back to timeline.weekly
- the scope.sources field is the conversation's source-allowlist;
a question about a non-listed metric should be acknowledged as
out-of-scope rather than answered from the aggregate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Coach snapshot used to ship only aggregated statistics (mean,
slope, SD, range, count) per metric. A turn that asked
"why was BP higher on Monday?" had no day-level rows to ground in
and the model fell back to apologetic aggregate-only phrasing
("from the snapshot I cannot isolate the specific Monday").
Extend buildCoachSnapshot to fold in:
- timeline.recent: one row per UTC day for the last 14 days, each
tagged with its weekday so the model can answer day or weekday
questions directly. BP rows pair systolic + diastolic; days with
only one side measured are dropped to avoid fabricating a
complement.
- timeline.weekly: ISO-week buckets covering days 15..N where N is
the configured window. Keeps the prompt budget tight on 30/90-day
windows while still letting the Coach cite older weeks.
- per-day medication adherence rows from MedicationIntakeEvent so
the Coach can answer "did I miss my dose Tuesday?" without
inventing the schedule.
Also add the optional `scope` field to coachChatRequestSchema (sources
+ window) so the upcoming sources-rail picker can narrow the snapshot
without breaking older clients — missing fields fall through to
all-source last30days.
A typical 30-day, 5-source snapshot lands around 2.4-2.8 KB JSON
(≈700-900 prompt tokens) — comfortably inside the 25k/day Coach token
budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /api/insights/generate route was still calling the legacy
getInsightsSystemPrompt that returns the v1.4.5 {changed, stable,
drivers, …} shape and never asks the model for a dailyBriefing block.
GROUND RULE 8 (emit dailyBriefing when the snapshot has signal) lives
on getStrictInsightsSystemPrompt in src/lib/ai/prompts/insight-
generator.ts; the route never adopted it. Re-runs from the hero strip
therefore stored a payload with no briefing, the use-insights-advisor
lift saw null, and the briefing card painted its empty state forever.
Switch the route to getStrictInsightsSystemPrompt(locale). Existing
strict-shape tolerance is unchanged: insightResultSchema.safeParse
falls through to the raw parsed body on shape mismatch, and
aiInsightResponseSchema's passthrough() keeps any custom keys for
forward compat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase E ticked complete in STATE.md; v1.4.20 archived to ROADMAP
"Previous milestone" with completion timestamp. Top of ROADMAP
scaffolds v1.4.21 with carry-over candidates from
.planning/v1421-backlog.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 commits landed on develop covering every Wave-D HIGH (13/13), the
design pushback on the disabled weekly-report button, 6 MED items,
and every simplify-apply-yes item. 22 MED + 16 LOW + 4 simplify-
apply-maybe deferred to v1421-backlog under a new "Phase D — v1.4.20
reconcile carry-over" section. Tests unchanged at 2026 unit + 81
integration; typecheck + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both EN + DE have heroGreetingNight pointing to the same string as
heroGreetingEvening (the night bucket maps to "Good evening" so 03:00
doesn't read as Morgen). Drop the duplicate key and fall through to
heroGreetingEvening in the resolver — the resolver decided the
collapse, the JSON catalog carried dead weight.
Fixes Design-M2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- weekly-report eyebrow chip drops uppercase + tracking-wide for the
sentence-case rule the rest of the app follows; print stylesheet
now neutralises bg + colour so monochrome output reads clean
(Design-M4, Design-L3).
- correlation-card "Try a 7-day experiment" CTA flips from default to
outline while disabled. Disabled-primary read as "the system is
broken right now"; outline reads as "this is on the roadmap"
(Design-M7).
- coach-drawer settings cog aria-label switches to the coming-soon
tooltip copy so SR users hear the same "arrives in v1.4.21" message
sighted users see (Design-M9).
- sources-rail static "fresh" dot becomes aria-hidden — v1.4.20 has
no real freshness state behind it; SR users no longer hear "Fresh"
five times in a row (Design-L4).
- summariseTitle slices on Unicode code points rather than UTF-16
code units so a user message ending mid-emoji ("🩺") no longer
produces a half-character on disk (Code-LOW-04).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three live byte-identical formatRelativeTime copies (hero-strip,
daily-briefing, coach history-rail) lifted to src/lib/i18n/relative-
time.ts. Both correlation-card and trend-annotation duplicated the
exact same low/moderate/high Dracula classnames; extracted to
src/components/insights/confidence-badge.ts (per-component i18n
labels stay local — the copy differs).
Drop the dead "null" placeholder branch in useCoachConversation's
queryKey: the ternary picked a different key when id was null, but
enabled: id !== null kept the query disabled so the placeholder never
fired. Key directly on id.
Hoist STORYBOARD_COLOR_BY_CATEGORY in /insights/page.tsx to module
scope so the map isn't recreated on every render.
Fixes Simplify-S-02, Simplify-S-03, Simplify-S-04, Simplify-S-05,
Code-LOW-02.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase D security review flagged a "Marc's data (572 paired
readings, …)" comment in the analytics route. It predates v1.4.20
but the file received new commits in this release, so neutralise the
example to a generic "busy account" wording that conveys the same
shape without leaking a maintainer name.
Fixes Security-LOW-4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero band's glow-purple box-shadow bled through the sticky
section nav (bg-background/80 + backdrop-blur) so the nav painted
with a faint purple cast on scroll. Add `isolate` to the hero
container so the shadow stays z-trapped inside its own stacking
context.
Fixes Design-HIGH-4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The drawer's lg:max-w-[1080px] cap left only ~200-286px of underlying
/insights visible on common laptop viewports (1280-1366px), turning
the "keep dashboard context behind it" sheet into a near-takeover.
Drop the lg cap to min(960px,75vw) with the inline sources rail
hidden below xl — the rail still surfaces on lg via the existing
mobile chevron tab. xl+ restores the full three-column layout. The
history rail stays visible inline on lg+ (it's the more frequently
referenced of the two).
Update the SSR test contract to match: history trigger hides on lg+,
sources trigger hides on xl+.
Fixes Design-HIGH-1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The streaming assistant bubble updated in place as the SSE tokens
landed but had no aria-live region — sighted users saw live text
while screen-reader users heard silence. Wrap the in-flight bubble
in role=log + aria-live=polite + aria-relevant=additions text so the
assistant prose announces progressively (matches the daily-briefing
pattern that already uses aria-live for its loading state).
Fixes Design-HIGH-3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chips painted at ~28px tall (px-3 py-1.5 text-xs). On a 375px viewport
the chip strip became a fingertip lottery. Bump to min-h-9 with
px-3.5 py-2 text-[13px] so chips clear the 36px mobile touch baseline
without the type-shrink that triggered the regression.
Fixes Design-HIGH-2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B4 shipped /insights/report/[week] but the hero action-row
button stayed disabled with a "Coming soon" tooltip. Wire it into the
current ISO week so a click opens this week's report. Older parents
that don't pass weeklyReportHref still get the disabled placeholder.
Fixes Design pushback (item 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every POST /api/insights/chat called extractFeatures(userId, false),
which ran prisma.measurement.findMany({ where: { userId } }) with no
date filter — fetching every measurement the user had ever recorded.
For power users with multi-year Withings imports that's tens of
thousands of rows on every Coach turn.
Add an optional sinceDays bound to extractFeatures (default unchanged
= full history; analytics dashboard + insights/generate keep their
existing behaviour) and have the Coach snapshot pass sinceDays: 90.
The Coach already only narrates the last7 / last30 / last90 windows,
so the 90-day floor preserves every metric the snapshot surfaces.
Fixes Senior-HIGH-3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the withIdempotency wrap on POST /api/insights/chat. The wrapper
caches response bodies via cloned.text() then replays through
NextResponse.json(JSON.parse(...)) — turning the SSE wire format into
a null body on retry. The PWA never sends Idempotency-Key so the bug
is hidden today; the iOS client would walk straight into it. Dedup
still holds via the conversationId existence check + 20-turn cap.
- Read the SSE response body even on !response.ok when the content type
is text/event-stream so the structured error frame the route emits
surfaces its errorCode to the bubble. Without this the stream was
read only on 2xx and the rich error channel was dead code.
- Stabilise the useSendCoachMessage opts reference via useRef so an
inline-literal { onDone } from the drawer no longer re-memoises send
on every render.
- Drop the unused snapshot parameter from streamProviderError (the
ReturnType<typeof Object.assign> alias widened to any). Switch the
error response to status 200 so the streaming client reads the SSE
body and parses the error frame instead of branching on HTTP status.
Fixes Code-HIGH-1, Code-HIGH-2, Code-HIGH-3, Code-HIGH-4, Security-M-1,
Senior-HIGH-1, Simplify-S-01.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B5 ticked complete in STATE.md; added Wave-B summary table covering
B1–B5 commits (29 total) and cumulative test deltas (+354 unit,
+14 integration). Status block records the canonical sample score
(95, green) and the integration test count.
phase-B5-report.md replaces the v1.4.15 onboarding-tour stub with
a fresh write-up of the Health Score formula, its sub-component
helpers, the route wiring, the panel slots, and the 4 flagged-
uncertain items for maintainer review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero strip's right side, empty by design since B1, now paints the
<HealthScoreCard> when `analytics?.healthScore` is non-null. Layout
flips from `flex-col` to `flex-row` on `lg+` so the panel sits beside
the title block on desktop and stacks below on mobile.
The `onAskCoach` prop signature picks up an optional `prefill`
argument so the same handler powers both surfaces — the action-row
button passes nothing (drawer opens blank), the score panel passes
"Why is my health score X out of 100?". One drawer instance, one
state pair, one consistent UX.
Tests: 5 new hero-strip cases pin the score panel slot, the lg-row
layout switch, and the conditional ask-coach button mounting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure presentational <HealthScoreCard> for the right-side hero
panel. Surfaces the composite 0..100 score with a band-coloured
number, a band-tinted progress bar, four sub-component rows
(BP / Weight / Mood / Compliance) with their individual scores, a
"vs last week" delta line with arrow + chip, the "Indicative — not
a clinical assessment" disclaimer, and an "Ask the Coach" button
that pre-fills the drawer with "Why is my health score X out of
100?".
Layout: 220 px wide on desktop, full-width on mobile so it stacks
below the hero title block on `<lg`. Dracula band tokens
(`--dracula-green/orange/red`) carry WCAG AA contrast against
`bg-card/65`.
Tests: 17 SSR cases covering each band, delta-arrow shapes, the
null-component em-dash fallback, the German locale, the score-aware
prefill argument format, the progressbar role + ARIA values, and
the score-clamp at 0..100 % bar width.
i18n keys land under `insights.healthScore.*` in en.json + de.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route now builds the four-component Health Score input from the
already-fetched BP figure plus a fresh weight + mood + medication
intake fetch (last 30 days, plus a 7-day-shifted snapshot for the
delta). The response gains a `healthScore: { score, band,
components, delta } | null` field; null when no component is
computable so the UI hero panel can hide cleanly.
The wide-event annotation carries `healthScore.score` + `band` +
`delta` so admin-app-logs can attribute coverage without DB queries.
The previous-week BP rate reuses the all-time figure (it's a slow-
moving aggregate; rewinding it cheaply isn't possible without a
full historical re-pair). Delta therefore primarily reflects week-
over-week movement in weight / mood / compliance — the components
that actually move on a weekly cadence.
Integration coverage seeds three synthetic-data shapes against the
postgres testcontainer:
- strong-positive → green band (>= 75)
- empty user → null healthScore
- compliance-only → bp/weight/mood null, compliance weight 1.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composite 0-100 score with three bands (green >= 75, yellow 50..74,
red < 50). Server-deterministic so the same inputs always produce the
same output.
Sub-components:
- bpInTargetRate (30 %) — reuses the existing all-time figure
- weightTrendAlignment (20 %) — slope vs target band over 30 days
- moodStability (20 %) — `100 - CV*100`, requires >= 5 entries
- complianceRate (30 %) — mean of medication compliance30
Null components scale the remaining weights proportionally so the
score never lies about what was actually measured. Pure helpers
(linearRegressionSlope, coefficientOfVariation, weightTrendAlignment,
moodStability, complianceRate) live alongside the composite.
Tests cover strong/mixed/poor scenarios, every null-redistribution
shape (bp null, bp+mood null, all-but-compliance null, all null) and
determinism (same input -> same output).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five-commit Phase B4 lands the printable weekly report route, the
hero banner card surfacing fresh weekly reports, additive storyboard
annotations on the 90-day BP chart, and mobile rail trays for the AI
Coach drawer. PROMPT_VERSION 4.20.1 -> 4.20.2 with new GROUND RULES
10/11 covering both new schema blocks.
Tests 1902 -> 1975 (+73). Typecheck + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The history + sources rails were desktop-only since B2b. B4 surfaces
them on `<lg` via two chevron-button triggers anchored to the inside
edges of the message thread. Tapping a trigger opens a side <Sheet>
that hosts the same HistoryRail / SourcesRail instance the desktop
layout uses, so the user can browse conversations or check what data
the Coach is reading without losing message-thread context.
Refactors the body grid into a separate <CoachDrawerBody> presentational
component so the SSR test harness can pin the trigger slots + their
lg:hidden Tailwind variant without rendering the outer Radix Sheet
portal (which is client-only).
Selecting a conversation from the mobile history tray closes the tray
automatically so the user lands back on the message thread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an additive annotations[] prop to <HealthChart> that paints a
vertical Recharts <ReferenceLine> per AI-narrated event on the
underlying timeline. The line + tooltip + comparison overlay remain
untouched — annotations sit between the cartesian grid and the data
lines so they read as orientation, never as data.
The /insights page wires advisor.payload.insights.storyboardAnnotations
into the 90-day BP chart, mapping the four canonical categories to
Dracula colours (medication=pink, event=cyan, milestone=green,
warning=orange).
Pure helper resolveAnnotationPositions() handles the date->pointIndex
mapping with a 7-day snap window (so annotations don't stack on
monthly buckets) and a 24-char truncation for the small-viewport
label. The unit suite pins exact-day pinning, near-day snapping,
out-of-window dropping, and label truncation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a slim banner-card on <HeroStrip> that surfaces a fresh weeklyReport
between the title block and the action row. Three actions:
- Read -> Next.js Link to /insights/report/[week]
- Share -> navigator.share() with clipboard fallback + sonner toast
- Export PDF -> deep-link to the report URL with ?print=1 so the report
page auto-fires window.print() after first paint
The banner only renders when the parent passes weeklyReportReady; the
insights page derives that prop from advisor.payload.insights.
weeklyReport so the banner appears the moment a fresh PROMPT_VERSION
4.20.2 generation lands.
EN + DE translations under insights.heroBanner.* (Read / Share / Export
PDF + share success/failure toasts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a newsletter-style report route at /insights/report/[week] that
parses the YYYY-Www slug, reads the cached weeklyReport block off the
AI advisor payload, and renders Summary / Going-well / Worth-watching /
Tips / Data-quality sections in a print-friendly layout.
Print export uses window.print() with Tailwind print: variants on the
toolbar (hidden) and article wrapper (full-width margins). The route
honours ?print=1 to deep-link the export PDF flow from the upcoming
hero banner card.
Includes:
- src/lib/insights/week-iso.ts — parseWeekISO / toWeekISO / weekISOToRange
- WeeklyReportView wrapper + WeeklyReportPresentation pure layer
- Empty-state with Generate CTA when the cached report's weekISO
doesn't match the route param
- EN + DE i18n keys under insights.report.* and insights.heroBanner.*
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend aiInsightResponseSchema with weeklyReport (ISO week + summary +
goingWell/worthWatching/tips bullets + optional dataQualityNotes) and
storyboardAnnotations (date + label + category + detail; max 20).
Both blocks are nullable + optional so legacy 4.20.1 caches round-trip.
Bump PROMPT_VERSION 4.20.1 -> 4.20.2 and add GROUND RULE 10 (weekly
report sections match layout, conservative phrasing, no causal claims)
+ GROUND RULE 11 (storyboard annotations are factual user-logged events
with neutral prose) to both EN and DE prompts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tick B3 in `.planning/STATE.md` against the six commit drops on
`develop` (correlation maths, schema + prompt rule,
TrendAnnotation + TrendsRow, CorrelationCard + CorrelationRow,
analytics route + page wiring, this report). Replace the stale
v1.4.16 host-load report with the v1.4.20 narrative — what
shipped, synthetic-data spot checks for each hypothesis, and
flagged-uncertain items for maintainer review (confidence-band
thresholds, eta-squared mapping, interpretation localisation
strategy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compute the three pre-defined correlation hypotheses
(BP × compliance, mood × pulse, weight × weekday) on the trailing
30-day window in `/api/analytics` and surface the typed result
shapes on the response payload. Each runner is gated on n >= 14
+ p < 0.05; below the bar the result.status === "insufficient"
and the per-card EmptyState paints in place of a misleading
near-zero scatter.
The insights page now mounts:
- `<CorrelationRow>` between the Daily Briefing and the Advisor
card — the layout slot the artboard reserves for the patterns
surface.
- `<TrendsRow>` immediately below it, fed from
`advisor.payload.trendAnnotations` (lifted client-side via
`trendAnnotationsSchema.safeParse`). Legacy cached payloads
predating PROMPT_VERSION 4.20.1 surface the per-metric
empty-state hint instead of a half-rendered annotation.
The wide-event annotation captures per-hypothesis status so admin
observability can attribute coverage gaps without re-querying
the DB.
10 source-scan tests pin the load-bearing imports, the JSX mounts,
the analytics-route surfacing, and the malformed-payload null
fallback in the advisor hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cards-and-row primitives for the Insights redesign:
- `<CorrelationCard>` — single hypothesis. Tone-coloured left bar,
scatter sparkline (deferred-bundled), confidence chip, source
chip ("based on N paired readings · last 30 days"), interpretation
prose, and a disabled "Try a 7-day experiment" CTA with a
Coming-soon tooltip. Below-threshold results render the
EmptyState in place of the chart so we never paint a misleading
near-zero scatter.
- `<CorrelationRow>` — 2-up on `>= md`, single column on `< md`.
Row-level correlation-disclaimer ("Patterns are observational,
not causal — talk to your doctor if BP runs high.") sits below
the card grid once per row, not per card.
13 tests cover OK + insufficient rendering, the disabled CTA's
tooltip wiring, the confidence chip variants, the disclaimer
position, the responsive grid, and the EN / DE locale switch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two presentational pieces for the Insights redesign:
- `<TrendAnnotation>` — pure renderer for the AI-authored
one-sentence annotation that sits below each Trends-row chart.
Empty state is locale-aware ("Awaiting more data to annotate
this trend"). Optional confidence chip surfaces a discrete
low / moderate / high band.
- `<TrendsRow>` — 3-up responsive grid (1 column < md, 3 columns
>= md) wrapping BP / Weight / Mood mini charts with their
annotations. Recharts ships through the existing `next/dynamic`
defer-load pattern so the bundle savings stay intact.
Adds the EN + DE i18n keys (trendsRow, trendAnnotation,
correlationRow). 15 unit tests cover the locale switch, the
empty-state hint per metric, the confidence chip variants, and
the row layout chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend `aiInsightResponseSchema` with an optional, nullable
`trendAnnotations: { bp?, weight?, mood? }` block — one short
sentence per metric, capped at 200 characters. Each field is
independently optional so the model emits only the metrics it can
ground in the snapshot.
Insight-generator prompt picks up GROUND RULE 9 (EN + DE):
single-sentence, observational, ≤ 200 chars, banned causal
phrases enumerated. PROMPT_VERSION bumps 4.20.0 → 4.20.1 to
signal the format change so cached payloads from B1 still parse
through the `.nullable().optional()` field.
13 tests cover the boundary (200 chars), the empty-string
rejection, the legacy-payload round-trip, and the EN/DE prompt
rule presence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `src/lib/insights/correlations.ts` with three pre-defined
hypothesis runners (BP × medication compliance, mood × resting
pulse, weight × weekday) plus pure Pearson + one-way weekday-ANOVA
helpers. Each runner gates the surface on n >= 14 paired rows AND
p < 0.05; below the bar a typed `insufficient` result lets the
card render an EmptyState rather than a misleading pattern.
Interpretations stay conservative ("a pattern worth watching" —
never "X causes Y"). 95 % confidence intervals come from a Fisher
z-transform on r and a discrete band-from-eta-squared mapping for
the weekday case. No new dependencies — the t-distribution +
F-statistic p-value approximations are inlined (Wilson-Hilferty +
Abramowitz-Stegun).
16 unit tests cover positive / negative / no-correlation / under-n /
non-significant / no-variance branches and the Monday-outlier
weekday flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B2b drawer UI shipped in four atomic commits on develop:
- TanStack Query hooks + SSE streaming client
- Drawer shell + message thread + source chips
- Composer + history rail + sources rail
- /insights wires the drawer via the hero strip's "Ask the coach"
button + suggested-prompt chips
Tests: 1781 → 1833 unit (+52). Integration unchanged at 78.
Six new test files cover the streaming parser, every drawer column,
the thread state machine, locale-aware labels, and the page wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>