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",