Illegal states, unrepresentable
An architecture review found both duel pages still carrying the same ninety-line derivation chain by hand. The fix collapses sixteen flat props into one discriminated model.
An architecture review looking specifically at the duel and PvP hot spots found that the panel’s ownership rule had never actually stopped being duplicated. Both /pve and /pvp still carried their own roughly ninety-line chain deriving ownership, the display word, the collision index, and the chip content, each copy carrying the same bug-fix comments referencing the same old issue numbers, because sharing the small ownership function hadn’t gone far enough to share everything downstream of it.
The fix moved the whole chain, not just the first step, into a single function that returns one discriminated model instead of handing DuelWordPanel sixteen separate flat props to reconcile on its own:
export function selectWordPanelViewModel(inputs: WordPanelInputs): WordPanelModel
WordPanelModel is a union of exactly four kinds, player-input, player-animating, opponent-draft, opponent-animating, each carrying only the fields that kind actually needs. Sixteen flat props could combine into states that never made sense, a draft that was somehow also the input, an opponent panel marked used when only the player’s own word ever gets marked that way, and nothing in the type system said so. Four kinds instead of sixteen booleans-and-strings means those combinations are no longer just untested, they’re impossible to construct. One input fell out entirely along the way: whether the player’s last word had crashed used to be its own field both pages had to set correctly; it’s now derived from the collision step index the panel already tracks, which is one less thing for two call sites to keep in sync with each other.
Nothing about what a player sees changed. The two pages still render bit-for-bit the same PvE and PvP behavior, since this was a seam moving, not new behavior arriving; the screen-reader announcement regions stayed exactly where they were, in each page rather than the shared component, since PvE and PvP genuinely announce different things for reasons specific to each mode. “Word panel” earned its own entry in the project’s shared terminology alongside the change, so the next person reading the code or a future issue about it reaches for that name rather than inventing a new one for the same idea.