docs: describe the derived cap and cite the coupling beads (improvise-felo.1)

Review of #307 at 7b9e868b: the duplicate-step evidence and argument still
said 12, tui.md bounded a collision tuple by twelve, and the coupling
inventory named no beads. The page-bar entry now notes its unclipped
set_string can overwrite the right border.

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:42:19 -07:00
parent 7b9e868bd2
commit cc81e71a85
4 changed files with 26 additions and 15 deletions
+14 -8
View File
@@ -598,40 +598,46 @@ fixture that fills to the cap either derives it or fills until
**Degrades as categories grow.** These are not fixed here; each is a place
where 24 is worse than 12. Most were already reachable at 12.
- **The TUI row header is the sum of its levels.** In `src/ui/grid.rs`, around
- **The TUI row header is the sum of its levels** (improvise-gtn7.5). In `src/ui/grid.rs`, around
`row_header_width`/`data_area_width` (and
`ui::viewport::compute_row_header_width`), each level takes 4 to 24 cells.
Categories on Row therefore cost at least 4 cells each, and the data area
saturates to zero width with no data drawn.
- **Column headers take one line per level, but the keypress path does not
count them.** `App`'s `visible_rows` is `term_height − 8`, a fixed allowance
count them** (improvise-gtn7.6). `App`'s `visible_rows` is `term_height − 8`, a fixed allowance
that ignores the column levels, the group row and the page bar. So with
several categories on Column, the cursor can scroll below the drawn grid.
- **The page bar is one unclipped line.** It is `GridWidget::render`'s
`page_coords` join. Default seating (`View::on_category_added`) sends every
- **The page bar is one unclipped line** (improvise-gtn7.7). It is
`GridWidget::render`'s `page_coords` join, drawn with a bare `set_string`
that is not clipped to the inner rectangle, so a long bar can overwrite the
block's right border. Default seating (`View::on_category_added`) sends every
regular category after the second to Page. A 24-category import therefore
puts 22 selectors on one line, clips most of them without a mark, and opens
on a single slice.
- **Side panels do not scroll.** In `src/ui/panel.rs`, an item past
- **Side panels do not scroll** (improvise-gtn7.8). In `src/ui/panel.rs`, an item past
`item_height` is not drawn, and the cursor can move onto it unseen. The
category panel lists every category plus its expanded items.
- **The wizard's field list does not scroll.** Its `ReviewProposals` arm in
- **The wizard's field list does not scroll** (improvise-8jk6.1). Its `ReviewProposals` arm in
`src/ui/import_wizard_ui.rs` breaks at the popup's bottom (the popup is at
most 30 rows high), unlike the steps that use `scrolling`. About 22 fields
fit, so the over-the-cap case cannot show every field it asks the user to
fix.
- **Loading the store is O(k log k) per cell and O(cells × k) in memory.** It
- **Loading the store is O(k log k) per cell and O(cells × k) in memory**
(improvise-felo.2). It
measured 3× from 12 to 24 (the table above).
- **A fit declines more columns.** `unaffordable_categories` drops the widest
category columns once the encoding passes `MAX_FEATURES`. A wide document
is fit on fewer of its categories, and the fit says so.
- **`cross_product` is exponential in the categories placed on one axis**, and
`prune_empty`'s `has_value` matrix is rows × columns. The cap never bounded
either.
either. Virtualizing the grid is improvise-felo.3.
- **Header repeat suppression costs O(levels) per position and level**
(`render.rs::shows`). That is O(rows × levels²) per frame, which is small at
24.
The records-mode build's quadratic (improvise-sk32) is not category coupling;
it is listed above because this measurement found it.
The browser adapts where the TUI does not. `#tiles` and `#categories` scroll,
and `web/shell.js` charges each column level to the extent.
+6 -4
View File
@@ -736,14 +736,16 @@ is already a category. `suggested_duplicate_category` lives on the pipeline, so
the CLI's `separate` uses the same rule.
**At the category cap, Separate is shown and disabled.** The reported import
already had 12 regular categories, which is `MAX_CATEGORIES`. Option (b) adds a
had 12 regular categories, which was `MAX_CATEGORIES` when this was written.
Option (b) adds a
**regular** category, and the user declined a hidden virtual one. So at the cap
the option is drawn "(unavailable)" with its reason and remedy: go back and
Skip a column or make it a Measure, or choose Sum. The reason is decided in
`ImportWizard::duplicate_options`, so every front end words it the same way.
This is the conservative reading, and it is **an open question for the user**:
lifting the cap for this one category, or counting it outside the cap, is a
model-layer decision this ticket did not take.
This was the conservative reading, and it left an open question for the user.
**The user answered it on 2026-09-25 by raising the cap to 24**
(improvise-felo.1), so "keep separate" is available for the reported import
now. The option is still disabled for any import that fills the new cap.
**Headless paths refuse by default.** `improvise import --no-wizard` has
nobody to ask, so `--on-duplicate refuse|sum|separate` defaults to `refuse`.
+1 -1
View File
@@ -581,7 +581,7 @@ and the list can never scroll away. It is by item, not by line, because the
wizard state cannot know the panel width that line-level clamping needs. The
residual is honest. A **single** item taller than the whole list area, roughly
1,500 characters at 80 columns, is still clipped at the panel's bottom. No
wizard warning is that long today: a collision tuple is bounded by twelve
wizard warning is that long today: a collision tuple is bounded by `MAX_CATEGORIES`
categories plus labels, but a label value is not bounded.
**Every write is clipped to the panel.** The widget draws through `Panel`,