mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
docs(planning): W21 simplifier findings
This commit is contained in:
1 parent
5448d19fee
commit
e8139e9f93
1 file changed
+591
@@ -0,0 +1,591 @@
|
||||
# W21 Simplifier Findings — v1.4.25 release-candidate
|
||||
|
||||
Scan scope: `v1.4.24..develop` (60+ commits, ~2.5k new files), focused
|
||||
on the W14b onboarding rebuild, W19c/d/e/f medication pure-modules and
|
||||
detail-page sections, W19c-Safety surface, W15 hygiene cluster, W18
|
||||
W10 Low+care items, and W20-rest P6 polish. Pure observation pass —
|
||||
no code changes; the W18 simplifier already shipped the
|
||||
apply-with-care list, so this is the fresh-eyes follow-up against the
|
||||
code that landed after that pass.
|
||||
|
||||
Severity rubric (from the W21 dispatch brief):
|
||||
- **Critical** — none expected; this is a quality lens, not a release-blocker lens.
|
||||
- **High** — duplication or dead branch that hides a bug, or a nested ternary that lives in production conditional logic the rubric explicitly flags.
|
||||
- **Medium** — clear simplification with >10-line savings AND clear callsite improvement.
|
||||
- **Low** — style preference, micro-refactor, defer to v1.4.27.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
No critical issues. Seven High findings, ten Medium, eight Low.
|
||||
|
||||
The Wave-4b medication-detail panels (W19d/e/f) plus the inventory
|
||||
disclosure all carry the same chrome (`border-border/60 rounded-md
|
||||
border` shell + bordered header strip + `border-t` content well + EMA
|
||||
disclaimer footer). Four near-identical `<section>` skeletons across
|
||||
three sections is exactly the threshold the rubric flags as "warrants
|
||||
a helper". Same story on the onboarding side: four step components
|
||||
each ship a private `readError(res)` helper and three of them ship a
|
||||
near-identical `async function advance()` with the
|
||||
fetch-rate-limit-redirect-toast pattern.
|
||||
|
||||
The W19c-Safety / W19c-Frontend surface introduced two locally
|
||||
redefined `interface ResearchModeStatus` types (advanced-section.tsx
|
||||
and DrugLevelChart.tsx) plus a reverse-lookup loop over `GLP1_DRUGS`
|
||||
that's now duplicated in two places. Neither is harmful today, but
|
||||
both look exactly like the W18 simplifier work the W10 lens predicted.
|
||||
|
||||
The `glp1-pk.ts` module exports four names (`shotPhaseAt`, `ShotPhase`,
|
||||
`OneCompartmentOptions`, `PkSample`) that nothing outside the test
|
||||
file imports. The module's own JSDoc cites these as "the shot phase
|
||||
chip on the dashboard tile (research §2.4)" — that surface hasn't
|
||||
shipped yet, so they sit in the file as scope-creep insurance. Either
|
||||
mark them with the `__testables` pattern from W15's api-handler or
|
||||
remove them.
|
||||
|
||||
---
|
||||
|
||||
## High
|
||||
|
||||
### H1 — Onboarding step components each ship their own `readError(res)`
|
||||
|
||||
**Where**: 4 files — `src/components/onboarding/{WelcomeCarousel,GoalsChipPicker,SourceCardGrid,BaselineForm}.tsx`
|
||||
|
||||
**Current** (identical 10-line block, repeated 4×):
|
||||
```ts
|
||||
async function readError(res: Response): Promise<string> {
|
||||
try {
|
||||
const json = (await res.json()) as { error?: string };
|
||||
if (typeof json.error === "string" && json.error.length > 0) {
|
||||
return json.error;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `Request failed (${res.status})`;
|
||||
}
|
||||
```
|
||||
|
||||
**Suggested**: hoist to `src/lib/api/read-error.ts` (or
|
||||
`src/components/onboarding/_helpers.ts`); 4 callers import it.
|
||||
|
||||
**LOC delta**: −30 (4 copies × 10 lines, minus one shared module of ~10 lines).
|
||||
**Risk**: low — pure helper, identical across all callers, no closure state.
|
||||
|
||||
---
|
||||
|
||||
### H2 — `templateFill` in TitrationSection.tsx reinvents the i18n `t(key, params)` contract
|
||||
|
||||
**Where**: `src/components/medications/TitrationSection.tsx:53-61`
|
||||
|
||||
**Current**:
|
||||
```ts
|
||||
function templateFill(template: string, values: Record<string, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (m, key) => (key in values ? String(values[key]) : m));
|
||||
}
|
||||
// then 5 call sites: templateFill(t("medications.titration.stepLabel"), { n: step.stepIndex + 1 })
|
||||
```
|
||||
|
||||
**Suggested**: drop `templateFill`; `t()` already accepts params. The
|
||||
i18n provider in `src/lib/i18n/context.tsx:130-153` does exactly the
|
||||
same `{param}` substitution natively:
|
||||
```ts
|
||||
{t("medications.titration.stepLabel", { n: step.stepIndex + 1 })}
|
||||
```
|
||||
|
||||
**LOC delta**: −9 helper + slightly cleaner call sites.
|
||||
**Risk**: low — `t()` already runs the same `replace(new RegExp(\`\\{${k}\\}\`, "g"), …)` substitution. Verified at `context.tsx:144-148`.
|
||||
|
||||
This is a clean bug-hide candidate: a future maintainer who renames a
|
||||
placeholder in the German locale but not in `templateFill`'s caller
|
||||
would get silent fallthrough.
|
||||
|
||||
---
|
||||
|
||||
### H3 — Nested ternary inside the Settings "Research Mode" status line
|
||||
|
||||
**Where**: `src/components/settings/advanced-section.tsx:181-191`
|
||||
|
||||
**Current**:
|
||||
```tsx
|
||||
{isLoading
|
||||
? t("common.loading")
|
||||
: status?.enabled
|
||||
? versionsAligned
|
||||
? t("settings.researchMode.acknowledgedOn", { date: ... })
|
||||
: t("settings.researchMode.enabledStaleStatus")
|
||||
: t("settings.researchMode.disabledStatus")}
|
||||
```
|
||||
|
||||
**Suggested**: extract to a small helper `function statusLabel({isLoading, status, versionsAligned})` with a switch/early-return chain. The rubric calls out nested ternaries explicitly ("prefer switch statements or if/else chains for multiple conditions").
|
||||
|
||||
**LOC delta**: roughly neutral (+5 helper / -10 inline) but materially more readable; the four branches are independent.
|
||||
**Risk**: low — pure presentation.
|
||||
|
||||
---
|
||||
|
||||
### H4 — `interface ResearchModeStatus` redeclared in two surfaces
|
||||
|
||||
**Where**:
|
||||
- `src/components/settings/advanced-section.tsx:70-75`
|
||||
- `src/components/medications/DrugLevelChart.tsx:75-80`
|
||||
|
||||
Both spell the same 4-field shape (`enabled`, `acknowledgedAt`,
|
||||
`acknowledgedVersion`, `currentDisclaimerVersion`). The API route
|
||||
already owns the canonical `ResearchModeResponse` type in
|
||||
`src/app/api/auth/me/research-mode/route.ts:45-50`, but it isn't
|
||||
exported.
|
||||
|
||||
**Suggested**: export the type from a shared `src/lib/medications/research-mode-types.ts` (the API route stays the source of truth, sibling to `glp1-pk.ts` where `RESEARCH_MODE_DISCLAIMER_VERSION` lives). Both components import the alias.
|
||||
|
||||
**LOC delta**: −10 (two 6-line interfaces collapsed to two import lines).
|
||||
**Risk**: low — pure type alias, no runtime change.
|
||||
|
||||
---
|
||||
|
||||
### H5 — Reverse-lookup loop `Object.entries(GLP1_DRUGS) → drugId` duplicated
|
||||
|
||||
**Where**:
|
||||
- `src/app/api/medications/[id]/titration/route.ts:69-78`
|
||||
- `src/components/medications/DrugLevelChart.tsx:134-140`
|
||||
|
||||
**Current** (both):
|
||||
```ts
|
||||
let drugId: Glp1DrugId | null = null;
|
||||
for (const [id, r] of Object.entries(GLP1_DRUGS)) {
|
||||
if (r === record) { drugId = id as Glp1DrugId; break; }
|
||||
}
|
||||
```
|
||||
|
||||
**Suggested**: add `findDrugIdByBrand(brand: string): Glp1DrugId | null` to `glp1-knowledge.ts` (mirroring the existing `findDrugByBrand`). Both callsites become a single named call.
|
||||
|
||||
**LOC delta**: −12 net.
|
||||
**Risk**: low — pure read of a static catalog.
|
||||
|
||||
---
|
||||
|
||||
### H6 — `assertMedicationOwnership` defined twice with byte-identical body
|
||||
|
||||
**Where**:
|
||||
- `src/app/api/medications/[id]/side-effects/route.ts:43-55`
|
||||
- `src/app/api/medications/[id]/inventory/route.ts:37-49`
|
||||
|
||||
Plus the same inline 3-line check is repeated in the W19e/f cadence/titration routes (`route.ts:41-47` and `:44-52`), and in `[id]/intake/route.ts`, `[id]/glp1/route.ts`, `[id]/route.ts`, `[id]/compliance/route.ts`, `[id]/phase-config/route.ts`, `[id]/api-endpoint/route.ts`, `[id]/intake/purge/route.ts`, `[id]/intake/import/route.ts` — 11 callsites total counted by grep.
|
||||
|
||||
**Suggested**: hoist to `src/lib/medications/route-guards.ts` (parallel to `src/lib/api-handler.ts`): `async function assertMedicationOwnership(id, userId, opts?): Promise<NextResponse | { medication }>`. Each route reduces to one call.
|
||||
|
||||
**LOC delta**: −40 net across the 11 callsites.
|
||||
**Risk**: low-medium — needs a careful pass on the variants that `include` schedules / doseChanges (cadence/titration use Prisma `include`, the side-effects route uses `select`). Helper either takes an `include` argument or exposes a two-step ownership check + caller-fetches-detail.
|
||||
|
||||
---
|
||||
|
||||
### H7 — `InventoryState` type alias redeclared as string-union in `inventory-section.tsx`
|
||||
|
||||
**Where**: `src/components/medications/inventory-section.tsx:35`
|
||||
|
||||
**Current**:
|
||||
```ts
|
||||
type InventoryState = "ACTIVE" | "IN_USE" | "EXPIRED" | "USED_UP";
|
||||
```
|
||||
|
||||
**Suggested**: import `MedicationInventoryState` from `@/generated/prisma/enums` (already used by `state-machine.ts`):
|
||||
```ts
|
||||
import type { MedicationInventoryState } from "@/generated/prisma/enums";
|
||||
```
|
||||
|
||||
**LOC delta**: −1 but eliminates a drift surface — adding a fifth state to the schema (Prisma enum) won't update the client union silently.
|
||||
**Risk**: low — pure type alias, same shape.
|
||||
|
||||
---
|
||||
|
||||
## Medium
|
||||
|
||||
### M1 — `SIDE_EFFECT_CATEGORY_VALUES` / `SIDE_EFFECT_ENTRY_VALUES` duplicate the Prisma enum keys
|
||||
|
||||
**Where**: `src/lib/medications/side-effects/validators.ts:18-48`
|
||||
|
||||
Both arrays restate the values that already live as the keys of
|
||||
`SIDE_EFFECT_CATEGORIES` and the entries of `SIDE_EFFECT_ENTRIES_BY_CATEGORY` in `taxonomy.ts`, and as a Prisma enum in `@/generated/prisma/client`.
|
||||
|
||||
**Suggested**: derive from the existing constants. The Zod 4 syntax
|
||||
that the rest of the validator file already uses supports
|
||||
`z.enum(Object.keys(SIDE_EFFECT_CATEGORIES) as [string, ...string[]])`
|
||||
or, simpler, `z.nativeEnum(MedicationSideEffectCategory)` from the
|
||||
generated Prisma enum.
|
||||
|
||||
**LOC delta**: −28 (two 14-line arrays collapse to two `nativeEnum` lines).
|
||||
**Risk**: low — Zod's `nativeEnum` is the documented Prisma-enum bridge.
|
||||
|
||||
Bonus: the `taxonomy.test.ts` fixture (`ALL_ENTRIES` / `ALL_CATEGORIES`
|
||||
at lines 30-60, 23 lines) is then derivable from the same enum, saving
|
||||
another ~20 LOC of test-time drift surface.
|
||||
|
||||
---
|
||||
|
||||
### M2 — Three `async function advance()` blocks in onboarding are 95% the same
|
||||
|
||||
**Where**: `src/components/onboarding/{GoalsChipPicker.tsx:166-184,SourceCardGrid.tsx:97-115,WelcomeCarousel.tsx:131-149}`
|
||||
|
||||
**Current** (sketched):
|
||||
```ts
|
||||
async function advance() {
|
||||
if (advancing) return;
|
||||
setAdvancing(true);
|
||||
try {
|
||||
const res = await fetch("/api/onboarding/step", { method: "POST", … body: { step: N } });
|
||||
if (!res.ok) throw new Error(await readError(res));
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
router.push(`/onboarding/${N}`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : t("onboarding.errorGeneric");
|
||||
toast.error(message);
|
||||
setAdvancing(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The only variation across the three callers is the step number (2, 3,
|
||||
4) and the route target.
|
||||
|
||||
**Suggested**: `useOnboardingAdvance({ from, to })` hook in
|
||||
`src/hooks/use-onboarding-advance.ts` returns `{ advance, advancing }`.
|
||||
The BaselineForm version is a slight superset (it threads through the
|
||||
profile PUT first) but can compose the hook for the step write.
|
||||
|
||||
**LOC delta**: ~−50 across the three callers; one new ~30-line hook.
|
||||
**Risk**: low — straightforward TanStack Query / router composition, mirrors the existing `useInsightStatus` pattern landed in this same release.
|
||||
|
||||
---
|
||||
|
||||
### M3 — Three near-identical `<section class="border-border/60 rounded-md border">` headers + content wells
|
||||
|
||||
**Where**: `src/components/medications/{TitrationSection.tsx,SchedulingSection.tsx,SideEffectsSection.tsx}` plus the `inventory-section.tsx` disclosure top.
|
||||
|
||||
Each section opens with the same shell:
|
||||
```tsx
|
||||
<section className="border-border/60 rounded-md border" aria-labelledby="…-heading">
|
||||
<header className="flex items-center justify-between px-3 py-2.5">
|
||||
<h2 id="…-heading" className="text-foreground/85 text-sm font-medium">{title}</h2>
|
||||
…
|
||||
</header>
|
||||
<div className="border-border/60 border-t px-3 py-3 text-xs">
|
||||
{body}
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Suggested**: a `<MedicationDetailSection title="…" trailing={…} aria-labelledby={…}>` wrapper composes the three. The TitrationSection / SchedulingSection / SideEffectsSection components shrink to ~80% of their current size since the header + chrome boilerplate drops out.
|
||||
|
||||
**LOC delta**: ~−45 across the three sections, plus one new ~30-line wrapper.
|
||||
**Risk**: low — visual diff should be zero (string-for-string class identical); each section keeps its own body. Worth doing now before the next Wave 4-b section ships and pins a fourth copy.
|
||||
|
||||
---
|
||||
|
||||
### M4 — `parseDoseMg` / `parseDose` / `parseDoseString` are three near-identical dose-string parsers
|
||||
|
||||
**Where**:
|
||||
- `src/components/medications/DrugLevelChart.tsx:522-527` — `parseDoseMg(input: string): number` (NaN sentinel)
|
||||
- `src/app/api/medications/[id]/titration/route.ts:140-146` — `parseDoseString(dose: string): number | null` (null sentinel)
|
||||
- `src/components/medications/medication-form.tsx:200` — `parseDose(dose: string): { amount, unit }` (separate concern: splits the unit; but the numeric extraction overlaps)
|
||||
|
||||
**Suggested**: a single `parseDoseMg(input: string | null): number | null` in `@/lib/medications/dose-string.ts` returns `null` on miss (matches the route's contract; the chart can `?? Number.NaN` once at the call site for its NaN-cascade math).
|
||||
|
||||
**LOC delta**: −7 net, plus one parser surface to fuzz instead of three.
|
||||
**Risk**: low — both helpers ship unit tests; merge the test suites.
|
||||
|
||||
---
|
||||
|
||||
### M5 — `daysRemainingInUse` exists in two places with subtly different signatures
|
||||
|
||||
**Where**:
|
||||
- `src/lib/medications/inventory/state-machine.ts:211-224` (pure, takes `nowMs`, drug-window param, requires full `InventoryItemView`)
|
||||
- `src/components/medications/inventory-section.tsx:59-68` (client-side, hardcoded `Date.now()`, takes ISO string only)
|
||||
|
||||
**Suggested**: extend the pure helper to accept a `firstUseAt: Date | null` signature for the client surface, then drop the duplicate from `inventory-section.tsx`. The client-side reimplementation exists because the pure version requires an `InventoryItemView`; loosen it to `Pick<InventoryItemView, 'firstUseAt'>` and the client can use the canonical helper.
|
||||
|
||||
**LOC delta**: −10 net.
|
||||
**Risk**: medium — the client passes `Date.now()` where the pure helper expects an explicit `asOf`. The reason for the pure-helper signature is testability; preserve it by widening, not narrowing.
|
||||
|
||||
---
|
||||
|
||||
### M6 — `pairDoses()` sorts twice in 50 lines
|
||||
|
||||
**Where**: `src/lib/medications/scheduling/cadence.ts:188-228`
|
||||
|
||||
`pairDoses` sorts `slots` by window-centre on entry (line 188-192),
|
||||
runs the matching loop, then re-sorts the result back to chronological
|
||||
on exit (line 228). Both sorts are over the same `slots.length`.
|
||||
|
||||
**Suggested**: build an index array `[0..slots.length-1]`, sort the
|
||||
index by window-centre, then iterate the index. Push results into a
|
||||
pre-allocated `PairedDose[]` at the original slot's index. One sort,
|
||||
predictable allocations.
|
||||
|
||||
**LOC delta**: ~+5 (slightly more code; the optimisation reasoning is
|
||||
the bigger win for the next maintainer to read).
|
||||
**Risk**: low — pure rearrangement; well-tested.
|
||||
|
||||
Note: low priority unless the cadence chart starts feeling slow on
|
||||
large windows. Today the window is 30 days × handful-of-schedules so
|
||||
both sorts are O(n log n) at n≈30 — micro-optimisation. Flagging
|
||||
because the comment "Restore the chronological order callers expect"
|
||||
at line 227 reads like a workaround.
|
||||
|
||||
---
|
||||
|
||||
### M7 — `escalationDue(drugId, currentStep, weeks)` drops `drugId` after the nextStep guard
|
||||
|
||||
**Where**: `src/lib/medications/titration/ladder.ts:193-201`
|
||||
|
||||
```ts
|
||||
export function escalationDue(drugId, currentStep, weeksOnStep): boolean {
|
||||
if (!currentStep) return false;
|
||||
if (!nextStep(drugId, currentStep)) return false;
|
||||
return weeksOnStep >= currentStep.typicalWeeks;
|
||||
}
|
||||
```
|
||||
|
||||
The `drugId` is used only by `nextStep` lookup — `typicalWeeks` is
|
||||
already on `currentStep`. The signature shape suggests it's load-bearing
|
||||
when it's a pass-through.
|
||||
|
||||
**Suggested**: keep the signature (it's a public API) but rename the
|
||||
parameter to `drugIdForCeiling: Glp1DrugId` or document the role in the
|
||||
JSDoc. Today the JSDoc explains *why* but not *what role the param plays*.
|
||||
|
||||
**LOC delta**: 0; comment-only.
|
||||
**Risk**: low — comment polish.
|
||||
|
||||
---
|
||||
|
||||
### M8 — `for (const [k, v] of Object.entries(rateLimitHeaders(rl)))` pattern across W19 routes
|
||||
|
||||
**Where**:
|
||||
- `src/app/api/medications/[id]/inventory/route.ts:90-92`
|
||||
- `src/app/api/medications/[id]/side-effects/route.ts:121-123`
|
||||
- `src/app/api/auth/me/research-mode/route.ts:105-107`
|
||||
|
||||
vs. the shorter `{ status: 429, headers: rateLimitHeaders(rl) }`
|
||||
pattern used in 7+ other routes (e.g. `auth/login/route.ts:33`).
|
||||
|
||||
**Suggested**: align the W19c/d/b routes on the existing shorter
|
||||
construction `apiError("Too many requests", 429, { headers: rateLimitHeaders(rl) })`. Or expose a `rateLimitedError(rl)` helper in `api-response.ts`.
|
||||
|
||||
**LOC delta**: −9 across three routes (4-line for-loop → 1-line option).
|
||||
**Risk**: low — the helper already supports option-bag construction in
|
||||
the other handlers.
|
||||
|
||||
---
|
||||
|
||||
### M9 — `goalsStorageKey(userId)` is exported but only one caller imports it
|
||||
|
||||
**Where**: `src/components/onboarding/GoalsChipPicker.tsx:94-96, 45`
|
||||
|
||||
```ts
|
||||
export const ONBOARDING_GOALS_STORAGE_PREFIX = "healthlog.onboarding.goals";
|
||||
export function goalsStorageKey(userId: string): string {
|
||||
return `${ONBOARDING_GOALS_STORAGE_PREFIX}:${userId}`;
|
||||
}
|
||||
```
|
||||
|
||||
Verified: zero importers outside the file (a `grep -rn 'goalsStorageKey'` returns only the declaration site). Likely intended for the BaselineForm to read the goal set on step 3 / 4 — but BaselineForm doesn't read it today.
|
||||
|
||||
**Suggested**: either wire BaselineForm to read the persisted set (bundled into the profile PUT or the step:4 write — true follow-up item), or move both names to a non-exported `const` inside the component and ditch the exported helper.
|
||||
|
||||
**LOC delta**: small but indicates an incomplete contract.
|
||||
**Risk**: low. The W14b-Content report mentions this as a v1.4.26 wire-up item — flagging here so it doesn't get forgotten.
|
||||
|
||||
---
|
||||
|
||||
### M10 — `glp1-pk.ts` exports `shotPhaseAt`, `ShotPhase`, `OneCompartmentOptions`, `PkSample` — zero non-test importers
|
||||
|
||||
**Where**: `src/lib/medications/glp1-pk.ts:121, 139, 301, 303`
|
||||
|
||||
Module's JSDoc references the dashboard tile "shot phase" chip (research §2.4 / §2.5) as a future caller, but that surface hasn't shipped. Today, only `DoseEvent`, `computeOneCompartment`, and `RESEARCH_MODE_DISCLAIMER_VERSION` are imported across the codebase.
|
||||
|
||||
**Suggested**: two options, neither is "delete":
|
||||
1. **Keep + mark internal**: move `shotPhaseAt` + its types into a `__testables` export bag (the W15 api-handler pattern):
|
||||
```ts
|
||||
export const __testables = { shotPhaseAt };
|
||||
export type { ShotPhase, PkSample, OneCompartmentOptions };
|
||||
```
|
||||
2. **Inline the dashboard tile wire-up**: connect the shot-phase chip to the existing `dashboard/glp1-tile.tsx` in a v1.4.26 task. The math is ready; the UI surface is the missing piece.
|
||||
|
||||
**LOC delta**: 0 either way; the noise is the public-API surface inflation, not the line count.
|
||||
**Risk**: low — neither option changes behaviour.
|
||||
|
||||
This is the inverse of "dead code" — it's *aspirational* code that was
|
||||
shipped early and labelled with the right comments. The W18 simplifier
|
||||
report explicitly handles this kind of scaffolding (`Apply-with-care`
|
||||
bucket). Defer the decision but pick one before v1.4.26 ships.
|
||||
|
||||
---
|
||||
|
||||
## Low
|
||||
|
||||
### L1 — `inventory/state-machine.ts:180-185` uses nested ternary for outcome classification
|
||||
|
||||
```ts
|
||||
const change: DecrementOutcome =
|
||||
nextDosesRemaining === 0
|
||||
? "depleted"
|
||||
: wasFirstUse
|
||||
? "first_use"
|
||||
: "consumed";
|
||||
```
|
||||
|
||||
Rubric flags nested ternaries. Two levels deep is on the edge. A
|
||||
`switch (true)` / chained-if would read more linearly. Defer.
|
||||
|
||||
### L2 — `DrugLevelChart.tsx:240-264` decision-tree uses a 5-deep ternary
|
||||
|
||||
The chart-body switch (`!drugId ? Gated : !gateOpen ? Gated : isLoading ? Loader : !hasDoses ? Empty : Chart`) is exactly the pattern the rubric warns about. There's a comment-block above it that names the five cases verbatim — so the author knew. Replace with an early-return chain or a tiny `switch (true)` for readability. Defer; not buggy.
|
||||
|
||||
### L3 — `OnboardingShell.tsx:111` repeats the `clamped === 0 ? 1 : clamped` expression twice
|
||||
|
||||
Once in `aria-valuenow`, once in the dot-pager's `aria-current` check. Hoist to a local `currentDotStep` const for symmetry.
|
||||
|
||||
### L4 — `cadence.ts` mixes `MS_PER_DAY`, `WEEK_MS`, `DAY_MS` — two of them are the same value
|
||||
|
||||
`DAY_MS` (cadence.ts:57) and `MS_PER_DAY` (state-machine.ts:31, glp1-pk.ts uses inline `1000 * 60 * 60`, compliance.ts:41). Five copies of the same constant across the new modules. Hoist to `src/lib/time/constants.ts` (or reuse the existing `@/lib/time/units` if it exists).
|
||||
|
||||
Verified at `grep -rn "MS_PER_DAY\|DAY_MS" src/lib/medications` → 5 declarations.
|
||||
|
||||
### L5 — `parseDoseString` regex `/[-+]?\d*\.?\d+/` vs `parseDoseMg` regex `/([0-9]+(?:[.,][0-9]+)?)/`
|
||||
|
||||
The first does not accept comma-decimals; the second does. The first
|
||||
matches signs; the second doesn't. They're not interchangeable. M4
|
||||
above wants one helper — pick the comma-aware regex (German users
|
||||
log "0,5 mg") and ditch the sign matching (negative doses don't exist).
|
||||
|
||||
### L6 — `SideEffectsSection` named in plural but `ScheduleSection` is singular
|
||||
|
||||
Naming: `SchedulingSection`, `SideEffectsSection`, `TitrationSection`, `InventorySection`. The plural on side-effects matches the API route (`/side-effects` is plural; `/cadence`, `/titration`, `/inventory` are singular). Consistent enough for now; flag for a v1.5 naming pass if the team adopts a strict singular convention.
|
||||
|
||||
### L7 — `OnboardingShell` is a server component but `OnboardingShellProps` is exported as a public type
|
||||
|
||||
`src/components/onboarding/OnboardingShell.tsx:33`. Nothing outside the file imports the type today (grep is clean). Drop the `export` modifier to shrink the public surface.
|
||||
|
||||
### L8 — `WelcomeCarousel.tsx:48-67` `SLIDES` constant + `CarouselSlide` type are file-private
|
||||
|
||||
The slide-list pattern could be the seed for a shared `<Carousel>` primitive if/when a second carousel ships in the app (e.g. Withings onboarding tour). Today it's the only carousel. Flagging only because the W21 brief asks about "three-or-more similar code patterns" — this is just one, so defer.
|
||||
|
||||
---
|
||||
|
||||
## Test redundancy notes
|
||||
|
||||
The new test suites under `src/lib/medications/{scheduling,titration,inventory,side-effects}/__tests__/` are well-organised — each maps one-to-one with the pure module. No double-testing observed.
|
||||
|
||||
One observation: `taxonomy.test.ts` re-declares `ALL_ENTRIES` (21 strings) and `ALL_CATEGORIES` (5 strings) at the top, then the test body iterates them to assert every entry maps to a category. This is structurally fine, but the same string lists also appear in `validators.ts` as `SIDE_EFFECT_ENTRY_VALUES` / `SIDE_EFFECT_CATEGORY_VALUES`. If M1 lands (derive validators from the Prisma enum), the test can read from the enum too and drop the literal arrays. Three sources of truth → one.
|
||||
|
||||
---
|
||||
|
||||
## CSS / Tailwind redundancy
|
||||
|
||||
Confirmed by grep — `border-border/60 rounded-md border` (the W19 section shell) appears in 4 places; `border-border/60 border-t` (the section content well) appears in 4 places. See **M3** for the wrapper-component proposal.
|
||||
|
||||
The `bg-primary/10 text-primary flex size-16 items-center justify-center rounded-full` (or `size-20` for the done screen) icon-badge appears in:
|
||||
- `WelcomeCarousel.tsx:195` (size-16)
|
||||
- `DoneScreen.tsx:33` (size-20)
|
||||
- `GoalsChipPicker.tsx:233-241` (size-9, plus a checked-state variant)
|
||||
|
||||
A small `<IconBadge size="sm|md|lg">` would compose these. Defer — the count is right at the rubric's "three-or-more" floor, and the variations are non-trivial (the GoalsChipPicker version flips colour on the checked state). Re-evaluate if the wizard adds a fifth IconBadge surface.
|
||||
|
||||
---
|
||||
|
||||
## Prompt / safety-contract redundancy
|
||||
|
||||
W19c-Safety added GROUND RULE 15 (`drug-level estimate refusal`) plus
|
||||
a peer-level `drug_level_refusal` block. Both ship in all six locale
|
||||
YAMLs with the matching translations.
|
||||
|
||||
Per the W21 brief I'm not reviewing prompt *content*. Structurally:
|
||||
the 15 ground-rules pattern is already a uniform schema (each rule
|
||||
has `parser_critical`, `surface`, `en`, `<locale>`, `trigger_examples`,
|
||||
`must_contain`) — that's good. The companion `drug_level_refusal`
|
||||
block uses a different shape; the YAML schema (`safety-contracts.ts:135`)
|
||||
already enforces the asymmetry, so this is correct, not redundant.
|
||||
No findings here.
|
||||
|
||||
---
|
||||
|
||||
## Files reviewed
|
||||
|
||||
Production code (new in v1.4.24..develop):
|
||||
|
||||
- `src/lib/medications/glp1-pk.ts` (W19c)
|
||||
- `src/lib/medications/research-mode-staleness.ts` (W19c-Safety)
|
||||
- `src/lib/medications/titration/ladder.ts` (W19f)
|
||||
- `src/lib/medications/scheduling/cadence.ts` (W19e)
|
||||
- `src/lib/medications/scheduling/compliance.ts` (W19e)
|
||||
- `src/lib/medications/side-effects/taxonomy.ts` (W19d)
|
||||
- `src/lib/medications/side-effects/validators.ts` (W19d)
|
||||
- `src/lib/medications/inventory/state-machine.ts` (W19b carry-over surface)
|
||||
- `src/lib/medications/inventory/service.ts`
|
||||
- `src/app/api/medications/[id]/titration/route.ts`
|
||||
- `src/app/api/medications/[id]/cadence/route.ts`
|
||||
- `src/app/api/medications/[id]/side-effects/route.ts`
|
||||
- `src/app/api/medications/[id]/inventory/route.ts`
|
||||
- `src/app/api/auth/me/research-mode/route.ts`
|
||||
- `src/app/api/onboarding/step/route.ts`
|
||||
- `src/components/medications/TitrationSection.tsx`
|
||||
- `src/components/medications/SchedulingSection.tsx`
|
||||
- `src/components/medications/SideEffectsSection.tsx`
|
||||
- `src/components/medications/inventory-section.tsx`
|
||||
- `src/components/medications/DrugLevelChart.tsx`
|
||||
- `src/components/medications/ResearchModeAcknowledgmentDialog.tsx`
|
||||
- `src/components/onboarding/OnboardingShell.tsx`
|
||||
- `src/components/onboarding/WelcomeCarousel.tsx`
|
||||
- `src/components/onboarding/GoalsChipPicker.tsx`
|
||||
- `src/components/onboarding/SourceCardGrid.tsx`
|
||||
- `src/components/onboarding/BaselineForm.tsx`
|
||||
- `src/components/onboarding/DoneScreen.tsx`
|
||||
- `src/components/settings/advanced-section.tsx`
|
||||
- `src/hooks/use-insight-status.ts` (W18 deliverable)
|
||||
|
||||
Wave 5 deliverables sampled:
|
||||
|
||||
- `src/lib/api-handler.ts` (W7d hardening + `__testables` pattern)
|
||||
- `src/lib/tz/format.ts` + `src/lib/tz/resolver.ts` (W7d client-bundle split)
|
||||
- `src/components/settings/sources-section.tsx` (W18 `reorderLadder` helper)
|
||||
- W15 i18n cleanup (380 dead keys removed) and W15 dead-prompt-constants removal — clean, no findings.
|
||||
- W20-rest P6 polish (a11y/aria + chart token fixes) — clean.
|
||||
|
||||
Tests sampled:
|
||||
|
||||
- `src/lib/medications/__tests__/glp1-pk.test.ts`
|
||||
- `src/lib/medications/__tests__/research-mode-staleness.test.ts`
|
||||
- `src/lib/medications/scheduling/__tests__/cadence.test.ts`
|
||||
- `src/lib/medications/scheduling/__tests__/compliance.test.ts`
|
||||
- `src/lib/medications/titration/__tests__/ladder.test.ts`
|
||||
- `src/lib/medications/inventory/__tests__/state-machine.test.ts`
|
||||
- `src/lib/medications/inventory/__tests__/service.test.ts`
|
||||
- `src/lib/medications/side-effects/__tests__/taxonomy.test.ts`
|
||||
|
||||
Safety contracts (structural review only):
|
||||
|
||||
- `src/lib/ai/prompts/safety-contracts.ts`
|
||||
- `src/lib/ai/prompts/safety-contracts.{en,de,fr,es,it,pl}.yaml`
|
||||
|
||||
---
|
||||
|
||||
## Closing
|
||||
|
||||
Top finding to act on next: **H2 — drop `templateFill` in
|
||||
TitrationSection.tsx**. The `t()` context already does
|
||||
`{param}` substitution; the local helper is silent-fallback drift bait.
|
||||
Two-line fix, no tests need to change.
|
||||
|
||||
Runners-up worth picking up in the W21-reconcile fix pass:
|
||||
|
||||
- **H1** (onboarding `readError`) — four files, mechanical extraction.
|
||||
- **H6** (`assertMedicationOwnership`) — 11 callsites; high signal, medium effort, defers nicely to a v1.4.26 cleanup task if the reconcile budget runs tight.
|
||||
- **M3** (`<MedicationDetailSection>` wrapper) — sets the right pattern before W19g (if it lands) or v1.5 adds a fifth detail-page section.
|
||||
- **M10** (`glp1-pk.ts` unused exports) — needs a decision call (internalise vs ship dashboard chip), not a refactor.
|
||||
|
||||
No findings I'd consider release-blocking. The Wave-4b medication
|
||||
work in particular is well-decomposed (pure modules + thin routes +
|
||||
read-only sections) — the duplications listed above are the kind that
|
||||
emerge naturally when three sibling features ship in close succession,
|
||||
not signs of a structural problem.
|
||||
Reference in new issue
Block a user