Commit Graph

70 Commits

Author SHA1 Message Date
Edward Langley 9872cc936f Every cell-editor opener awaits its seed while a write is unanswered (improvise-p6v6)
mdfc put the "any unanswered write" check inside `advanced` alone, so an
editor opened with i/a (`edit_or_drill`) still seeded from the cache the
write had made stale: `i 9 Tab Esc i 5 Enter`, typed faster than the
answer, stored 15 in the browser where the TUI stores 195.

The in-flight half now lives in `opened_on`, so no opener can forget it.
`advanced` keeps the "this frame writes" half, since the commit being
resolved is not in flight yet; dropping it fails the 3tt4/3uzg tests.

Stacked on #227 (fix/fy9o-reopened-editor-reseeded, de4a7d78).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:33:33 -07:00
Edward Langley de4a7d7862 protocol.md §3: name the type-ahead divergence that remains after mdfc (improvise-n4i9)
Measured in Chromium over the worker: with keys pressed back to back,
`i 9 Tab Tab 5 Enter` stores 5 or 5095 depending on whether `5` beats the
answer; with a pause, 5095 in 5 of 5 runs. Never the stale 505.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:07:53 -07:00
Edward Langley 3fcd407b08 A reopened editor awaits any unanswered write, settles only while editing, and states its awaiting flag (improvise-mdfc, improvise-4cb5, improvise-mgis)
improvise-mdfc: an untouched commit seeded the reopened editor from the
cache even while an earlier write from this client was unanswered, so
Tab Tab at the rightmost column stored 505 where the TUI stores 5095.
`advanced` now awaits when this frame writes or any in-flight frame is
awaited. The lockstep test sends three Tabs before answering any.

improvise-4cb5: `settle_awaited_seed` also requires an editing mode, so
an Esc before the answer no longer leaves the seed in Normal's buffer.

improvise-mgis: `OpenedEditor::awaiting` loses `serde(default)`; a frame
missing it fails to parse as the missing field (protocol.md §3, 1wl0).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 16:59:23 -07:00
Edward Langley baabe1a9b1 Evidence and the argument for seeding a reopened editor from the host's answer (improvise-3uzg, improvise-xbgm)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 10:59:41 -07:00
Edward Langley 1d2032ef82 Merge remote-tracking branch 'gh/main' into fix/fy9o-reopened-editor-reseeded
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
#	crates/improvise-wasm-client/tests/lockstep.rs
2026-09-23 10:54:59 -07:00
Ed Langley 0ff17c0f0e Merge pull request #240 from fiddlerwoaroof/fix/ugj4-jibp-mouse-editor
Mouse editor: seed from the target, report the first edit (improvise-ugj4, improvise-jibp)
2026-09-23 10:49:45 -07:00
Edward Langley 35013b6bda Evidence, docs and the bundle surface for the mouse editor's seed and first edit (improvise-ugj4, improvise-jibp)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 08:52:26 -07:00
Edward Langley 37a64445e0 Merge commit 'c6de77dbde8d05ba287cb69d92c230089098916f' into wave/km71-edit-formula
# Conflicts:
#	context/code/command.md
2026-09-23 08:42:50 -07:00
Ed Langley aa96cd6404 Merge pull request #224 from fiddlerwoaroof/wave/tt22-no-legacy-json
.improv is the only native format: remove the legacy JSON document loader (improvise-tt22)
2026-09-23 08:41:29 -07:00
Edward Langley e5b18d3499 Merge remote-tracking branch 'gh/main' into wave/km71-edit-formula
# Conflicts:
#	context/test-evidence/command.md
#	crates/improvise-protocol/src/reduce.rs
2026-09-23 08:32:01 -07:00
Edward Langley 8ee5fade27 fy9o: corner lockstep guard, evidence and protocol.md; file improvise-jmti
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 07:57:01 -07:00
Edward Langley 7fb6960cf8 WIP: drop the serde derives only the legacy loader used; docs (improvise-tt22) 2026-09-23 07:50:26 -07:00
Edward Langley 77f059c852 Merge remote-tracking branch 'gh/main' into wave/km71-edit-formula
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
2026-09-22 23:43:39 -07:00
Edward Langley a91db1d857 improvise-km71: edit the formula under the cursor, in place
Enter or e in the formula panel opens FormulaEdit on the formula under the
cursor, seeded with its text. The editor names the formula by a FormulaId
captured when it opens, and commits through a new ReplaceFormula effect and
ModelCommand. The edit keeps the formula's position, renames it in place when
the target changes, and refuses out loud if the formula was removed or
rewritten meanwhile, if the new target belongs to another formula, or if the
text does not parse. A refusal leaves the editor open over the user's text.

