Wizard text is clipped to the panel and measured in cells; the list header says what is shown (review B1, B2, Codex P2 on #304)

Every wizard write goes through Panel, which clips to the panel and saturates
its height arithmetic, so no step panics on a small terminal. The bottom row
truncates in cells, so a wrapped message with a multibyte character is drawn
whole. A scrolling list's range is named on its header row, where no item can
cover it. Tests: a multibyte message at every width, wrap bytes vs cells, a
no-panic sweep of every step at 20-100 x 3-20, the first item filling the list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-24 19:11:42 -07:00
parent f00ed3e979
commit 22cbfa679e
7 changed files with 510 additions and 149 deletions
+17 -10
View File
@@ -680,15 +680,20 @@ record lands is `Plan::place`: its categories, then its date parts, then its
labels, then its measure values, or a reason it is dropped. `collisions()` and
`build_model_with_warnings` both call it, so the question the wizard asks and
the thing the build does cannot disagree. Neither has a second walk of the
record. A collision is a tuple reached by **two or more records that each
carry a measure value**. A record with no measure makes no cell, so it cannot
collide. The tuple includes label values, as `CellKey` always has.
record. **A collision is a cell that more than one record writes**, meaning a
tuple plus a measure (improvise-9bit). A `Collision` names the tuple and only
the records that share one of its cells. A record with no measure makes no
cell, so it collides with nothing. The tuple includes label values, as
`CellKey` always has.
Two records on one tuple with **disjoint** measures (one has `Revenue`, the
other `Cost`) count as a collision even though summing them loses nothing. The
unit is the record, the user's own words were "rows collide on every
coordinate", and a cell-level rule would make "which records collided" depend
on which measure you look at.
This PR's first version keyed on the tuple alone. It was **superseded in review
(improvise-9bit)**, and the argument it gave is withdrawn. Under that rule, two
records with **disjoint** measures on one tuple (one has `Revenue`, the other
`Cost`) counted as colliding although no cell sums. The wizard then offered
"Sum them into one cell" where nothing would sum, and with the `Refuse` default
the CLI refused a sparse-measure CSV that had always imported cleanly. The
question the user is asked is "these values would be added together", so the
rule has to be the one that decides whether anything is added.
The data is structured. A `Collision` holds `coords` plus 1-based `records`
positions. `WizardCache::collisions` carries it, and
@@ -710,8 +715,10 @@ at `Refuse`, which `validation_error` blocks, so a bare advance could never
leave the step.
**Separate is an occurrence index, not a row number.** This is the user's
preferred shape. Records on a colliding tuple get `1, 2, 3…` in source order,
and every other record gets `1`. The category holds only as many items as the
preferred shape. The records in a collision get `1, 2, 3…` in source order,
and every other record gets `1`. That includes a record on the same tuple that
shares no cell with the others: it lands on occurrence `1` beside the first
colliding record, and since the two write different measures, nothing sums. The category holds only as many items as the
deepest collision, rather than one item per record, and every existing
coordinate keeps its meaning. Items are added `1..=n` in numeric order
(gotcha 18). The category goes on **`Axis::None`**. Left to