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).
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 21:02:52 +02:00
1 parent 7dfc212634
commit f0f3143cab
1 file changed
+178
+178
View File
@@ -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 <touched-surface>` — 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 ] |
| <medication name> · <dose> |
+--------------------------------------------------+
| <DrugLevelChart> (W19c — already mounted) |
+--------------------------------------------------+
| <SideEffectsSection> (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) |
+--------------------------------------------------+
| <IntakeHistoryList> (existing) |
+--------------------------------------------------+
```
**For W19e:** insert the new section immediately *after* the
`<SideEffectsSection>` JSX block and *before* `<IntakeHistoryList>`.
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 `<details>` 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.