diff --git a/.planning/STATE.md b/.planning/STATE.md index 553d578b3..fe3966d4c 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -125,12 +125,21 @@ commit. ### B3 — Correlation discovery + Trends row with AI annotations (~3-5 days) -- [ ] 3 pre-defined hypotheses: BP×compliance, mood×pulse, +- [x] 3 pre-defined hypotheses: BP×compliance, mood×pulse, weight×weekday -- [ ] New `src/lib/insights/correlations.ts` -- [ ] New `src/components/insights/{correlation-card,trends-row, - trend-annotation}.tsx` -- [ ] Confidence ≥ 0.95, n ≥ 14, conservative phrasing +- [x] New `src/lib/insights/correlations.ts` (Pearson + weekday-ANOVA; + n >= 14 + p < 0.05 surfacing gate) +- [x] New `src/components/insights/{correlation-card,correlation-row, + trends-row,trend-annotation}.tsx` +- [x] Confidence interval via Fisher z-transform; conservative + phrasing locked in by code-review convention +- [x] PROMPT_VERSION 4.20.0 → 4.20.1; new `trendAnnotations` block + on the AI response schema (nullable + optional for legacy + payloads) +- [x] `/api/analytics` now emits `correlations: {bpCompliance, + moodPulse, weightWeekday}` server-side +- [x] `` + `` mounted on `/insights` + between Daily Briefing and the Advisor card - Detailed report: `.planning/phase-B3-report.md` ### B4 — Weekly Report + Storyboard + Mobile passes (~3-4 days) diff --git a/.planning/phase-B3-report.md b/.planning/phase-B3-report.md index b46c4f469..5eaf38c1a 100644 --- a/.planning/phase-B3-report.md +++ b/.planning/phase-B3-report.md @@ -1,89 +1,147 @@ -# Phase B3 — Admin System-Status host-load chart (v1.4.16) +# Phase B3 — Correlation discovery + Trends row with AI annotations (v1.4.20) -Marc asked for a 2-hour host-load graph above /admin/system-status — CPU, -memory, disk-IO. Phase ran in Wave-B in parallel with B4/B5a/B7 and the -verification gate. +Three pre-defined hypothesis cards (BP × medication compliance, mood × +resting pulse, weight × weekday) and an AI-annotated Trends row land +on `/insights` between the Daily Briefing and the Advisor card. +Six atomic commits on `origin/develop`. ## Approach -Picked Option B (in-process sampler) over Option A (Coolify Sentinel -scrape). Coolify exposes no clean read endpoint for per-server metrics -without HTML scraping or private RPC; an `os.loadavg()` + `os.totalmem()` +Scope discipline locked in early: v1.4.20 ships **3 pre-defined +hypotheses**, NOT general automated correlation discovery. Full +metric-pair scan with FDR control is v1.5/v1.6 work. -- /proc/diskstats sampler is self-contained, costs one row per minute, - and works on macOS dev as well as Linux production. +Quality bar non-negotiable per the strategic plan: + +- `n >= 14` paired data points +- `p < 0.05` + +Below the bar each card paints an `` rather than a +near-zero scatter that would imply a pattern that isn't there. ## Deliverables -1. **`HostMetric` model + migration `0032_host_metric`** — - `prisma/schema.prisma`, indexed by `captured_at`. Disk fields nullable - so non-Linux hosts don't fail. -2. **Per-minute sampler** — `src/lib/jobs/host-metric-sampler.ts`. Reads - `os.loadavg()`, `os.totalmem() - os.freemem()`, and (Linux) - /proc/diskstats. Skips loop/ram/dm-/md/sr/fd/nbd/zram pseudo-devices. - Wired into `reminder-worker.ts` with `* * * * *` cron. 7-day retention - enforced inside the worker; env override `HOST_METRIC_RETENTION_DAYS` - capped at >= 1 day. -3. **`GET /api/admin/host-metrics?since=2h`** — `requireAdmin()`-gated. - Returns `{ samples, meta }`. Server-side BPS derivation across - consecutive samples handles counter resets (host reboot) by emitting - `null` instead of negative deltas. Six `since` presets: 30m / 1h / 2h - (default) / 6h / 12h / 24h. -4. **`` component** — - `src/components/admin/host-metrics-chart.tsx`. Recharts wrapper with - load (yellow, left axis) + memory % (cyan, right axis) + disk-IO % - (purple, right axis, hidden when no disk data). `next/dynamic` import - from the consumer keeps the Recharts bundle off the rest of the admin - panel. 60s `refetchInterval` matches sampler cadence. -5. **System-status section wiring** — `system-status-section.tsx` now - wraps the existing card in a `space-y-6` flex with the chart above. -6. **i18n** — 9 EN+DE keys under `admin.hostMetrics.*` (title, load1, - memUsedPercent, diskBusyPercent, diskReadBps, diskWriteBps, - last2hours, empty, loadError). +1. **`src/lib/insights/correlations.ts`** — pure runners (`pearson`, + `weekdayAnova`, `correlateBpCompliance`, `correlateMoodPulse`, + `correlateWeightWeekday`). 95 % CI on r via Fisher z-transform; + eta-squared as the effect-size statistic for the weekday case. + Two-sided p-values from inline Wilson-Hilferty (F) + + Abramowitz-Stegun (z) approximations — no new dependencies. + 16 unit tests pin positive / negative / no-correlation / under-n / + non-significant / no-variance branches and the Monday-spike + weekday flag. + +2. **`aiInsightResponseSchema` + prompt rule 9** — added a nullable + + optional `trendAnnotations: { bp?, weight?, mood? }` block (each + string ≤ 200 chars). PROMPT_VERSION 4.20.0 → 4.20.1. EN + DE + ground rule 9 explicitly bans causal language and caps the + sentence length. 13 schema + prompt-shape tests. + +3. **`` + ``** — pure presentational. Row + is 1-column on `=md`. Annotations show a + `Sparkles` icon + the AI sentence + an optional confidence chip + (low / moderate / high). Empty state hints at the gap without + breaking the row's visual rhythm. 15 component tests. + +4. **`` + ``** — card has tone-coloured + left bar, scatter sparkline (deferred-bundled via the existing + `` wrapper), confidence chip, source-chip + ("based on N paired readings · last 30 days"), interpretation + prose, disabled "Try a 7-day experiment" CTA with `title="Coming + soon"`. Row mounts the three cards in 2-up grid on `>=md` and + single column on `` mounts above `` + between Daily Briefing and the Advisor card. The advisor hook + lifts `trendAnnotations` from the cached AI payload through + `trendAnnotationsSchema.safeParse` so legacy 4.20.0 payloads + resolve to null and trigger the per-metric empty hint. 10 + source-scan tests pin imports, JSX mounts, and the wide-event + annotation. ## Tests -- 8 unit tests for the sampler (env retention parsing, disk-stats null - - throw fallbacks, create + deleteMany call shape). -- 6 integration tests against testcontainer Postgres for the API - endpoint (auth gating, 2h default window, BPS derivation across a - counter reset, `?since=30m` preset, empty-DB happy path). -- 5 component tests for the chart helper + loading skeleton render. -- All 13 unit + 6 integration tests green; i18n parity test green. +- vitest unit suite: 1833 → 1907 (+74 net) +- integration suite: 67/67, no regression +- typecheck + lint clean (no new warnings introduced) -## Cross-agent collisions +## Synthetic-data spot checks -The verification-gate stash/restore loop in this marathon repeatedly -wiped my untracked files (chart component + tests) and hijacked my -`git commit` invocations so commits 4-6 ended up landing under another -agent's commit message (`8d9f864 docs(planning): mark Wave-B B5a …`). -Functionally everything is on origin/main; commit attribution is -muddled. Commits 1-3 (db, jobs, api) landed cleanly under my titles -(`5d1ece1`, `f1bd801`, `2877710`). Commit 3 (`2877710`) is missing the -Co-Authored-By trailer due to a one-line oversight in my heredoc. +- `correlateBpCompliance({...})` for the canonical 20-row strong-fit + fixture (compliance 50→97.5 %, systolic 150→120) → + `status: "ok"`, `statistic ≈ -1.0` (perfect linear), `pValue < 0.001`, + confidence band label `"high"`, n = 20. +- `correlateMoodPulse({...})` for the 20-row low-mood/high-pulse + fixture → `status: "ok"`, negative `statistic`, p well under 0.05. +- `correlateWeightWeekday({...})` for the 4-week Monday-spike fixture + (Monday ≈ 84 kg, every other day ≈ 80 kg) → `status: "ok"`, + `outlierIndex = 0`, eta² > 0.14 (Cohen "large"), interpretation + flags Monday with the conservative "a pattern worth watching" + phrasing. -Recommendation for v1.4.17: spawn each agent in its own git worktree -(`superpowers:using-git-worktrees`) to eliminate the shared-cwd -staging race entirely. The current marathon's verification gate keeps -collapsing concurrent untracked files into shared stashes, which makes -even simple `git add path/to/file && git commit` racy. +## Did NOT ship -## Files added / modified (final state on origin/main) +- **Real "Try a 7-day experiment" scheduling.** The CTA is intentionally + disabled in v1.4.20 with the Coming-soon tooltip; live scheduling is + a v1.5 surface. +- **Storyboard / 90-day BP timeline annotations.** Still B4 work. +- **Weekly Report `/insights/report/[week]` route.** Still B4 work. -- `prisma/schema.prisma`, `prisma/migrations/0032_host_metric/migration.sql` -- `src/lib/jobs/host-metric-sampler.ts` + `__tests__/host-metric-sampler.test.ts` -- `src/lib/jobs/reminder-worker.ts` (sampler queue registration) -- `src/app/api/admin/host-metrics/route.ts` -- `tests/integration/admin-host-metrics.test.ts` -- `src/components/admin/host-metrics-chart.tsx` + `__tests__/host-metrics-chart.test.tsx` -- `src/components/admin/system-status-section.tsx` (wiring) -- `messages/en.json`, `messages/de.json` (admin.hostMetrics.\*) -- `tests/integration/setup.ts` (truncate `host_metrics`) +## Decisions that may want maintainer review -## Did NOT touch (scope guards) +- **Confidence-band-from-CI mapping.** `width(CI) <= 0.4` → "high", + `<= 0.8` → "moderate", else "low". The thresholds are heuristic; if + the user-facing chip ends up too aggressive (e.g. "high" pops on + noisy small-n results) we can tighten to 0.3 / 0.6. +- **Eta-squared confidence band for weekday.** No closed-form CI on + eta² without a non-central F distribution we don't carry. Mapped + via Cohen conventions (`eta² >= 0.14` → "high", `>= 0.06` → + "moderate") with an n-driven nudge. If marathon QA flags this as + too lenient we can drop the nudge or surface "n-low" as its own + pseudo-band. +- **Interpretation phrasing.** Locked to a small set of conservative + templates inside the runners — never reaches the AI provider — + so the EN strings live in `correlations.ts` rather than i18n. If + Marc wants German-locale interpretations, we'd need to localise + these or move them through `t()`. +- **Trend-annotation surface gating.** The annotation block is + optional — if the model omits a metric, the empty hint surfaces + ("Awaiting more data to annotate this trend"). For the legacy + (4.20.0) cached payload everyone sees the empty hint until they + click Regenerate. Opt: we could auto-regenerate once on first + page-mount post-deploy; opted not to so users keep their existing + cache TTL. +- **Inline ANOVA p-value approximation.** Wilson-Hilferty for the + F-distribution is good enough for the surfacing decision, but a + reviewer should sanity-check against a small-df edge case if Marc + is worried about precision near `p ≈ 0.05`. -- `src/lib/ai/`, `src/components/insights/`, - `src/components/charts/scatter-correlation-chart.tsx` (B5a/B5c-e) -- `src/components/settings/*` (B2/B6/B7) -- audit-log section (B4) -- No new dependencies — Recharts pattern reuse only. +## Files added / modified + +- `src/lib/insights/correlations.ts` (+ + `__tests__/correlations.test.ts`) +- `src/lib/ai/schema.ts` — `trendAnnotationsSchema` + integration +- `src/lib/ai/prompts/insight-generator.ts` — PROMPT_VERSION 4.20.1 + + GROUND RULE 9 (EN + DE) + JSON shape entry +- `src/lib/ai/__tests__/trend-annotations-schema.test.ts` +- `src/components/insights/trend-annotation.tsx` + + `__tests__/trend-annotation.test.tsx` +- `src/components/insights/trends-row.tsx` + + `__tests__/trends-row.test.tsx` +- `src/components/insights/correlation-card.tsx` + + `__tests__/correlation-card.test.tsx` +- `src/components/insights/correlation-row.tsx` + + `__tests__/correlation-row.test.tsx` +- `src/app/api/analytics/route.ts` — three runners + Berlin-day + bucketing helpers +- `src/app/insights/page.tsx` — mounts CorrelationRow + TrendsRow +- `src/components/insights/use-insights-advisor.ts` — lifts + trendAnnotations from the cached payload +- `src/app/__tests__/insights-b3-wiring.test.ts` +- `messages/en.json`, `messages/de.json` — + `insights.{trendsRow,trendAnnotation,correlationRow}` keys