The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
Finishes round 1. Part 1 took the runtime bugs; this takes the prose that
had gone false and the three "the comment says so" gaps behind it.
CLAUDE.md is auto-loaded into every agent session and ui/README.md is
not, so an architecture paragraph that still described a flat components
bag was the most expensive stale sentence in the repo: the next agent
adding a popover reads it, sees no platform layer, and writes the generic
component into components/ — the invisible-layer failure this epic exists
to end. It now names components/ui/ and says the boundary is lint-enforced.
The canonical-patterns table gained the three rows the epic established:
ui/Button over a hand-rolled <button>, a keymap row over a private keydown
listener, and disabled-when-busy vs inert-when-inapplicable.
ui/README.md's inventory was written in this branch's first commit and
never learned about CommandPalette, ContextMenu/useContextMenu,
KeyboardHelp or gridColumns — in a section whose own closing rule is that
a second component painting the same thing is how the last inconsistency
happened. It also claimed "everything is rem-derived" while VirtualGrid's
minCol/GRID_GAP are deliberately px; that is now stated as the exception
it is, and a new section records which promises here are tests rather
than sentences.
Three contracts stopped being prose:
- A text-less Button must carry a name. "Required for iconOnly" was a doc
comment, which is why the migration shipped two nameless icon buttons
through eight green lanes. It is a union now, covering both spellings
(iconOnly, and size="icon"/"icon-sm"), satisfied by label, aria-label or
title. It caught one more on the way in: Banner's dismiss took an
optional dismissTitle, so it could render unnamed — it falls back to
common.close now.
- gridColumns' gutter arithmetic. Every existing assertion survived
replacing floor((w + GAP) / (minCol + GAP)) with the naive
floor(w / (minCol + GAP)): the fixed widths agree, the capped cases are
cap-bound, and the loops compare columnsFor against itself. Pinned at
the exact-fit boundaries where the two first disagree (536 and 828).
- ShareDialog's link row. Only Copy was migrated in S1, leaving one 2rem
button between two 1.75rem ones — and on a coarse pointer .touch-target
grew only the migrated one to 2.75rem while the destructive Revoke
stayed small.
Also: the maxCols docstring said 8 columns of 384px where the test it
shipped with asserts 369.
The viewport contract has two ends, and only the floor was enforced. An auto-fill grid
answers "more room" with more columns, never bigger ones, so it settles just above its
minimum column at any width: measured at 3440px, the feed laid out 11 columns of 264px.
A feed diluted across an ultrawide fails the contract as surely as a broken one at the
floor.
Multi-column feed views now carry a `maxCols`; past it the leftover width flows into
the items. At 3440: cards 11x264 -> 8x370, cardsSmall 15 -> 11x264 (still visibly the
dense card, and never as wide as cards gets), tiles 7 -> 6x498. Each cap is set so the
item tops out near 1.5x its own minCol, which is what keeps a view recognisably itself.
A cap can only bind ABOVE the width at which that many columns already fit, so it is a
pure ultrawide affordance — 1024, 1920 and 2560 lay out exactly as before. Verified in
the browser at all three widths and across all five views.
The Channels grid needs no cap: the page's own 96rem --content-mw already bounds it.
Also renames VirtualGrid's `maxCol` to `colMaxWidth`. It was never a column count — it
is the single-column reading-width cap — and `maxCol`/`maxCols` one letter apart is the
kind of drift that has bitten this repo before.
The column maths moves to ui/gridColumns.ts as a pure function so both ends of the
contract are unit-testable without a layout engine; the tests assert the cap's numbers,
that it never binds at the floor, and that the dense card cannot grow into the standard
one.