From c88b4b5d6968fcfca2a30608ad25578bc26cf588 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 22 Sep 2026 21:08:20 -0700 Subject: [PATCH] docs: CellDisplay::value and the dsxq evidence Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- context/code/protocol.md | 35 ++++++++++++++++- context/test-evidence/clients.md | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/context/code/protocol.md b/context/code/protocol.md index 7b797818..fdbf9e8e 100644 --- a/context/code/protocol.md +++ b/context/code/protocol.md @@ -552,8 +552,39 @@ client over the same grid, and they disagreed by a comma. `numeric` and `error` are decided by the server for the same reason: it is the side that knows which it was, and re-deriving client-side is how the two -ends drift apart. `CellDisplay` is deliberately not a `CellValue` — by the -time a cell reaches a client the number formatting has already happened. +ends drift apart. `CellDisplay` is deliberately not *drawn from* a `CellValue` +— by the time a cell reaches a client the number formatting has already +happened, and `text` is what a renderer paints. + +### `CellDisplay::value` — what `yank` copies (improvise-dsxq) + +The value the host evaluated at this position, the same `Option` +that `text` was formatted from: `evaluate_aggregated` over the view's +`none_cats` in a pivot grid, `records_value` in a records grid. It exists +because the browser's `yy` has to copy what the TUI's `YankCell` copies, and +neither string on the cell can be turned back into it: + +- `edit_text` is `CellValue::to_string()`, which writes a non-whole number + `{:.4}` — `1.23456789` becomes `1.2346`. +- Re-parsing any text with `typed()` turns number-like text into a number — + `Text("007")` becomes `Number(7.0)`. +- In a pivot grid `edit_text` is the *stored* cell (`get_cell`), so an + aggregate or a formula cell has none and yanked nothing at all. + +So the value is **projected**, not re-derived (gotcha 47): a client that +wants the value asks for the value. It is kept separate from `edit_text` +rather than replacing it, because the two answer different questions — what +an editor starts with is a *string*, deliberately the one the TUI's buffer +holds, and changing it would change what the TUI's own editor shows. + +It cost `Eq`: `CellValue` holds an `f64`, so `CellDisplay`, `GridCache` and +`GridUpdate` are `PartialEq` only. Nothing hashed or `Eq`-bounded them. + +In a records grid the TUI does **not** yank this value: `YankCell` and +`PasteCell` resolve the synthetic `(_Index, _Dim)` key, so the TUI yanks +`None` and pastes a junk cell into the document (improvise-6p8q). The browser +is right there and the TUI is wrong, which is why the lockstep yank test stays +on a pivot grid. ### `AxisFrame` — the grid's frame, and why it is *one* type diff --git a/context/test-evidence/clients.md b/context/test-evidence/clients.md index fd39ad65..38de6f6e 100644 --- a/context/test-evidence/clients.md +++ b/context/test-evidence/clients.md @@ -4101,6 +4101,12 @@ so the paste is evidence about the yank rather than about a hand-seeded field. **Fault injected:** the `"yank"` arm of `composed` renamed. Fails on the yank. +**Superseded in part by improvise-dsxq** (entry at the end of this file). The +fixture's `1200` is the one kind of value on which the edit text and the value +agree, so this test could not see that the arm yanked the edit text re-parsed. +The arm now yanks `CellDisplay::value`, and the test sets that field on its +hand-built cell. The parity claim lives in the lockstep tests, not here. + ### `a_second_key_that_matches_nothing_still_spends_the_prefix` `App::dispatch_key` `take()`s the prefix before looking the second key up, so @@ -4177,3 +4183,62 @@ and converged. **Fault injected:** the same `Binding::Prefix` → `Upstream` reversion. Fails on the second press. + +## improvise-dsxq — the browser's `yy` copies the value, not a string + +PR #161 (improvise-lut7) made the browser's `yank` arm compose +`SetYanked(typed(edit_text))`. The TUI's `YankCell` yanks +`evaluate_aggregated(key)`, the value itself. Those differ in three ways, and +#161's only fixture, `1200`, hits none of them: + +- `edit_text` is `CellValue::to_string()`, which rounds a non-whole number to + four places. +- `typed()` turns number-like text into a number. +- In a pivot grid `edit_text` is the stored cell only, so an aggregate or a + formula cell has none and the browser yanked `None`. + +The fix projects the evaluated value as `CellDisplay::value`, and the arm yanks +that (`context/code/protocol.md` §3). Reproduced at `40b8913` by both tests +below before the fix: the browser yanked `Number(1.2346)` for `1.23456789`, and +`None` for the aggregate. + +### `yy_then_p_copies_the_value_the_host_copies` + +Three cases, one per disagreement, each on a real `App` and a `ClientState` +seeded by `grid_of`. The host presses `y y p` through `reduce_key`; a twin +host applies exactly what the client composed. Each case asserts the host +yanked the expected value (so a fixture that yanks nothing cannot pass), that +the client yanked the same, that the host's paste landed, and that the two +documents match. Sources sit at row 2 or 3 and targets at row 1, never row 0, +and the first column is an unused `Units` measure, so no cursor is at index 0. + +- `1.23456789`, a non-whole number. +- `Text("007")`, number-like text. +- `Profit = Revenue - Cost` over `10.123456` and `0.5`, a formula's value, + pasted into a stored `Revenue` cell. + +**Faults injected, each run:** +- The pre-fix arm, `typed(edit_text)`. Fails on the first case with + `Number(1.2346)`. +- The arm yanking `typed(&value.to_string())`, the projected value re-rendered. + Fails on the first case with `Number(1.2346)`. +- The producer coercing number-like `Text` through `typed()`. The first case + passes, and the second fails with `Number(7.0)`. +- The producer projecting the raw `get_cell` instead of the evaluated value. + The first two cases pass, and the third fails with `None`. + +### `yy_over_an_aggregate_copies_the_sum_the_host_copies` + +`Product` on the None axis with two stored records, `1.23456789` and `1.0`, so +the cell at row 1 is an `Aggregate` with no stored value. Yank only, since an +all-aggregate grid has no record cell to paste into. The host must yank +`Number(2.23456789)`, which rules out a fixture that is not an aggregate. + +**Faults injected, each run:** the pre-fix arm (fails with `None`), the +re-rendered value (fails with `Number(2.2346)`), and the raw `get_cell` +producer (fails with `None`). + +Records mode is deliberately not covered. There the TUI yanks `None` and its +`p` writes a junk `_Dim`/`_Index` cell, because it resolves the synthetic key +(improvise-6p8q). The browser is right and the TUI is wrong, so a lockstep +case there would assert the bug.