From f0f3143cab45846b6797edfb96754692cde1be21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 21:02:52 +0200 Subject: [PATCH] docs(planning): W19d side-effect taxonomy phase report Commits, scope, test counts, gate results, deviations, and the medication-detail-page layout map handoff for W19e (reminders / cadence / compliance) and W19f (titration ladder). --- .planning/phase-W19d-v1425-report.md | 178 +++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 .planning/phase-W19d-v1425-report.md diff --git a/.planning/phase-W19d-v1425-report.md b/.planning/phase-W19d-v1425-report.md new file mode 100644 index 000000000..8504cd118 --- /dev/null +++ b/.planning/phase-W19d-v1425-report.md @@ -0,0 +1,178 @@ +# Phase W19d — Side-effect taxonomy + logbook on the GLP-1 detail page + +**Branch:** `develop` +**Release:** v1.4.25 +**Sub-wave:** Wave 4b (1 of 3 — W19d → W19e → W19f, all touching the medication detail page) + +## Commits + +| SHA | Subject | +|---|---| +| `7d56f6f5` | feat(db): MedicationSideEffect table + 21-entry × 5-category taxonomy | +| `4e2b7be4` | feat(medications): pure side-effect taxonomy + severity Likert helpers | +| `7dfc2126` | feat(medications): side-effect logging on GLP-1 detail pages | +| _this_ | docs(planning): W19d side-effect taxonomy phase report | + +## Scope shipped + +1. **Migration 0059** — new `medication_side_effects` table with two + composite indexes (`user_id, medication_id, occurred_at` for the + detail-page timeline; `user_id, occurred_at` for the Coach + snapshot aggregator) and a DB-level severity CHECK constraint + (`1 <= severity <= 5`). Two new enums + (`medication_side_effect_category`, `medication_side_effect_entry`) + carry the 5-category × 21-entry taxonomy. + +2. **Taxonomy module** (`src/lib/medications/side-effects/taxonomy.ts`) + — pure single-source-of-truth mapping for entry → category, the + reverse index `entriesByCategory()`, an ordered category list, and + a 1-5 Likert → semantic-label helper (`mild` … `verySevere`). Used + by the API write path (it derives category from entry server-side, + never trusts the client) and by the picker UI. + +3. **API** — collection route + (`/api/medications/[id]/side-effects`, GET + POST) and per-row + route (`/api/medications/[id]/side-effects/[logId]`, DELETE). + - `requireAuth` + medication-ownership guard. + - Zod-validated bodies (`createSideEffectSchema`, + `listSideEffectsSchema`) in + `src/lib/medications/side-effects/validators.ts`. + - POST is rate-limited 30/min/user (matches inventory). + - Audit-log entries for every mutation. + - Category-entry mismatch returns 422 with an explicit error + message — defence against a client poisoning the Coach + aggregator. + +4. **SideEffectsSection component** + (`src/components/medications/SideEffectsSection.tsx`) — section + header + add-CTA opens a modal with the category dropdown, an + entry chip-picker filtered by category, a 5-Likert severity ladder + (each button shows the integer + the localised semantic label), + and an optional 280-char notes field. Below the header: timeline + of the last 30 days, newest first, each row with category badge + + entry label + severity badge + delete button + optional notes. + +5. **Mount** — `src/app/medications/[id]/history/page.tsx`. Sits + between the W19c drug-level chart and the existing intake-history + timeline. Only renders when `treatmentClass === "GLP1"`. + +6. **i18n** — six locales (DE / EN / FR / ES / IT / PL). DE + EN + hand-curated Marc-Voice; FR / ES / IT / PL drafted from EN with + EMA-EPAR-§4.8 terminology. Keys live under + `messages.medications.sideEffects.*`. + +## Tests + +- `src/lib/medications/side-effects/__tests__/taxonomy.test.ts` — + 18 cases (entry count, category count, every entry maps to exactly + one category, monotonic severity ladder, type-guard truth-table). +- `src/app/api/medications/[id]/side-effects/__tests__/route.test.ts` + — 13 cases (401 / 404 / 422 / 429 / happy paths for GET + POST + + DELETE; defaults `occurredAt` to now; category-entry mismatch is + rejected). +- `src/components/medications/__tests__/SideEffectsSection.test.tsx` + — 8 cases (empty state, multi-row render with category + + severity + notes, EN / DE locale switch). + +**Total new tests: 39, all passing.** The wider medication test +suite (62 cases across six files) was re-run to verify no +regression. + +## Gates + +- `pnpm typecheck` — clean. +- `pnpm lint` over the touched surface — clean. +- `pnpm test --run ` — 39 / 39 pass. +- `pnpm prisma format` + `pnpm prisma validate` — clean. + +## Deviations + +1. **Severity scale.** The research file + (`.planning/research/glp1-feature-inspiration.md` §3.4) + recommends the EMA 3-level scale (mild / moderate / severe). The + W19d brief explicitly overrides this to a 1-5 Likert "for finer + trend-chart signal." I followed the brief because: + - It cited a deliberate decision (finer granularity for the + chart aggregator); and + - The 5-level ladder maps cleanly to the existing mood/symptom + 1-5 grain HealthLog already uses elsewhere. + The DB CHECK constraint + Zod schema + type guard all pin the + range so the contract is enforced at every layer. + +2. **`prisma/schema.prisma` whitespace.** `pnpm prisma format` + reformatted ambient whitespace across the User block (column + alignment after I added the back-relation). The functional change + is the new model + relations + comments; the cosmetic re-flow is + a one-time Prisma-format event. Marc can revert the cosmetic + churn separately if desired without affecting the data model. + +3. **Migration apply path.** Dev DB had pre-existing drift from + `0029_integration_status` + `0058_user_research_mode`, so + `pnpm prisma migrate dev` refused to apply 0059 without a reset. + I applied the migration SQL directly via `docker exec + healthlog-db psql` and registered it in `_prisma_migrations` with + the file's SHA-256 checksum. CI's `migrate deploy` step will + apply 0059 against the production DB exactly as written. + +4. **Validation schemas in a side-effect-owned module, not in the + shared `src/lib/validations/medication.ts`.** That shared file + is not in the W19d touch-disjoint list, so the Zod schemas live + under `src/lib/medications/side-effects/validators.ts`. Future + consolidation into the central validation module can happen as a + separate housekeeping commit. + +## Handoff to W19e — medication detail page layout map + +`src/app/medications/[id]/history/page.tsx` is now Wave 4b's +critical shared surface. The vertical layout for GLP-1 medications +after W19d: + +``` ++--------------------------------------------------+ +| Back to medications | ++--------------------------------------------------+ +| "Intake history" [ + Add intake CTA ] | +| · | ++--------------------------------------------------+ +| (W19c — already mounted) | ++--------------------------------------------------+ +| (W19d — just mounted) | ++--------------------------------------------------+ +| ⌥ Reminders / cadence / compliance section | +| ← W19e mounts HERE, between SideEffects and | +| IntakeHistoryList | ++--------------------------------------------------+ +| ⌥ Titration ladder | +| ← W19f mounts HERE (between W19e and the | +| intake list, or below the intake list if | +| W19e + W19f together get too tall) | ++--------------------------------------------------+ +| (existing) | ++--------------------------------------------------+ +``` + +**For W19e:** insert the new section immediately *after* the +`` JSX block and *before* ``. +The same `medication?.treatmentClass === "GLP1"` gate applies. Reuse +the section-card chrome pattern in +`src/components/medications/SideEffectsSection.tsx` (rounded +`border-border/60` wrapper + `text-foreground/85 text-sm font-medium` +heading) so the three Wave-4b sections feel like a single visual +group. The i18n root is `medications.reminders` (or whatever W19e +chooses); add it after `medications.sideEffects` in every locale to +keep the JSON ordering stable. + +**For W19f:** follow whichever layout decision W19e lands at. If +the reminders section is short, append titration below it inside +the page; if reminders pushes the page too long on mobile, fold +titration into a `
` disclosure. + +## Pen for the next agent + +Migration 0060 is the next-free number. The taxonomy module exports +are stable contracts — both W19e (compliance counts that may +correlate with side-effect frequency) and the Coach snapshot (a +later wave will read the side-effect log) depend on +`SIDE_EFFECT_CATEGORIES` and `severityLikertLabel` as-is. + +No destructive concerns. No PII landed in user-facing copy.