diff --git a/context/code/clients.md b/context/code/clients.md index b4062cca..c7f06583 100644 --- a/context/code/clients.md +++ b/context/code/clients.md @@ -1573,8 +1573,10 @@ Four smaller decisions that would be re-derived wrongly: Since improvise-n6t8 it shares `#bottom-dock` with the toast, which stacks **above** it, so the prompt is neither covered nor displaced. -`RenderCell::edit_text` is the stored value, not the formatted one: seeding an -editor from `text` and committing turns `1200` into the string `"1,200"`. The +An editor is seeded from `edit_seed(target)`, the stored value the target +captured, never from `text`: seeding from the formatted text and committing +turns `1200` into the string `"1,200"`. `RenderCell` carries no `edit_text` +since improvise-ugj4, because a paint can be stale while an editor is open. The `numeric`/`error` flags are the server's answers, echoed — the client must not re-derive them from the text, which is how the two ends drift. @@ -1617,7 +1619,7 @@ The browser half of improvise-cr3, kept as thin as it can be made, now talking to either of two peers (improvise-djm). **The boundary is JSON strings in and out, so this file holds no protocol knowledge at all**: it never inspects a `Command`, never builds a `CellId`, never decides whether a cell is editable or -whether a typed string is a number. Five calls are the whole harness: +whether a typed string is a number. These calls are the whole harness: | call | what it is | |---|---| @@ -1625,6 +1627,8 @@ whether a typed string is a number. Five calls are the whole harness: | `client.on_frame(json)` | fold one frame, in arrival order | | `client.render_model()` | everything to paint, already decided | | `client.edit_target(r, c)` | what an editor opened here edits (the record, the version seen, and the seed it opened on): an opaque string the page holds, or `null` | +| `client.edit_seed(target)` | the text the target's editor opens on: the seed it captured, never the paint (improvise-ugj4) | +| `client.edited_target(target)` | the same target marked edited, so retyping the seed still writes (improvise-jibp) | | `client.typed_cell(target, s)` | the frame for "the user typed s into that cell", by identity (improvise-2wu3), or none if `s` is still the target's seed (improvise-ixxx) | | `client.on_key_event(k,c,a,s,m)` | the frame for a keypress, folded locally first where this client may answer it | @@ -1832,9 +1836,10 @@ Neither arm leaves one armed. - **`editable` is the server's answer relayed through Rust**: a stored cell. An aggregate is a drill target and drill is not on the wire yet, so it is simply not offered. -- The inline editor seeds from `RenderCell::edit_text` — the STORED value, not - the drawn one: `td.textContent` has been through the number format, so a stored - 1200 reads `"1,200"` and committing that stores the *string* `"1,200"`. Enter +- The inline editor seeds from `client.edit_seed(target)` — the STORED value the + target captured, not the drawn one: `td.textContent` has been through the number + format, so a stored 1200 reads `"1,200"` and committing that stores the *string* + `"1,200"`. Enter removes the focused ``, which fires `blur`, which calls `close` again: harmless, but only by accident. - **The title bar is three fields and one joining.** `doc_name`, `doc_dirty` and diff --git a/context/code/protocol.md b/context/code/protocol.md index 97cdab54..d00dcb36 100644 --- a/context/code/protocol.md +++ b/context/code/protocol.md @@ -699,7 +699,9 @@ for that, beside `typed()`. Every commit path reaches it: - **Browser mouse.** The seed is a field of #188's `EditTarget` (`opened`), captured by `edit_target_at` from the cache's `edit_text` beside the record and its version. `typed_target_frame` starts with the `untouched` check, and - the page passes no seed (`typed_cell(target, text)`). + the page passes no seed (`typed_cell(target, text)`). The `` shows + `edit_seed(target)`, so what the page shows and what it compares against are + one capture (improvise-ugj4). **The seed is captured at open, never recomputed at commit.** Where the editor opened is where `OpenedEditor` is written. For the TUI that is `EditOrDrill` @@ -742,14 +744,16 @@ deliberately do **not** mark it: The comparison stays exact: whitespace is content, and nothing trims. -**The mouse editor tracks no `touched`.** Its edits happen in a DOM `` -that Rust never sees, so on that path `untouched` compares content alone, and -the 2054 case survives there (improvise-jibp). The fix belongs with -`EditTarget`, for example an `input` listener that reports the first edit. A -second gap sits on the same path. The `` is still seeded from the -**painted** `editText`, while `EditTarget` captures the **cache**. So a paint -that went stale while another editor was open shows old text against a newer -seed and version (improvise-ugj4). +**The mouse editor reports its first edit.** Its edits happen in a DOM +`` that Rust never sees, so the page's `input` listener, registered +`{ once: true }`, swaps its target for `edited_target(target)`, which is +`EditTarget::edited`: the same capture with `touched` set (improvise-jibp). +Before that, the mouse path compared content alone, so the 2054 case survived +there. The `` used to be seeded from the **painted** `editText` while +`EditTarget` captured the **cache**. `repaint` returns early while an editor is +open, so a paint that went stale then showed old text against a newer seed and +version (improvise-ugj4). The page now seeds from `edit_seed(target)`, and +`RenderCell` no longer carries `edit_text`, so the paint cannot be asked. This does **not** change classification. `007` typed fresh is still `Number(7)`, and `a_touched_browser_commit_still_writes` pins that. diff --git a/context/test-evidence/clients.md b/context/test-evidence/clients.md index 8d718569..d206d459 100644 --- a/context/test-evidence/clients.md +++ b/context/test-evidence/clients.md @@ -4933,11 +4933,50 @@ sends no write, and a touched one does". It wraps `Worker.prototype.postMessage` opens the editor on cell 4, and presses `Enter`. No `SetCell` may be posted. Then it types `1` and presses `Enter`, and exactly one must be. -**Known gap, filed as improvise-ugj4.** The `` is still seeded from the -painted `editText`, while `EditTarget` captures the cache. A paint made stale -while another editor was open (`repaint` returns early while `editing` is set) -therefore shows old text against a newer seed and version. -**improvise-jibp** is the other residue: the mouse editor has no `touched` flag. +Both gaps this entry used to list are closed: the stale-paint seed +(improvise-ugj4) and the missing `touched` flag (improvise-jibp). See the +entries that follow. + +### `a_mouse_target_marked_edited_writes_its_own_seed` + +**improvise-jibp**, the Rust half. A target on the seed `"1200"` commits +`"1200"` and composes nothing: that is the control. `edited()` must leave the +seed where it was, and the same text committed through the edited target must +compose a write. **Rival:** `edited()` leaving `touched` false fails this test +and `the_page_opens_its_editor_on_what_the_target_captured`, and nothing else. + +### `the_page_opens_its_editor_on_what_the_target_captured` + +**improvise-ugj4 and improvise-jibp**, through the wasm-bindgen surface the +page calls. The cell is drawn `"1,200.50"` and stored `"1200.5"`, and +`edit_seed` must return `"1200.5"`. A projected write then moves the cell to +`"777"`. A target taken afterwards seeds `"777"`, and the first target still +seeds `"1200.5"`, because a target is one capture. `typed_cell(later, "777")` +composes nothing, and the same text through `edited_target(later)` composes a +write. **Rival:** as above, `edited()` leaving `touched` false fails here. I +wrote no rival for an `edit_seed` that re-reads the cache. `EditTarget` names a +record, not a position, so that rival would need a lookup the client does not +have. + +### a mouse editor typed back to its own seed still writes — `harness/tests/contract.spec.js` + +**improvise-jibp**, the page half. The editor opens on cell 4, and the test +types the seed over the selection and presses Enter. Exactly one `SetCell` must +be posted. Before the fix the page sent `typed_cell(target, seed)` with an +untouched target, which composes nothing. **Rival:** delete the page's `input` +listener. This test fails, and the rest of `contract.spec.js` passes (16). + +### an editor opened after an unpainted write shows and commits what it captured — `harness/tests/contract.spec.js` + +**improvise-ugj4.** The test opens an editor on cell 0, so `repaint` stops. +While it is open, it delivers a synthetic frame that moves cell 4 to `777`. It +builds that frame from the last `Cells` entry the page actually received for +cell 4, changing only `text`, `edit_text` and `value`, so the record and the +version are real. Then it opens cell 4. The `` must show `777`, and +Enter with no edit must post no `SetCell`. **Rival:** seed the `` from the +paint (`drawn.replaceAll(",", "")`). This test fails on the value, and so does +"an untouched mouse editor sends no write, and a touched one does", because the +drawn text is not the seed. The other 15 pass. ### `a_browser_editor_typed_back_to_its_seed_still_writes` diff --git a/crates/improvise-wasm-client/src/lib.rs b/crates/improvise-wasm-client/src/lib.rs index e1cafc33..cd5dca15 100644 --- a/crates/improvise-wasm-client/src/lib.rs +++ b/crates/improvise-wasm-client/src/lib.rs @@ -313,11 +313,25 @@ mod tests { ], }); let first = client.edit_target(0, 0).unwrap().expect("a record cell"); - assert_eq!(client.edit_seed(&first).unwrap(), "1200.5", "not the drawn text"); - client.fold_frame(ServerFrame::Commands(vec![Command::Project(at("777", "777"))])); + assert_eq!( + client.edit_seed(&first).unwrap(), + "1200.5", + "not the drawn text" + ); + client.fold_frame(ServerFrame::Commands(vec![Command::Project(at( + "777", "777", + ))])); let later = client.edit_target(0, 0).unwrap().expect("a record cell"); - assert_eq!(client.edit_seed(&later).unwrap(), "777", "not the cache at open"); - assert_eq!(client.edit_seed(&first).unwrap(), "1200.5", "a target is one capture"); + assert_eq!( + client.edit_seed(&later).unwrap(), + "777", + "not the cache at open" + ); + assert_eq!( + client.edit_seed(&first).unwrap(), + "1200.5", + "a target is one capture" + ); assert!(client.typed_cell(&later, "777").unwrap().is_none()); let edited = client.edited_target(&later).unwrap(); diff --git a/web/tests/bundle/wasm_surface.test.js b/web/tests/bundle/wasm_surface.test.js index babcd2ca..a9f8550f 100644 --- a/web/tests/bundle/wasm_surface.test.js +++ b/web/tests/bundle/wasm_surface.test.js @@ -80,9 +80,13 @@ test("the page still reaches the client through a handful of calls", () => { "click_page_item", "click_tile", "click_view", + // improvise-ugj4: the editor shows the seed its target captured, not the paint. + "edit_seed", // improvise-2wu3/0j0f: what an editor edits, captured when it opens, so // its commit names that record at that version and not a position. "edit_target", + // improvise-jibp: the first edit of the marks the target touched. + "edited_target", "on_frame", "on_key_event", "render_model",