PR #162 (improvise-xg1z) made render.rs paint edit_seed when Editing
arrived without its edit buffer, but commit and append-char still read
the absent buffer as the empty string. So the overlay showed the value,
Enter composed ClearCell, and a typed character replaced the text
instead of extending it: two derivations of what is in the editor.
The fold now seeds the missing buffer (ClientState::settle_editor, next
to settle_mode), and render.rs reads the buffer through read_buffer, as
commit does. edit_seed goes back to pub(crate) and no renderer calls it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
In the TUI, EditOrDrill hands an aggregated cell to DrillIntoCell, so `i`
on an aggregate is drill by another door. The help page called it only
"Edit current cell", and the browser refused it in words unrelated to `>`'s,
so a user could not tell the two were one missing feature.
- help.rs: the edit-or-drill row says it drills into an aggregate.
- help.rs: Normal-mode Enter was captioned "Edit current cell (same as i)"
but is bound to enter-advance alone, which only moves down; the row moves
to Cursor movement and says so.
- resolve.rs: the Aggregate arm now reads "an aggregate is edited by
drilling in — " + not_yet("drill"), the same sentence `>` uses.
- context/code/clients.md and the evidence entry reverse the recorded
"different sentences on purpose" rationale, which was false about the TUI.
- harness/tests/drill.spec.js follows the new wording.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
A SEMANTIC merge conflict that auto-merged cleanly: PR #160 added
`vocabulary_is_composed.rs`, whose `NOT_COMPOSED` list names `SetYanked`
with the verdict "improvise-lut7: nothing resolves yank, so paste reads a
permanently empty slot". This branch is the fix for improvise-lut7, so
that entry is now false.
git reported no textual conflict — the two sides touched different files —
and `the_uncomposed_list_names_nothing_that_is_composed` caught it on the
merged tree, which is exactly the both-directions check #160 added it for.
Worth stating because the two PRs agree on the diagnosis: the list cites
the ticket this branch closes, and the guard is what turned that agreement
into a build failure rather than a stale sentence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
ChatGPT Codex on PR #161, and the finding was correct. The structural
guard counted `requested(name).is_some()` as "answered", but `chord`
maps `Step::Request(_)` to `None` — so a future prefix binding for
`save` would have PASSED the guard while the request was discarded at
runtime. That is the exact shape improvise-lut7 was filed about: a green
derivation over a broken thing.
The guard now refuses a request-backed prefix name. `prefixed` is split
so the binding step is `chord(state, event, Option<&Binding>)`, which
lets a test drive a binding the shared table does not contain — the
regression the review asked for. It shows the outcome is the disarm
ALONE, and asserts `requested("save").is_some()` first so the guard's
refusal clause is known to be able to fire.
Two faults injected. Binding `save` behind `g` in `default_keymaps` —
the scenario the review described — fails the guard, naming `save`. And
making `chord` return `KeyOutcome::Request(r)` (the review's other
suggested option) fails the new test, because the disarm is then lost:
that is the evidence the alternative trades one stranding for another.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
improvise-k84e, scoped to what PR #109 left: a frame whose LoadModel
SUCCEEDS, rebases every mirror and emits Snapshots, and is THEN rolled back
by a later command in the same frame.
The document half was already covered by
a_rolled_back_load_leaves_no_ghost_view_and_no_false_status. This covers the
Sessions half, which nothing touched: mutating_frame clones the whole
Sessions before the batch and undo_frame puts the clone back, so the reseed
is undone with it and the computed Snapshots are discarded rather than sent.
It reproduced nothing — the mechanism is sound, as the bead predicted. Its
value is the fault it can see. Injected: `self.sessions = mirrors;` replaced
by `let _ = mirrors;`. The test failed on the projection assertion, naming
the asker's mirror and printing the whole correcting batch — the restored
document being re-sent to a client left holding the discarded one, which is
a client hearing about a load that never happened.
Three things keep it non-vacuous: a separate control asserting the fixture's
load really does rebase both clients, the generation check made before
anything is concluded from the rollback, and a bystander seated where a
wrong relay could move it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
`render_model` built `editing` as
`is_editing().then(|| buffers.get("edit").cloned().unwrap_or_default())`,
so an `AppMode::Editing` that arrived without its `SetBuffer` painted
`Some("")` — a blank editor drawn over a cell that still holds a value.
The page opens an `<input>` seeded from that string, so the next Enter
commits the empty string and the value is gone. An ABSENT buffer and an
EMPTY one are two different facts, and `unwrap_or_default` collapsed them.
Reachable because the pair is two `Command`s rather than one: this
client's own resolver emits `SetBuffer` then `ChangeMode`, and a
`Snapshot` or a host batch can carry the mode without the buffer.
The fallback is `improvise_protocol::edit_seed`, which is the EXISTING
home of "what an editor opened here starts with" — `seed_at` at the
cursor, the function `enter-advance` already seeds from. A second
reconciliation of the scroll offset inside `render.rs` would have been
gotcha 42's own trap, so this costs one `pub` rather than a copy.
Two tests, and the second is the one that matters for the shape of the
fix: an empty buffer is a state the user typed their way into with
backspace, so refilling it would make the last backspace un-doable. The
over-broad repair — "empty or absent, refill" — fails that test alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
`resolve` answered `Binding::Prefix` with `KeyOutcome::Upstream`
unconditionally, and `Host::handle` answers `ClientFrame::Key` with an
explicit empty vec (gotcha 47) — so `g`, `y` and `Z` were black holes and
everything behind them was dead. `grid_move`'s `GridMove::ToStart(true)`
arm was correct and unreachable, so a browser user could reach the last
row and never get back to the first.
Arming is now a `Command::SetTransientPrefix`, which was already on the
wire and already folded by `reduce_view`; nothing composed it. The second
key is resolved in the sub-keymap through `Keymaps::prefix_keymap`,
mirroring `App::dispatch_key` line for line — including that the prefix
is spent whether or not the second key matched, because leaving it armed
strands every key after it in a sub-keymap the user has forgotten about.
Every outcome of a chord therefore carries the disarm, so the second key
can never be `Upstream`: there would be nowhere to put it. The two names
that reach no composition, `wq` and `toggle-col-group-under-cursor`, are
answered by `declined` instead (improvise-rmvx: silence was worse), and
`every_name_bound_behind_a_prefix_is_answered_by_this_client` makes that
structural — a future prefix-bound name nothing answers fails the build.
`p` could never do anything because nothing could put a value in
`ViewState::yanked` (improvise-lut7) — a *state* reason no name-level
derivation can see, which is why paste appeared on neither the declined
list nor the host-needed one. `yank` now composes, off the `edit_text`
the host already projects, which is the value `YankCell` evaluates.
The three prefixes leave `the_command_names_that_still_need_the_host_are_these`.
Not predicted by either ticket: `tests/lockstep.rs` — the instrument for
"host and client agree on a keypress" — passed unchanged through all of
this, because no chord had ever been driven through it. Every key it
pressed was a single key. `the_g_chord_returns_to_the_first_row_on_both_ends`
closes that gap and compares both ends after *each* press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
improvise-m0sx. `Command::ClearBuffer(String)` was declared on the wire and
composed by nothing in production: its only occurrences were the enum, the
fold arm, and two of reduce.rs's own tests. Its fold arm removed the buffer
key, while the only two implementations of its own name — the host's
`clear-buffer` command and resolve.rs::write_buffer — both compose
`SetBuffer { value: "" }`, which inserts. So the variant was invented rather
than derived and disagreed with both ends.
Deleted rather than given an effect. Adding a ClearBuffer effect to justify
the variant reverses decision(improvise-cqi, command_set_derived_not_invented),
and it would buy nothing: both readers of the map end in unwrap_or_default(),
so an absent key and an empty one are indistinguishable to every reader.
The guard is the generalisation. vocabulary_is_composed.rs reads the variant
list off serde's derive — the one enumeration the compiler maintains — and
requires each name to be constructed in non-test source, or named in a short
NOT_COMPOSED list with its verdict, checked in both directions.
It found two more orphans nobody had connected to the vocabulary, both
already filed as resolver gaps rather than inventions: SetYanked
(improvise-lut7) and SetSearchMode (improvise-fo0w item 1).
Proved discriminating twice: removing SetTileCatIdx's three production
composers failed the first test naming it, and listing the composed
SetBuffer in NOT_COMPOSED failed the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
Conflict was context/test-evidence/command.md only — the third evidence-file
conflict in this batch, since every agent appends at EOF. Both sides kept and
verified line-by-line against both parents.
1727 passed / 0 failed over 38 binaries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
`Repr::Nonfinite(&'a str)` could only be produced by a deserializer that
borrows from its input, so the new wire spelling round-tripped through
`from_str` and `from_slice` and through nothing else. Measured on the
pre-fix head:
from_str {"Number":"inf"} -> Ok
from_str {"Number":"inf"} -> Err (the SAME JSON document)
to_value then from_value -> Err (does not round-trip its own output)
from_reader -> Err
The escaped form is the serious half: escaping is not a distinction JSON
makes, so the wire accepted a subset of the encodings of the spelling it
claims to share with the file. `Cow<'a, str>` fixes all four; the serialized
form is unchanged.
The PR's own tests could not see this — both use `from_str` exclusively,
which is precisely the one path that borrows. A check adjacent to the
question it names, inside the change that defines the question.
Also tightened `a_string_that_is_not_one_of_the_three_tokens_is_refused`: its
second disjunct accepted `data did not match any variant`, which is exactly
the message the above bug produces, so it passed in both worlds.
Conflict was `context/test-evidence/core.md` only, against #143. Both entries
kept and verified line-by-line — neither test cites the file, so a dropped
entry would pass `--check-evidence`.
Gates on the merge: 1728 passed / 0 failed / 7 ignored over 38 binaries,
clippy -D warnings clean, fmt clean, and `cargo check --target
wasm32-unknown-unknown -p improvise-core` clean — the one gate the review
asked for and could not run.
Found by adversarial review (confirmed by injection, not reasoned).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
Conflict was `context/test-evidence/gp.md` only — this PR and #146
(improvise-ipqu) appended at the same point. Both entries kept, verified
line-by-line against both parents, because --check-evidence cannot see a
dropped entry: it checks that every heading names a running test, and neither
of this PR's tests cites the file, so losing one would pass the gate.
The format string was broken across source lines with no `\` continuation, so
eighteen spaces were baked into what a user reads on --predict against a
corrupt model. Found by adversarial review, which printed the message rather
than reading the source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH