mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
7dfc212634
commit
f0f3143cab
1 file changed
+178
@@ -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.
|
||||
Reference in new issue
Block a user