diff --git a/context/code/core.md b/context/code/core.md index 7b338d3a..d4e53230 100644 --- a/context/code/core.md +++ b/context/code/core.md @@ -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. diff --git a/context/code/formula-and-io.md b/context/code/formula-and-io.md index 9330d702..2d07d8e6 100644 --- a/context/code/formula-and-io.md +++ b/context/code/formula-and-io.md @@ -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`. diff --git a/context/code/tui.md b/context/code/tui.md index a792bb5e..a4e1fec3 100644 --- a/context/code/tui.md +++ b/context/code/tui.md @@ -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`, diff --git a/context/test-evidence/formula-and-io.md b/context/test-evidence/formula-and-io.md index e2fb1193..04e4b012 100644 --- a/context/test-evidence/formula-and-io.md +++ b/context/test-evidence/formula-and-io.md @@ -1566,8 +1566,11 @@ bounded to four advances, so the rival fails it rather than stalling the suite. ### keeping_duplicates_separate_is_refused_at_the_category_cap -Twelve categories plus a collision, which is the reported import. It asserts -option (b) is disabled with a reason naming 12 and Skip, that choosing it leaves +A full set of categories plus a collision, which is the shape of the reported +import. The fixture is `category_records(MAX_CATEGORIES)`, so it tracks the cap +(12 when the test was written, 24 since improvise-felo.1). It asserts option (b) +is disabled with a reason naming the cap (`reason.contains(&cap)`, where `cap` +is `MAX_CATEGORIES` spelled out) and Skip, that choosing it leaves the step and the policy (`Refuse`) untouched, and that the message is the reason. The rival **cap checked with `>`** fails it.