diff --git a/.planning/STATE.md b/.planning/STATE.md index 55da2e290..fdd40589a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,7 +1,7 @@ # v1.4.22 marathon — state log -Status: Wave 4 — backlog cleanup + bug closure complete -Last update: 2026-05-10T21:45+02:00 +Status: Wave 5 — multi-agent QA + reconcile complete +Last update: 2026-05-10T22:30+02:00 > Previous patch: v1.4.21 live (image digest > `sha256:4e818d44702c…`, `/api/version=1.4.21`). @@ -172,13 +172,13 @@ Apply or confirm-defer every item: ## Wave 5 — Multi-agent QA + Product-Lead review -- [ ] code-reviewer -- [ ] security review -- [ ] design / UX review -- [ ] senior-dev review -- [ ] simplify -- [ ] Product Lead — v1.5 plan refresh based on v1.4.22 state -- [ ] Reconcile applied CRITICAL + HIGH; defer MED/LOW with reasoning +- [x] code-reviewer (`.planning/phase-W5-v1422-code-review.md` — 0 CRIT, 2 HIGH, 4 MED, 5 LOW) +- [x] security review (`.planning/phase-W5-v1422-security-review.md` — 0 CRIT, 0 HIGH, 2 MED, 4 LOW) +- [x] design / UX review (`.planning/phase-W5-v1422-design-review.md` — 0 CRIT, 3 HIGH, 7 MED, 5 LOW) +- [x] senior-dev review (`.planning/phase-W5-v1422-senior-dev-review.md` — 0 CRIT, 2 HIGH, 5 MED, 4 LOW) +- [x] simplify (`.planning/phase-W5-v1422-simplify-review.md` — 5 apply-yes, 4 apply-maybe, 6 apply-no) +- [x] Product Lead — v1.5 plan refresh based on v1.4.22 state (`.planning/phase-W5-v1422-product-lead-review.md`) +- [x] Reconcile applied CRITICAL + HIGH; defer MED/LOW with reasoning (10 commits; report at `.planning/phase-W5-v1422-reconcile-report.md`; backlog at `.planning/v1422-backlog.md`) ## Wave 6 — Release v1.4.22 diff --git a/.planning/phase-W5-v1422-reconcile-report.md b/.planning/phase-W5-v1422-reconcile-report.md new file mode 100644 index 000000000..6a1a700bf --- /dev/null +++ b/.planning/phase-W5-v1422-reconcile-report.md @@ -0,0 +1,108 @@ +# Wave 5 reconcile — v1.4.22 + +Branch: `develop` (10 reconcile commits ahead of `363e73c`). +Date: 2026-05-10. +Inputs: six W5 reviews + product-lead strategic memo. + +## Summary + +- 0 CRITICAL findings across all reviews. +- 7 HIGH findings — all applied inline. +- ~6 MEDs applied inline (judgment-pick high-leverage). +- Remaining MED + LOW deferred to `.planning/v1422-backlog.md`. + +## Commits + +| # | SHA | Title | Cross-ref | +|---|-----|-------|-----------| +| 1 | `75ccc01` | fix(coach): graceful fallback when sentinel-only output strips empty | Code-H1 | +| 2 | `ef597bf` | fix(insights): align BD-Zielbereich delta math with comparison window | Code-H2 | +| 3 | `b1ce6eb` | refactor(auth): fold onboarding-cookie write into createSession + destroySession | Sr-H1 | +| 4 | `2075d39` | refactor(api): extract createSseStream helper from chat route | Sr-H2 | +| 5 | `a2e24b1` | a11y(insights): polish sticky section nav (aria-current, focus, motion-reduce) | Design-H1, Design-H3, Code-MED-4, Design-LOW-4 | +| 6 | `651a529` | fix(insights): collapse BP tile to single secondary row at =sm` keeps the full layout. +- **Design-H3** ✅ medical disclaimer pinned at bottom of message thread (always visible regardless of viewport); rail footer kept for desktop redundancy. + +### MED (applied) + +- **Sec-MED-1** ✅ `hl_onboarding` cookie `SameSite=Strict`. +- **Sec-MED-2** ✅ `/onboarding` exact-match + subroute guard. +- **Code-MED-2** ✅ dead i18n keys (`coach.settings`, `coach.settingsTooltip`) dropped. +- **Code-MED-3 (Berlin tz)** ✅ `berlinDayKey()` lifted to `src/lib/analytics/berlin-day.ts`; targets sparkline + analytics route share the helper. Four new unit tests for DST + UTC-midnight edge cases. +- **Code-MED-3 (streaming race)** ✅ 150 ms grace window on message-thread suppresses the persisted twin while the streaming bubble is still in flight. +- **Code-MED-4** ✅ IntersectionObserver picks highest-intersection entry; folded into Design-H1 commit. +- **Code-LOW-5** ✅ `queryKeys.dashboardWidgets()` centralised; three call sites migrated. + +### MED (deferred) + +All other MEDs documented in `.planning/v1422-backlog.md` under "Wave 5 reconcile carry-over": + +- Sr-M1..M5 (analytics-route unbounded findMany; targets-route 7-pass sparkline; targets-page render mutation; provenanceFromJson hand-rolled validation; sentinel parser malformed enum). +- Design-M1..M3, M5..M7 (coach evidence aria semantics; sparkline min-points threshold; severity badge eyebrow + i18n; row-fill heuristic; live-preview affordance; sparkline aria sentiment). +- Code-MED-1 already applied; Code-MED-2 already applied; Code-MED-3 already applied (×2); Code-MED-4 already applied. + +### LOW (deferred) + +All LOWs deferred to backlog: Code-LOW-1..LOW-4, Sec-LOW-1..LOW-4, Design-L1..L5, Sr-L1..L4. + +### Simplify (5 / 5 apply-yes applied) + +- **S-01..S-05** ✅ all five applied in single `refactor: simplify-pass apply-yes cleanup` commit (`1631d80`). +- **S-M1..S-M4** deferred (maybe-tier). +- **S-N1..S-N6** rejected (per simplify review). + +## Test count delta + +- Baseline (pre-W5 reconcile): 2097 unit tests across 243 files. +- Final (post-W5 reconcile): **2111 unit tests across 245 files** (+14 unit tests, +2 files). +- Integration tests: 81 → 84 (+3) per the new auth-flow + coach-chat + proxy entries; verified locally (`pnpm test:integration auth-flow` passes 6/6). + +New test surfaces: +- `src/lib/sse/__tests__/create-stream.test.ts` (3 tests) +- `src/lib/analytics/__tests__/berlin-day.test.ts` (4 tests) +- `src/lib/analytics/__tests__/bp-in-target.test.ts` (3 new tests for priorMonth/priorYear/null windows) +- `src/app/__tests__/insights-polish.test.ts` (2 new guards for Code-H2) +- `src/__tests__/proxy-onboarding-redirect.test.ts` (2 new guards for Sec-MED-2) +- `tests/integration/coach-chat.test.ts` (1 new test for sentinel-only leak) +- `tests/integration/auth-flow.test.ts` (3 new tests for createSession/destroySession cookie contract) + +## Verification gates run between commits + +For every commit: +- `pnpm typecheck` (clean — 0 errors) +- `pnpm test --run` (passes after each change; final 2111/2111) +- `pnpm lint` (15 pre-existing warnings, 0 errors) + +For HIGH 1 + HIGH 3 + Sec-MED-2 (per brief): +- `pnpm test:integration auth-flow` 6/6 passes +- `pnpm test --run src/__tests__/proxy-onboarding-redirect.test.ts` 9/9 passes + +## Items the maintainer should weigh in on + +1. **CSP `connect-src` for `https://chatgpt.com`** is gated to `/settings/ai/**` only (per existing proxy.ts comment). The Coach SSE route hits OpenAI/Anthropic via the provider runner, which runs server-side — no browser fetch — so this is correct. Calling out only because the W5 simplify review flagged proxy.ts as out-of-scope and the cookie attribute changed; verify the connect-src isn't accidentally re-broadened by a future MED-2 follow-up. + +2. **Insights page split deferred to v1.5 P5** per the product-lead memo. The W5 senior-dev review's L3 (`InsightsSectionNav` extraction) is a natural sub-task at that point. Leaving the page at 1700+ LOC for one more cycle is the right call but worth a v1.4.23 reminder when the next polish PR opens. + +3. **Sr-M5 sentinel parser malformed-enum** carries the highest signal-to-effort ratio of the deferred MEDs (4-state enum, ~30 LOC, tightens ops observability before iOS volume lands). Suggest pulling forward to v1.4.23 if room. + +4. **The `/api/auth/me` resync write** stays as a fall-back per the original v1.4.22 C4 design. Sec-LOW-1 calls it out only as a "future cargo-cult" risk — not an actual bug. Optional one-line code comment about not growing the GET-side mutation. Filed in backlog as LOW. + +## Anything I couldn't reconcile + +- All 7 HIGH applied successfully. No unresolved blockers. +- The Code-LOW-2 colon-bearing label parser test was deferred (LOW + low-priority). The defensive contract is documented in the prompt few-shots; an explicit constraint comment in the prompt header is the cleanest fix when v1.4.23 next-touches the prompt. diff --git a/.planning/v1422-backlog.md b/.planning/v1422-backlog.md new file mode 100644 index 000000000..ad78a9e9d --- /dev/null +++ b/.planning/v1422-backlog.md @@ -0,0 +1,68 @@ +# v1.4.22 backlog + +Items deferred from the W5 reconcile pass. Carried into v1.4.23 +. + +## Wave 5 reconcile carry-over + +### Code review + +- **Code MED-1** (`src/app/api/insights/targets/route.ts:199-216`): `sparklinePoints()` once-per-day bucket now uses Berlin tz (lifted to `src/lib/analytics/berlin-day.ts` in W5 reconcile). DST-boundary edge-case unit test added. `analytics/route.ts` now imports the shared helper. — APPLIED in W5 reconcile, kept here for trace. +- **Code MED-4** (`src/app/insights/page.tsx:1729-1747`): IntersectionObserver picks the highest-intersection entry now. — APPLIED in W5 reconcile (folded into Design-H1 commit), kept here for trace. +- **Code LOW-1** (`src/components/insights/trends-row.tsx:148`): `` self-resolves a localised title; the explicit prop tightens coupling. Drop the `title` prop on all three TrendsRow callsites OR add a comment. Cosmetic only. +- **Code LOW-2** (`src/lib/ai/coach/keyvalues.ts:53`): `parseKeyValueLine` regex uses the first `:` as label/value boundary. A future prompt rev that adds a time-of-day pin would break (`"Mon 6 May, 7:30 reading: 142/88"`). Add a parser test for a colon-bearing label as a regression pin OR document the constraint in the prompt explicitly. +- **Code LOW-3** (`src/app/api/insights/chat/route.ts:284-291`): sentinel parse failure logged with `kept` count but not the prompt-version-vs-malformed-cause breakdown. Extend `SentinelParseResult` with `reason: "ok" | "no_close_marker" | "truncated" | "empty_block"`. +- **Code LOW-4** (`src/app/targets/page.tsx:272-273`): `Math.min(...points)` / `Math.max(...points)` spread-call patterns can blow stack at ~10k arguments. Replace with `for` loop or `points.reduce(...)`. +- **Code LOW-5** (`src/lib/query-keys.ts`): `["user", "dashboardWidgets"]` centralised in `queryKeys.dashboardWidgets()` in W5 reconcile, three call sites migrated. — APPLIED. + +### Security review + +- **Sec MED-1** (`src/lib/auth/session.ts:32`): `hl_onboarding` cookie `SameSite=Lax` → `Strict`. — APPLIED in W5 reconcile. +- **Sec MED-2** (`src/proxy.ts:28-33` + `:152`): `/onboarding` `startsWith` match tightened to exact + subroute. — APPLIED in W5 reconcile. +- **Sec LOW-1** (`/api/auth/me`): GET that mutates state (cookie write) on every call. Idempotent; review left as-is, optional comment about not growing. +- **Sec LOW-2** (Coach few-shots): synthetic numeric values baked into the system prompt. Optional `WARNING` comment so a future contributor doesn't paste real readings. +- **Sec LOW-3** (`src/app/api/insights/chat/route.ts:211-220`): transcript builder flattens role labels into a single user prompt; user-self-injection only. v1.4.23 candidate: switch to structured chat-completion payload. +- **Sec LOW-4** (`src/lib/ai/coach/keyvalues.ts:147-180`): truncated/closeMissing booleans not surfaced to wide-event meta. Extend with `truncated` / `closeMissing` for ops visibility. + +### Design review + +- **Design M1** (`coach-panel/message-thread.tsx:259-307`): coach evidence disclosure has no `aria-controls` / `aria-expanded` semantics; `` reports without expanded state in iOS Safari ≤ 17.3. Wrap the summary in a 36-px-min hit zone, add `aria-expanded` mirror via `useState` + `onToggle`. +- **Design M2** (`src/app/targets/page.tsx:264-303`): Sparkline has no min-points threshold guard — 3-4 points spread over 30 days reads "lots of activity". Either require `length >= 7` before painting, or render at `stroke-opacity:0.5` for `length < 10` plus a `{n} of 30 days logged` caption. +- **Design M3** (`src/components/insights/recommendation-card.tsx:328-335`): severity badge uppercase eyebrow re-introduces the v1.4.20 M4 marketing pattern AND surfaces the internal severity enum (`urgent` / `suggestion`) — no i18n. Drop `tracking-wide uppercase`, replace `{norm.severity}` with `{t(\`insights.recommendation.severity.\${norm.severity}\`)}`. Add EN/DE keys. +- **Design M4** (Coach disclaimer reach): pinned the disclaimer at the bottom of the message thread in W5 reconcile (Design-H3 fix). — APPLIED. +- **Design M5** (`src/app/insights/page.tsx:1070-1078`): row-fill rule on the medication-correlation row uses `showMoodSection` heuristic. Either hide the BP-medication card entirely when `bpMedicationScatterData.length < 5` (let mood-vs-BP take 100 %) or restore the fixed 50/50 with per-card empty state. +- **Design M6** (`dashboard-layout-section.tsx:204-241`): comparison-toggle has no live-preview affordance. Add a tiny `` preview swatch under the picker. +- **Design M7** (`src/app/targets/page.tsx:286-301`): SVG sparkline `aria-hidden` skips the only "things changing" signal for SR users. Add `aria-label` to the `` parent OR move the sentiment into the delta caption prose. +- **Design L1** (i18n voice asymmetry on `coach.evidenceLabel`): EN ("What I'm looking at") declarative; DE ("Worauf bezieht sich das?") curious. Decide on a unified voice. +- **Design L2** (`coach-panel/message-thread.tsx:286-304`): React `key={\`\${kv.label}-\${idx}\`}` allows duplicate-label collisions. Use a hash of `\${label}-\${window}-\${value}`. +- **Design L3** (`targets/page.tsx:288`): sparkline `preserveAspectRatio="none"` distorts y vs x. Either keep + accept, or fix Y-range to the target range. +- **Design L4** (`scroll-mt-28` overshoot): tuned to `scroll-mt-16` in W5 reconcile (Design-H1 commit). — APPLIED. +- **Design L5** (api-tokens table date wrap): cells wrap to two lines on 1024 px. Either short-format `formatDate` + tooltip, or push `` width to 14 %. + +### Senior-dev review + +- **Sr H1** (createSession cookie fan-out): folded into createSession in W5 reconcile. — APPLIED. +- **Sr H2** (chat route SSE construction duplicated 3×): extracted `createSseStream` helper in W5 reconcile. — APPLIED. +- **Sr M1** (analytics route fetches ALL BP measurements ever, twice): bound by horizon (5 years) OR materialise a per-user `bp_target_aggregate` row refreshed on measurement-write. +- **Sr M2** (targets route's 7-pass sparkline computation): collapse to a single Postgres-side `GROUP BY date_trunc('day', measured_at), type`. +- **Sr M3** (targets page mutates server response inside render): wrap the glucose-target conversion in `useMemo([data, t, displayGlucoseUnit])` to keep refs stable. +- **Sr M4** (`provenanceFromJson` hand-rolls keyValues validation): replace 35 lines of manual `typeof` checks with `coachKeyValueSchema.array().safeParse(parsed.keyValues)`. Same `safeParse` lift fixes the windows / metrics enum-membership gap. +- **Sr M5** (sentinel parser conflates 3 distinct malformed conditions): four-state enum (`"ok" | "truncated" | "unclosed" | "empty"`) so ops dashboards can distinguish the cases. +- **Sr L1** (`Sparkline` `preserveAspectRatio="none"`): same as Design L3. +- **Sr L2** (`setOnboardingPendingCookie` `async` but does no async work): could be sync if `cookies()` resolved by caller. v1.5 cleanup. +- **Sr L3** (`InsightsSectionNav` re-creates IntersectionObserver on every mount): single mount per page, so the practical cost is negligible — observer should move with the component on extraction. +- **Sr L4** (`as TargetItem` cast in `targets/route.ts:281`): the BP entry ships sys-only data without typing it. Pre-existing. + +### Simplify review + +- **S-01..S-05**: applied in W5 reconcile (single commit). — APPLIED. +- **S-M1..S-M5**: maybe-tier — leave for now; revisit when adjacent work re-touches the surfaces. + +### Product Lead memo (cross-link) + +- **Insights page split** held to v1.5 P5 (Apple Health work edits the same sub-trees). +- **Pearson p-value normal approximation at low df**: 30-LOC incomplete-beta or raise gate to df >= 20. Pull in v1.5 P0 before auto-discovery ships. +- **Coach token-budget UX**: 25 000 tokens/user/day = ~13 turns. iOS surface in v1.5 will multiply this. Replace generic 429 with localised "limit reached, resets at 00:00 UTC". +- **Conversation persistence eviction policy**: auto-archive after 90 days, hard-delete after 365. v1.5 P3 latest. +- **OpenAPI spec drift CI guard**: pull forward to v1.4.23 if possible. +- **CoachDrawer `key={prefill}` weaponising React keys (Sr-HIGH-4)**: heavy refactor; v1.5 P3 latest. +- **Cross-user feedback aggregation cron** (P6 in product-lead memo): wired alongside v1.5 P3 once usage volume materialises.