Files
improvise/crates
Edward Langley 585f25bf78 Invalidate the cursor by observing the grid's frame, not by a per-effect table
improvise-ni6k. Twelve effects called `app.reset_cursor()`; that table is
the shape gotcha 24 names as the disease, and it had a hole the day it
was written. The observation replaces it: `view_frames` fingerprints
every view's drawn frame, `apply_effects` diffs it per model effect, and
the cursor resets when the view it is in reframes. No effect declares
anything, and `adopt_cursor_epoch` reads the same set.

FOUR THINGS THE TICKET DID NOT PREDICT, each one a fixture or a design
question rather than a line:

1. The frame fingerprint and `layout_version` answer DIFFERENT questions,
   so they are two observations and not one. `AddCategory` of an empty
   category moves no row or column but does change the coordinates a
   client's CellKey hangs off — layout_version must bump, the cursor need
   not. `view_shape`/`last_reshaped_views` (config) is untouched and still
   drives `layout_version`; `view_frame`/`last_reframed_views` (drawn) is
   new and drives the cursor. Unifying them broke two tests in both
   directions, which is how the distinction was found.

2. The observation must run INSIDE the effect loop, not after it. At
   end-of-batch it clobbered deliberate cursor placement in the same batch
   — `[AddItem(measure), <place cursor>]` sent the cursor home, failing
   three records-mode tests. In-loop, a later effect wins, which is the
   same ordering property gotcha 45 gives `document_config`.

3. `GridLayout::new` does NOT apply `prune_empty`; `with_frozen_records`
   does. A frame hash built on `new` cannot see a pruned row, which is
   exactly the class improvise-7kvw is in.

4. Two per-effect no-op guards were carrying the whole weight and are now
   orphaned: `effective_items` and `item_is_drawn` are deleted, along with
   six inline `moved`/`drawn`/`real`/`existed` predicates.

COST, measured as the ticket required, `benches/reshape_observation.rs`
(new), one SetCell batch, same fixtures as `grid_projection`:

    typical   400 cells   172µs -> 206µs   (+20%)
    large    9.6k cells  3.70ms -> 4.17ms  (+13%)
    18k cells            7.09ms -> 8.41ms  (+19%)
    pathological 90k    35.2ms -> 39.8ms   (+13%)

The batch is dominated by `rebuild_layout`/`recompute_formulas`, not by
the fingerprint.

WHAT CLOSES AS A CONSEQUENCE: improvise-7kvw, asserted by
`paging_across_a_prune_that_drops_a_row_invalidates_the_cursor`.
AddFormula/RemoveFormula join the shared `reshaping_commands` table (a
formula target is a `_Measure` item, so it is a column — gotcha 7), each
proved reshaping by the table's own `assert_ne!` on the grid shape.

WHAT DOES NOT CLOSE, and this is the honest part: `SortData`. A pure
record reorder changes no label and no count, and records-mode `row_items`
are positional, so a row_items fingerprint cannot see it. I wrote the
mechanism (hashing `layout.records`' coords) and then REMOVED it, because
I could not build a fixture where `SortData` actually reorders anything —
records came back in key order whatever order the cells went in. Shipping
a confident comment over a mechanism I could not demonstrate is this
repo's named defect, so it is reported instead. See the hand-back.

Negative arms unchanged and still passing, including the refused-command
arm and `SetNumberFormat`; disabling the observation fails the whole
reshaping table, which is the proof it is what does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 20:45:34 -07:00
..