Merge pull request #196 from fiddlerwoaroof/fix/projection-identities

improvise-exdg/7xyx: a coordinate cell is CellId::Coordinate; FrameGroup::cat deleted
This commit is contained in:
Ed Langley
2026-09-22 22:28:36 -07:00
committed by GitHub
9 changed files with 155 additions and 71 deletions
+43 -13
View File
@@ -614,14 +614,37 @@ broader than its reason — improvise-gcvb, which also records the two things
that make it more than a one-liner: an inserted `+cat` column means the TUI
does **not** advance, and `AddCategory` can abort.
### `CellId::Coordinate` — a drawn coordinate is not "no cell" (improvise-exdg)
A records-mode coordinate cell (the `Region` column holding `North`) used to
project `None`. That was a **third** meaning of `None`, next to the padded slot
and the blanked position: a visible cell that holds text and whose write
belongs to the host. So absence was encoding a permission, the same conflation
`Create` removed for the creating columns (improvise-poia). The pinning test,
`a_coordinate_column_is_projected_with_no_identity`, asserted the bug and is
gone.
`Coordinate` carries **nothing**. Editing a coordinate renames it across every
measure cell in the row (gotcha 1), and no client composes that operation: it
needs `record_col`, and the browser declines every records-mode edit before
reaching a cell. Giving the variant a `RecordHash` would hand a client a
surrogate with nothing to spend it on. The operand belongs with the command
that consumes it, whenever that command exists. Nothing a client can do has
changed. `render.rs::editable` is now an exhaustive `match` rather than a
`matches!`, so the next variant has to be classified. `typed_cell_command`,
`record_at_cursor` and `record_cell_at_cursor` still answer `None` for it.
So paste onto a coordinate is still a silent `Upstream` in the browser. The
TUI has declined it out loud since PR #184. That divergence is
improvise-kkcy, and a named kind is what lets the browser decline deliberately
now.
### `CellDisplay::id` — `None` is not "aggregate"
That is `Some(CellId::Aggregate(..))`, and the distinction is the whole point
of the enum. `None` means the slot holds **no projected cell**: a `GridCache`
position padded by `fit_to_labels`, blanked by a reframing, or a
records-mode column that is not a cell to write into at all (a category column
renames a coordinate across the whole row, gotcha 1, which is a different
intent from `SetCell`). Conflating the two either hides a cell or offers a
position padded by `fit_to_labels`, or one blanked by a reframing. Every
records-mode column is some kind of cell (`Record`, `Derived`, `Create` or
`Coordinate`). Conflating the two either hides a cell or offers a
write the type system exists to make unconstructible.
### `CellDisplay::edit_text` — the difference corrupts data
@@ -637,8 +660,8 @@ and an editor's seed is exactly a feedback path.
It is the projection of `App::display_value_at_cursor`, which is what
`EditOrDrill` seeds the TUI's buffer from, so both front ends start from one
string rather than each deriving one. `None` where there is nothing to edit:
the same set of positions `id` is `None` for, plus cells whose value is
absent. An error cell carries none, because its text is a diagnostic and not
positions whose `id` is `None`, plus pivot cells with no key. A records-mode
cell always carries one, even an empty one. An error cell carries none, because its text is a diagnostic and not
a value.
A browser found this: `tests/lockstep.rs` drove `i` through the host and the
@@ -785,9 +808,16 @@ one**. `AxisFrame::flat` leaves `cats` empty on purpose, so it is not a way to
smuggle a multi-level axis in flattened.
`FrameGroup::collapsed` is on the wire rather than left to the client because
it is a fact about the *view*, which only the server holds. `FrameGroup::cat`
travels so a client can echo it back in `ToggleGroup` without ever working out
which category a label came from — which slash-joined labels made impossible.
it is a fact about the *view*, which only the server holds. There is no
`FrameGroup::cat` any more (improvise-7xyx). Its doc comment promised that a
client echoes it back in `ToggleGroup`, but no client ever did. `resolve.rs`
has no `toggle-group-under-cursor` arm (`z` is on the still-needs-the-host
list), and neither renderer read the field. A promise about an interaction no
code performs is the stale-sibling defect, so the field went with it. It is
not a loss for `z`. The TUI's `ToggleGroupAtCursor` also places the cursor on
the layout the toggle *will produce*, which a thin client cannot compute. So
composing `z` in a browser needs its own design, and the operand should come
back with that design rather than wait for it.
### `AxisFrame`: four accessors that are **not for a draw loop**
@@ -1294,13 +1324,13 @@ frozen behaviour reads as decided afterwards and nothing says it was only frozen
for the duration of the move.
**The ticket's own proposed fix does not work, and that is worth more than the
fix.** h0iv suggested comparing the `(cat, name)` pair, `FrameGroup` carrying
`cat` precisely so a client need not work out which category a label came from.
fix.** h0iv suggested comparing the `(cat, name)` pair; `FrameGroup` carried
`cat` then (improvise-7xyx has since removed it).
But its own reachability case is *one* category: `Month` grouped into `Q1`,
nested under `Region`, giving `North/Q1` and `South/Q1` adjacent — the two
headers are `(Month, Q1)` and `(Month, Q1)`, **equal as values**, so the pair
separates nothing. `the_two_headers_are_equal_as_values_so_comparing_cat_and_name_cannot_split_them`
pins that, so the next reader does not re-propose it. What separates them is
separates nothing. `the_two_headers_are_equal_as_values_so_comparing_them_cannot_split_them`
pins that as whole-value equality, so the next reader does not re-propose it. What separates them is
that they are two different `FrameEntry::Group` entries, and `groups()` already
borrows each from `self.entries` — so the comparison is `std::ptr::eq` on the
header, which is the literal statement of "the same header opened both".