# REVIEW_02 — Simplification & maintainability audit Date: 2026-05-08 Scope: full repo at `main` (commit `1dba877`), against SPEC.md as of the same SHA. Sister document to `doc/REVIEW_01.md` — that one captures security and "fishy patterns"; this one captures **overcomplicated solutions that could be rewritten more simply with no functional change.** This document is meant to be referenced on each refactor loop alongside SPEC.md and REVIEW_01.md. Findings are ordered by severity (highest first) and each carries: - **ID**: stable handle (`R02-Nxx`) — quote when committing a fix. - **Severity**: HIGH / MEDIUM / LOW. Severity here measures _maintenance pain_, not security. HIGH = a real bug surface (already caused regressions, or two-place edits routinely diverge); MEDIUM = meaningful churn / readability / tested-once-then-forgotten; LOW = cosmetic / churn for churn's sake unless the area is being touched anyway. - **Status**: `open` / `accepted-by-design` / `fixed-in-`. - **Where**: file paths + line ranges (current; will shift after edits). - **What**: the pattern that's overcomplicated. - **Why it's worth fixing**: what breaks today, or what the next contributor will trip over. - **Suggested rewrite**: shape of the change, not a full diff. Keep the spec invariants intact: vanilla JS, strict CSP, Twig auto-escape, audit-row-per-mutation, capacity math under §5. When a finding is closed, set Status to `fixed-in-` and append a short note. Do not delete entries — they're history, same contract as REVIEW_01. --- ## How to use this with /loop 1. Pick the lowest-numbered `open` finding whose severity ≥ MEDIUM. 2. Re-read SPEC.md §5 (capacity math), §7 (audit), and §9 (phase log) to confirm the invariant the simplification must preserve. 3. Apply the rewrite, add/adjust tests, update SPEC.md if a public contract changes. Most findings here are pure internals — no spec touch required. 4. Commit; flip Status to `fixed-in-` and add a one-line note. 5. Commit the doc update separately. --- ## HIGH ### R02-N01 — Capacity formula duplicated in PHP and JS - **Severity**: HIGH (already a documented two-place-edit hazard). - **Status**: open. - **Where**: - `src/Services/CapacityCalculator.php` — authoritative implementation. - `public/assets/js/sprint-planner.js` lines 172–222 — `recomputeRow()` + `applyServerCapacity()` reproduce the same `round_half`, `after_reserves`, `committed_p1`, `available` chain on the client. - SPEC.md §5 calls this out explicitly: _"Runs identically in `App\Services\CapacityCalculator` (PHP) and in `sprint-planner.js` (JS). Any edit must touch both."_ - **What**: The capacity rule (the core domain invariant of the app) lives in two places. The JS copy exists so the on-screen totals update during typing without waiting for the server round-trip. Every PATCH response already returns the freshly computed `per_worker` values though, and `applyServerCapacity()` is called immediately after each save (lines 249, 559, 612). - **Why it's worth fixing**: any future change to capacity (a new reserve type, a different rounding rule, priority-3 tasks, anything) must be made twice. The invariant is the kind of thing that gets changed during product evolution; missing one side has produced subtle display bugs in similar projects historically. - **Suggested rewrite**: drop `recomputeRow()`'s arithmetic. Two paths: - **Path A (preferred):** keep typing-responsive feel by relying on the existing 400 ms debounce — the user usually pauses before expecting Available to update anyway. Render `Available` from server values only; on edit, optionally show a "saving…" indicator. - **Path B:** keep an immediate visual update but make it a *plain sum* (no reserve arithmetic) so the formula stays single-source. Server PATCH response is authoritative for Reserves/Available. Either way the JS file shrinks by ~30 lines and the spec's "any edit must touch both" line in §5 goes away. ### R02-N02 — `buildTaskRow()` mirrors the Twig partial by hand - **Severity**: HIGH (already the source of two phase-bound hotfixes). - **Status**: open. - **Where**: `public/assets/js/sprint-planner.js` ~lines 361–510 — `buildTaskRow(task, ...)`. - Twig source of truth: `views/sprints/_task_list.twig` lines 200–283. - Historical regressions documented in SPEC.md §9 Phase 10: - hotfix `7c298d3` — owner dropdown empty in JS-built rows because `ownerChoices()` scraped the pre-Phase-10 `[data-owner-filter] option` selector that no longer existed. - hotfix `23ab365` — JS-built cells missed `data-col`, breaking the Columns dropdown and Phase 13 focus auto-hide. - Plus the `whitespace-nowrap` re-mirror in the "Task table polish" commit `f204611`. - **What**: when an admin clicks "+ Add task", the new row is built in JS to avoid a page reload. The function manually emits ~15 data attributes + ~10 CSS classes that the Twig partial already emits on server-rendered rows. Every time the partial changes, this function silently drifts. - **Why it's worth fixing**: it's a recurring foot-gun. The partial is the spec for what a task row looks like; `buildTaskRow` is a forgetful copy that has bitten the project at least three times. - **Suggested rewrite**: server-rendered hidden `