FormulaCache now carries each formula's id, so the browser composes the same
edit, and a formula-row click opens the editor on the clicked row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:42:33 -07:00
Edward Langley 6f04819d40 Mouse evidence rewritten for EditTarget.opened; file improvise-ugj4
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:41:32 -07:00
Edward Langley 1dbf6de936 Evidence and context for improvise-2054 and improvise-wv3h; file improvise-jibp
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:32:56 -07:00
Edward Langley 7407abbca6 Merge remote-tracking branch 'gh/main' into fix/ixxx-untouched-commit
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
#	crates/improvise-command/src/command/cmd/commit.rs
2026-09-22 23:11:06 -07:00
Edward Langley 6b3b9f03de Merge remote-tracking branch 'gh/main' into fix/6yvg-dz8c-json-float
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
2026-09-22 22:57:58 -07:00
Edward Langley 7628770d79 Merge remote-tracking branch 'gh/main' into fix/ixxx-untouched-commit
# Conflicts:
#	context/test-evidence/command.md
2026-09-22 22:31:19 -07:00
Edward Langley 95bd593dbe Merge remote-tracking branch 'gh/main' into fix/help-keys-checks
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/protocol.md
2026-09-22 22:30:19 -07:00
Ed Langley d1c4884399 Merge pull request #196 from fiddlerwoaroof/fix/projection-identities
improvise-exdg/7xyx: a coordinate cell is CellId::Coordinate; FrameGroup::cat deleted
2026-09-22 22:28:36 -07:00
Edward Langley 4c45166b8a WIP: the mouse editor hands wasm its captured seed (improvise-ixxx)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:24:17 -07:00
Edward Langley feefe17d53 Merge remote-tracking branch 'gh/main' into fix/6yvg-dz8c-json-float
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/tooling.md
2026-09-22 22:19:30 -07:00
Edward Langley 95a35e8cce Every JSON parser reads numbers exactly, and the check derives its crates (improvise-6yvg, improvise-dz8c)
improvise-6yvg reproduced. A JSON import in improvise-command and a legacy
JSON .improv load in improvise-io stored 0.24815714285714285 as
0.24815714285714283 in each crate's own test build. The root binary's
`improvise import` used the same lossy parser. Now float_roundtrip is on every
normal serde_json dependency in the workspace: root, improvise-command,
improvise-io, and the optional test-support dependency of improvise-protocol.

improvise-dz8c: the hand-listed cargo-tree check over three frame-parsing
crates is replaced by tests/json_numbers.rs. It reads
`cargo metadata --no-deps` and names every package, including target-specific
entries, whose normal serde_json dependency lacks the feature. The rule and why
it beats "parses a frame", the build graph, and enabling the feature once in
the protocol crate are in context/code/protocol.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:19:05 -07:00
Ed Langley 608ba6a6c8 Merge pull request #195 from fiddlerwoaroof/wire2/59q8
Source scanner: bound test-only items by brackets, evaluate cfg predicates (59q8)
2026-09-22 22:15:24 -07:00
Ed Langley 1a6b877482 Merge pull request #194 from fiddlerwoaroof/fix/umi4-edit-seed
improvise-umi4: an editor opens on the value, not on its rounded Display
2026-09-22 22:06:30 -07:00
Edward Langley 6b3dd8ca3e improvise-exdg: correct the paste claim against #184, and name improvise-kkcy
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:58:43 -07:00
Edward Langley 25a06d52ff improvise-7xyx: delete FrameGroup::cat and the echo it promised
The field's doc comment said a client echoes it back in ToggleGroup, but
resolve.rs has no toggle-group arm and neither renderer read it. Deleting
it is the minimal honest answer: composing `z` in a browser needs its own
design (the TUI places the cursor on the layout the toggle will produce),
and the operand should come back with that consumer. The h0iv test now
asserts whole-header equality, which is stronger than the (cat, name)
pair it compared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:56:53 -07:00
Edward Langley 43ba5e6151 Bound test-only items by brackets and evaluate cfg predicates in the source scanner (improvise-59q8)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:55:32 -07:00
Edward Langley bb1a05674c improvise-exdg: a records coordinate cell projects CellId::Coordinate
A drawn coordinate cell projected id: None, which ADR 002 and gotcha 42
define as "no projected cell". Absence was encoding a permission (the
host's). It is now its own kind, carrying no operand because no client
composes a coordinate rename. render.rs::editable is an exhaustive match
so the next variant must be classified. Paste, clear and edit are
unchanged: every write path still matches Record alone. The test that
pinned the bug (a_coordinate_column_is_projected_with_no_identity) is
replaced by one that enumerates the whole records grid.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:54:11 -07:00
Edward Langley 141e41ec4b improvise-umi4: an editor opens on the value, not on its rounded Display
CellValue's Display writes a non-whole number {:.4}, and every editor seed
was that string, so i then Enter on an untouched 1.23456789 stored 1.2346,
in the TUI (pivot and records) and in the browser, which seeds from the
projected edit_text. CellValue::edit_text is now the one home of the seed:
the shortest decimal that reads back as the same f64 (non-finite values
take the core.md §16 token). display_value_in's two arms and
cell_display's records arm all go through it; the now-orphaned
GridLayout::resolve_display is removed.

Text that reads as a number (007) is exact in the seed but still
reclassified by typed(); filed as improvise-ixxx.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:49:20 -07:00
Edward Langley 544744cfd2 context: the help rows' premise is a type
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:43:51 -07:00
Edward Langley 3c6001ac7b Merge remote-tracking branch 'gh/main' into fix/6oy4-float-roundtrip
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/protocol.md
2026-09-22 21:41:49 -07:00
Edward Langley c5e72e889f Merge remote-tracking branch 'gh/main' into fix/help-keys-checks
# Conflicts:
#	context/test-evidence/protocol.md
2026-09-22 21:37:17 -07:00
Edward Langley da8c67109a CellDisplay equality is the wire's, so a NaN cell quiesces (improvise-4w09); docs and evidence for both 2026-09-22 21:37:12 -07:00
Ed Langley f95dc25d64 Merge pull request #172 from fiddlerwoaroof/wire2/lane
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Vocabulary completeness guard, position-free ModelCommand, one source scanner (h3y7, hkbc, c7mu)
2026-09-22 21:27:50 -07:00
Edward Langley d9d064c882 Forward guard reads an effect name past attributes and doc comments (improvise-94x2); clippy slice fix
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:20:07 -07:00
Edward Langley bdb01b8763 context: the prefix rows are derived; the pages expose what they ask for
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:15:41 -07:00
Edward Langley c88b4b5d69 docs: CellDisplay::value and the dsxq evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:08:20 -07:00
Edward Langley 77c2471bcc WIP: docs and evidence for h3y7 forward guard, hkbc and c7mu
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:56:44 -07:00
Edward Langley 6b5ff2dd0e WIP: forward vocabulary guard (h3y7) and position-free ModelCommand guard (hkbc)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:46:28 -07:00
Edward Langley 21184bbf64 Delete the invented Command::ClearBuffer, and guard the class
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
2026-09-22 19:59:41 -07:00
Edward Langley 253b84b2da fix(protocol): a refused frame is put back by the fold that made it (improvise-nxah)
`Client::key` folds a locally-resolved batch before sending it, so the page
redraws on the keystroke. A refusal left those folds in place: measured against
a real `Host`, a refused commit left the client one row down with its edit
buffer cleared while the host's mirror of that same client was still at the
origin holding the user's typing.

What rolls back is `ViewState` — the thing the fold produces — never a cache. A
`Command::Model` is a no-op in `reduce_view`, so the optimistic fold never
touched the caches, and the bug report's own evidence (`page_coords`) cannot
diverge this way: it moves only under `Projection::Grid`. What diverges is the
mode, cursor, status and buffers that ride along with the model command.

The rollback needed a positive acknowledgement first, which the protocol did not
have: with only `Rejected` on the wire, a client holding two frames in flight
cannot tell which one an answer addresses. `ServerFrame::Accepted` closes that —
emitted once per accepted mutating frame, last, after everything the frame
produced — the same shape as the basis-publication gap ADR 001 originally left.

Two existing facts make the rule usable rather than a convention: only
`Command::Model` can be refused, so a frame carrying none is answered by neither
and needs no entry; and `Rejection::refused_an_edit` already separates the two
`Save` refusals, so a failed write cannot consume the answer owed to an edit.
A refusal drops one frame and replays the rest, because a single saved snapshot
is unsound under pipelining — it would discard a later frame the host applied.

This does not do improvise-3al1's force, and it does not contradict it: the
2026-09-21 wave recorded the two tickets as contradictory on the ground that a
rollback would wipe the staged typing. It does the opposite — the typing frames
carry no model command, are never refused, and have already settled, so rolling
back the commit restores the buffer to the typing. Asserted, not inferred.

Reasoning: context/code/clients.md, context/code/protocol.md §4.
Evidence: context/test-evidence/clients.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 15:50:29 -07:00
Edward Langley a913390322 feat: the page bar becomes a control, and [/] reach the modes about the document
Three tickets on one subject: paging.

improvise-rw6l — clicking a page chip opens a menu of that axis's items, with
a down-arrow saying so. Almost nothing had to be derived: `PageAxis::items` was
already projected and `ModelCommand::SetPageSelection` was already on the wire.
The one gap was that `RenderPageAxis` dropped the names, keeping the position
and count improvise-p1ym needed for `[`/`]`. `items` is relayed INSIDE `cycle`
rather than beside it, so there is one predicate and not two: a Filter axis has
no cycle, therefore no arrow, no menu, no items, and composes nothing if a click
reaches it — all four off `PageAxis::cycle`, the one home of gotcha 27's split.

improvise-771e — the browser tile bar said "Quarter Pag". `short_label` is three
characters so the TUI aligns in a fixed-width terminal (gotcha 26); a browser has
no column to fit. `render.rs` asks `plain_label` instead; `short_label` is
untouched. Note what pinned the truncation was `harness/tests/drill.spec.js`,
not `tests/grid_frame_agreement.rs`, which does not mention the tile bar.

improvise-wiew — `[`/`]` move from Normal onto a new `document_keymap` between
the global and the modes whose subject IS the document (Normal, the three
panels, TileSelect; RecordsNormal through Normal). Help and the import wizard
take the global directly. Per the user's ruling: an overlay has a subject of its
own, and these are the first shared keys that MUTATE the document — under the
help overlay that is an invisible change to the grid it is covering.

Boundary crossings to flag, both outside the brief's stated write scope and
neither a wire change:
  - `resolve.rs` gains `Click::PageItem` and `lib.rs` a `click_page_item`
    binding. Composing the command anywhere else would put a derivation in the
    page, which design-principles §5 forbids; `Command`/`ModelCommand` are
    untouched.
  - `improvise-command/src/command/keymap.rs` gains three tests. The two
    existing guards were the mechanism that forced the wiew argument, so they
    had to be answered where they live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:24:38 -07:00
Edward Langley 6b28e58352 Give the browser a key that reaches the view history (improvise-cfly)
PR #115 put `Command::ViewBack`/`ViewForward` in the view half, made
`reduce_view` fold all three view effects, and gave every client its own
`view_back_stack`. `resolve.rs` was owned by another agent that wave, so
`declined()` still refused `view-back`: the merged wave held the capability
and no key, and `<` in a browser reported a feature that was fully supported.

Delete the decline together with its comment, whose stated reason ("it
commits a drill first, so composing it would commit another client's typing")
was false by the time it shipped. `ApplyAndClearDrill` is off the wire, so a
resolver cannot compose it; and no browser path stages a drill at all —
`drill-into-cell` is declined and `edit_or_drill`'s `Aggregate` arm answers
out loud. The divergence from the `Cmd` is confined to a state this client
cannot enter.

The asymmetry that IS real is a different one: `reduce_view`'s `stepped` is
silent on an empty stack where `ViewNavigate` emits "No previous view" /
"No forward view". `step_history` mirrors both arms, so a client that has
nowhere to go hears the same sentence the TUI does.

Found on the way and not predicted by the ticket: `view-forward` is bound in
NO keymap, in either front end. The arm is added and tested by name, and the
test asserts the absence of a binding (with `view-back`'s presence as the
control) so that adding one has to come there and say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 21:46:56 -07:00
Edward Langley fa1f572335 Merge remote-tracking branch 'gh/main' into wave/t-drill-wire
# Conflicts:
#	context/test-evidence/command.md
2026-09-21 21:28:00 -07:00
Edward Langley 507447c48a docs: the arguments the two commits' doc comments point at
Every `context/code/...` pointer added by this branch now names prose that
exists. protocol.md §2 carries why `ViewBack`/`ViewForward` CLOSE a gap in the
view half's derivation rather than widening it, why the fold and the effect had
diverged, and why a guard was written and then deleted. command.md carries why
`forget_vanished_views` and `adopt_deleted_views` do not subsume each other.

A pointer to prose that does not exist reads as a promise the explanation does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 19:30:42 -07:00
Edward Langley 11320f771a feat(protocol): one global keymap every mode inherits from (improvise-636d)
`?` resolved in 2 of 17 modes: bound once on Normal and reached by
RecordsNormal through the single inheritance edge. It was missing from all
three panels, TileSelect, SearchMode, ImportWizard and Help itself.
Meanwhile `bind_panel_toggles` had two call sites and `:` was written into
two spines while `?` was in one — nothing kept the spines in agreement.

`global_keymap()` binds `?`, `:` and the three panel toggles, and all
seventeen modes are built `Keymap::with_parent(global)`. The answer to "which
modes must not inherit" is **none**: `lookup_local` is exhausted before the
parent, so the eight text-entry modes and SearchMode shadow every global key
with their own `AnyChar`, and ImportWizard with `Any`. Only Help writes an
override, and it is a decision — `?` closes the overlay.

The global binds Char keys only, because `AnyChar` is what does the
shadowing; F1 therefore stays on Normal although it runs the same command.
`TextEntry::with_parent` is folded into `new`, which now takes the parent
every text-entry mode has.

Four guards: the chain ends at one global (Arc::ptr_eq, not by value), the
global is Char-only and prefix-free, no mode binds a toggle of its own, and
every global key resolves in every mode and types where it must. Each was
proved by injecting its fault; evidence in context/test-evidence/protocol.md
records the two cases where a neighbouring test stayed green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 17:48:36 -07:00
Edward Langley 81fe546594 Settle the mode by observing the grid, not at each composition site
ChatGPT Codex's P1 on #98: switch_view_at composes
[SwitchView(target), ChangeMode(Normal)], and changed_mode settled that
against the grid the client STILL HOLDS -- the outgoing view's. Records ->
pivot emitted RecordsNormal and pivot -> records emitted Normal, which is
the cross-view drift this PR exists to close, by a second door.

The hand-back predicted this as follow-up 3 and called it unreachable
because R is a stranding decline in the browser. That is right about the
TOGGLE route and wrong about this one: records-ness is auto-detected from
the axes (gotcha 1), so a view switch changes it with nothing in the batch
saying "records". One door was enumerated and there were two.

So the remedy is general rather than a patch to switch_view_at.
changed_mode is DELETED; every site emits the plain AppMode::Normal; and
ClientState::fold settles after every fold against the grid it HOLDS.
Route-independent, so no enumeration of composition sites is load-bearing
any more -- the same shape the repo already prefers in cursor_epoch over
matching on CycleAxis, document_generation over matching on LoadModel, and
the dirty flag over a table of command variants.

improvise_protocol::settled_mode is the one rule and all three ends call
it: ClientState::fold, ClientSession::fold (the server's mirror, so the two
cannot diverge in a field nobody currently reads today) and
App::settle_normal_mode. App::normal_mode is gone -- it was that
function's host-side duplicate, and my change orphaned it.

Bidirectional coverage drives a real Host: a session connected to a
records-mode host, the pair folded locally and sent as a
ClientFrame::Commands, every reply delivered in arrival order. The
composed ChangeMode carries the STALE answer deliberately, because
composing the friendly value would let the records -> pivot direction pass
under the fault. Both directions fail with settle_mode no-opped, each
proved separately since the loop stops at the first.

Evidence, the injections and why the first fix's unreachability argument
was too narrow: context/test-evidence/clients.md, context/code/protocol.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:44:17 -07:00
Edward Langley 618801701e The browser asks the grid what kind of grid it is (improvise-e5pf)
One substitution at three decision points: wherever the host asks
layout.is_records_mode(), resolve.rs asked state.view.mode.is_records().
The two agree only while the mode and the layout agree, and
Command::ChangeMode(AppMode::Normal) is the message that separates them --
the host settles Normal against the layout the batch ends on, reduce_view
folds the variant it was handed.

GridCache::records carries the fact, projected by grid_of off
layout.is_records_mode() and diffed like the extent. Deriving it on the
client from row_frame.cats would have been a second implementation of
is_records_mode (improvise-edp's disease); deriving it from the mode is
the bug. Like Extent it frames nothing, so apply does not re-fit and its
batch position is not load-bearing.

The client's settling has one home, resolve::changed_mode, reached by
every site that can emit AppMode::Normal -- including entered(), which is
how a bound `enter-mode normal` gets there. The three decision points ask
state.grid.records, so a commit at a records cell declines rather than
composing typed_cell_command against a grid whose columns are
[coords.., +cat, measures.., +].

lockstep now DRIVES the keys: in_lockstep presses on both ends and
refreshes the client from a real projection rather than from the host's
slice, which is what let the old records coverage start from agreement.
The reachable path is Tab (category panel) then Esc; c/f/v are Ignored in
RecordsNormal and R is a stranding decline, which a first draft of the
test found the hard way.

Two resolve.rs fixtures put a records mode over a non-records grid -- a
state no host can produce -- and had to be corrected with the fix.

Evidence and both injected faults: context/test-evidence/clients.md.

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