d1ce2ecbc319edfeda4a4bac2932e8a3754d9f1a
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d1ce2ecbc3 |
fix(web): an advertised key is never silent, a formula is visible, and +cat commits
improvise-rmvx, improvise-fdt6, improvise-gcvb — three faces of the same
thing: a browser offering something and then doing nothing.
rmvx. `the_command_names_that_still_need_the_host_are_these` was built with
`in_mode(AppMode::Normal)`, so it only ever asked about Normal-mode bindings:
`add-record-row` (RecordsNormal) and `delete-formula-at-cursor` (FormulaPanel)
were outside its reach entirely and could never have failed it. A named list
that silently scopes itself to one mode is a guard that looks broader than it
is. `every_advertised_command_answers_in_its_own_mode` crosses
`help::advertised` — command NAMES, so no hint string is tokenised — against
`resolve` in every mode, over a fixture furnished enough that an `Upstream` can
only mean an unhandled name. It found a seventh instance the ticket had not
predicted: `view-back` is advertised in RecordsNormal.
Six names now decline out loud, each with its own true subject, out of one
`declined` table; `stranding` returns the subject rather than a bool, so `:`
says the command line is unavailable instead of doing nothing. A declining step
takes its whole `Binding::Sequence` with it, which is what keeps `o` ("add a
record row, then edit it") from opening an editor on a row nobody created.
fdt6. `commit-buffer formula` already resolved; what did not exist was any way
to SEE the typing. `RenderModel::editing` is scoped to the two cell-editing
modes and reads the "edit" buffer by name, so FormulaEdit, CategoryAdd, ItemAdd
and the three prompts opened and swallowed every character. `RenderModel::
minibuffer` is `AppMode::minibuffer()` plus the buffer that config names, read
through `improvise_protocol::read_buffer` so gotcha 11 keeps its one home, and
`web/app.js` paints it.
gcvb. The records-mode early return was broader than its reason. A
`CellId::Create` commits the typed NAME as one existing `ModelCommand`, reads
no `record_col` and writes no cell, so the return narrows rather than
disappears. `+cat` must NOT advance: an inserted column renumbers every index
to its right and the advance was computed from the pre-commit layout, so a
client that advanced anyway would land one column off, silently. Both halves
proved by injection against `CommitAndAdvance` in `tests/lockstep.rs`.
Every new guard was proved by injecting the fault it names. The one worth
repeating: `web/tests/render_surface.test.js` went green over a page that
painted nothing, because the string `model.minibuffer` was inside a function
nothing called. The static check is a completeness check, never an execution
check — `harness/tests/keys.spec.js` caught it in Chromium on the first run.
1394 → 1397 Rust tests; harness 17 → 19 passed, 3 skipped; node 26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
850ddcf0eb |
Merge gh/main (#48 active_view, #50 shared front end) into wave/b-projection-gaps
Eight conflicts, all resolved keeping both sides. The three in context/ were append-vs-append. The five in code were the restructure #50 declared in advance: - render.rs: render_model became a free fn over &ClientState and the mode label moved to improvise_protocol::mode_label. Took that shape and re-added the three doc_* fields. - state.rs -> improvise-protocol/src/client.rs: only the import path conflicted. The file now lives IN improvise-protocol, so `crate::` is right, plus DocumentCache. - keys.rs went 1387 -> 187 lines because resolve/step moved to improvise-protocol/src/resolve.rs. Took gh/main's slim file and ported every addition into resolve.rs: the Derived and Create arms of edit_or_drill, the or-patterns in record_at_cursor and record_cell_at_cursor, page_cycle with its Odometer, the two bindings, and `[`/`]` leaving the still-needs-the-host list. - project.rs: kept the value-based numeric/error from improvise-tom1 with gh/main's cell_id(app, layout, ..) signature; kept BOTH document_update and view_in; kept our title-bar projection inside their per-view projection set. - lockstep.rs: both sides appended tests and the hunks interleaved mid-function, so took gh/main's file and re-appended our page-turn block whole. THE CLEAN MERGE WAS THE DANGEROUS ONE, exactly as warned. `cargo check --workspace --all-targets` passed with three of our tests SILENTLY GONE — they lived in keys.rs's test module, which moved wholesale. i_on_a_derived_cell_names_ the_formula, the_page_keys_turn_the_odometer_as_the_command_does and a_filter_axis_is_projected_but_not_cycled are ported into resolve.rs, with client_on now returning a ClientState rather than a Client. One assertion changed meaning under us and is corrected: lockstep's page_selections read workbook.active_view(), which since improvise-jytm is the document's DEFAULT. SetPageSelection writes App::view(), the client's. They coincide in a one-view fixture, so it was passing by fixture luck; it now reads App::view() and still fails under the injection. All four injections re-run on the merged code and all four still discriminate: reversed odometer, dropped records formula_for lookup, title bar gated on Dirty, and filtering_cats leaking into the protocol. 1348 passing, clippy clean, fmt clean, all eight wasm crates clean, both comment-run checks clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fc4035dbca |
Merge remote-tracking branch 'gh/main' into wave/h-shared-frontend
# Conflicts: # crates/improvise-command/src/ui/reduce.rs |
||
|
|
f66c4aff04 |
fix(protocol): records_named has one home, and the lockstep sees every cursor
Findings 1 and 2 of the adversarial review of PR #50, both demonstrated by injection before being fixed. **F1.** `resolve.rs`'s `structural`/`record_write` were a second derivation of `reduce.rs::records_named`, and nothing tested them: `lockstep.rs`'s `apply_on` restates the basis against its twin with `caught_up`, which re-derives it with the SERVER's answer and throws the client's claim away before anything is compared. Emptying `record_write`'s claim left the workspace green. Fixed by collapsing rather than testing, which is what this unit argues for everywhere else. `records_named` is pure and `RecordHash` is already re-exported by the protocol, so it moved DOWN into `improvise-protocol` beside the enum it reads; `improvise-command` re-exports it; `resolve.rs` has one `model()` that claims `records_named(&cmd)`. The claim is now right by construction and the guard is the exhaustive match that was always there. Verified by emptying the shared function's `SetCell`/`ClearCell` arm: five server-side tests fail, where the client's copy had none. What the old shape cost, which was not a compile error: `structural` accepted any variant and claimed `[]`, so a new record-writing command compiled clean on the client with the wrong answer and the symptom was a dead browser key, refused as `RecordUnstated`, with nothing failing. **F2.** Three resolver divergences the suite could not see, all now failing: `delete_view_at_cursor`'s cursor step-back (its only decision — the test pressed `n` and `Enter` while its doc comment said "create, switch and delete"), and both Formula arms of `CommitKind`, Formula being the only kind whose `on_commit()` is `Some`. `both_ends` now compares every cursor, not only the document, mode and status. A fourth injection — `cycle_panel_focus`'s `start` index — still survived, because `PanelKeymap` binds Tab to `enter-mode normal` inside a panel so no key reaches those arms; they are now driven directly by `panel_focus_cycles_from_the_focused_panel_not_the_first`. Widening the commit test found a real divergence, pinned rather than fixed as improvise-cl7y: the client folds `CommitBuffer`'s optimistic "Formula added" and cannot parse, so a browser reports a formula it did not add. `a_formula_the_client_cannot_parse_diverges_on_the_status_line` asserts both sides, so a fix has to come there and change it. F3 filed as improvise-thpe and deliberately not fixed here. `apply_on`'s doc comment pointed at an evidence entry that did not exist and now names `context/code/clients.md`, which has the argument. 1324 tests pass, up from 1322. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
414d02c4a6 |
feat(protocol): one home for UI strings (improvise-x7sw, ktv1, k099)
Three tickets asked one question — where do UI strings live relative to a
serde wire enum — and answering it three times is the disease this unit exists
to cure. One answer, in `improvise-protocol/src/help.rs`:
**A UI string is keyed by a command name, never by a key, and never hangs off
the enum that crosses the wire.** `AppMode::label`/`hint_text` are gone;
`help::mode_label`/`help::hint_line` replace them, so the enum a client
deserializes carries no English at all (improvise-k099). **And wherever a
string names a key, the key is looked up rather than spelled**:
`Hint { keys, command, caption }` takes `keys` off `Keymaps::default_keymaps()`,
`command` is the semantic value a localized front end keys off, and `caption`
is the default English — the `AxisColor`/`axis_fg` shape.
- **improvise-x7sw**: `help::which_key` is the which-key popup's rows, keys and
captions together. The client already holds the keymaps, which is the answer
the ticket preferred, so `ViewState::transient_prefix` stays an address.
- **improvise-ktv1**: the help pages are data — `help::pages()` — with every
key row derived. `src/ui/help.rs` keeps its ratatui styles and loses ~310
lines of content; `web/app.js` paints rows it does not author, and
`harness/tests/shell.spec.js` asserts that `?` really draws them.
**What this deletes is the measure of it.**
`every_hint_key_resolves_in_its_mode` and its 60-line hint tokenizer are gone:
they existed to hold a hand-written string honest against the keymap, and the
string is derived now, so the property is true by construction
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
`Keymap::binding_hints` goes with them — `which_key` is the one walker, and the
binding dump reads it. What replaced the test checks the *other* direction, a
mode advertising a command nothing binds, which was never checkable before.
Two defects the derivation exposed, both fixed here: `key_label` dropped
modifiers, so Ctrl-arrows read as bare arrows, and `Char(' ')` printed a literal
space instead of `Space`.
**What is deliberately not derived**, and says so: the import wizard's keys
(its whole keymap is one `Any` binding into `HandleWizardKey`, which decides
per step inside the command — a real hole, left visible), `gg`/`yy`/`ZZ` and
the ex-commands (`HelpLine::Literal`), and the captions, which no test can
judge. `context/code/protocol.md` §8 has the argument.
Cost, stated: the hints changed shape. They are longer, they name arrow keys
beside `hjkl`, and two sub-lists on the Panels page lost their visual indent.
That is the price of the keys being true.
Evidence: 1321 tests pass, and the 7 Chromium shell tests pass against the
assembled site. Two faults were injected into the help overlay — dropping the
`paintHelp` call, and removing the `mode != Help` guard so it is never hidden —
and each fails the new spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8b3de934fc |
feat(protocol): project the page-axis item lists so [ and ] work in a browser
improvise-p1ym. GridCache::page_coords carried (category, selected item), so PageNext/PagePrev resolved to a command name the browser held but could not compose an argument for. They reached Host::handle, which answers an empty vec, so a smoke test found them inert on the live page. The item list is not a second field beside the selection: page_coords becomes Vec<PageAxis>, and `items` is Some exactly when the axis is Axis::Page. That puts gotcha 27's split in the type — a Filter axis constrains without being navigable, so it carries no list and no caller can compose a cycle for one. GridLayout::page_coords stays filtering_cats(), which is right for the bar, and navigation.rs::page_cat_data stops spelling the Page-vs-Filter rule a second time: it reads the layout's list. The browser's page_cycle mirrors PageNext/PagePrev line for line, odometer carry included, and `[`/`]` leave the still-needs-the-host list. Two decisions stated in context/code/core.md. The list is effective_item_names — the same call that already chose the default selection, so selection and list come from one place; page_cat_data used ordered_item_names before, which for a _Measure page axis let the default selection be a formula target `[`/`]` could not reach. And the list is NOT filtered to visible items: the TUI cycles hidden ones, and making the browser differ would be the divergence rather than the fix. The lockstep test's first version was vacuous — it rebuilt the client's cache from the host and compared it against that same host. It now drives two independent documents and compares View::page_selection; the injection that reverses the odometer's significance now fails it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
555d9f1ca7 |
feat(protocol): fifteen more bindings resolve on the client (improvise-e24v)
`cycle-panel-focus`; the category panel's `cycle-axis-at-cursor`, `open-item-add-at-cursor`, `filter-to-item`, `toggle-cat-expand` and `delete-category-at-cursor`; the view panel's `switch-view-at-cursor`, `create-and-switch-view` and `delete-view-at-cursor`; tile select's `set-axis-for-tile` and `cycle-axis-for-tile`; the help overlay's `help-page-next`/`help-page-prev`; `commit-buffer` in all three kinds; and the two cell writes ADR 002 lets a thin client name, `clear-cell` and `paste`. Each mirrors its `Cmd` line for line, status sentence included — a client that emitted the mutation without the sentence beside it would diverge on the status line and nowhere else. Two orderings are copied rather than rationalised: `CommitBuffer` writes its optimistic status first (improvise-6t3) and `TileAxisOp` writes its axis first. **A divergence found and fixed on the way.** `effect::HelpPageNext` clamps at the last page and `HelpPagePrev` saturates at zero, while `reduce_view`'s `Command::HelpPage` arm *wrapped*. Two derivations of one fact, disagreeing, invisible because `h`/`l` in the help overlay had always been upstream keys and nothing had ever sent the command. `walking_off_the_end_of_the_help_pages_ agrees_with_the_host` was written first and failed — client 0, host 4 — and the wire was then changed to match the effect, since the effect is what ships in the TUI. Whether both should wrap instead is improvise-trd3. **Three names the ticket listed do need enrichment**, filed as improvise-4bwa: `toggle-prune-empty` words its status off `View::prune_empty`, which is in no cache; `delete-formula-at-cursor` needs `(target, target_category)` and `FormulaCache` carries only the raw text; and `view-back` emits `ApplyAndClearDrill`, which is deliberately off the wire (gotcha 38) and is blocked on ADR 001 layer 2 rather than on a projection. The lockstep harness is new and is the evidence: it applies what the client composed to a **second authoritative host** and compares the documents as `.improv` text (`Debug` prints the symbol table's `HashMap`s, so it differs per process — gotcha 18). Four injected faults — a wrong axis, a wrong status sentence, a constant view index, and `AddCategory` for `AddItem` — each fail a named test; the second one did *not* until the fixture was made to sit on a category header as well as an item row. `the_command_names_that_still_need_the_host_are_these` shrinks by exactly what closed. 1318 tests pass, up from 1311. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5b7b53466f |
feat(active-view): split the document's default from each client's view
improvise-jytm, improvise-6c6q, improvise-k69y. `Workbook::active_view` becomes the document's *default* — what a freshly connected client opens and what `.improv` writes as `Initial View` — and `ViewState::active_view` becomes authoritative per client. Two clients may sit on different views of one document: the model is shared, the UI is not. - `App::switch_to_view` writes only the client's slice; `App::view()`/ `view_mut()`/`view_name()` resolve it against the workbook. - `effect::SwitchView`/`ViewBack`/`ViewForward` are `ViewOnly`, which takes `ViewBack`/`ViewForward` off `ModelCommand` — their operand is one client's history stack (the `ApplyAndClearDrill` argument). - `grid_in`/`category_tree_in`/`tile_bar_in` project against the session's view; `ClientSession` observes its own move through `projected_view`. - `apply_switch` takes a `SessionId`, moves only the asker's mirror, and relays `Broadcast::ToAsker` only when the document refused. `adopt_active_view` and `Broadcast::ToOthers` are deleted: nothing produces them any more. - `draw_status` reads the client's copy (improvise-k69y). `the_two_active_view_names_never_diverge` is replaced, not deleted, by `the_documents_default_view_outlives_this_clients_switches` and `a_fresh_session_opens_the_documents_default_view`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8ed3df8ce3 |
docs(wasm-client): move the fold, render and lockstep arguments to context/
lib.rs's crate statement, socket.rs's frame/wording/composition arguments (including the "obligation stated but not discharged" note on `typed`), render.rs's four silent-if-wrong decisions and state.rs's two dispatch properties are now prose in context/code/clients.md. The test narratives from both integration tests — the visual-vs-data row confusion Copilot found on PR #23, the multi-command frame improvise-955n had to keep accepting, and the lockstep fixtures' deliberate shapes — are in context/test-evidence/clients.md. No behaviour change: comments only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
301a74821a |
feat(protocol): the grid's frame on the wire, as AxisFrame
`GridCache::row_labels`/`col_labels` were `Vec<String>` built by `GridLayout::row_label`, which slash-joins a multi-level tuple — the exact rendering `two_row_categories_produce_cross_product_labels` asserts must not appear. So the protocol carried a form of the labels the TUI itself considered wrong, and `improvise-wasm-client/src/render.rs` drew precisely that form: the browser showed `Food/Alice` where the TUI showed two header columns, with no groups and no collapse indicators (gotcha 44). Both fields become one `AxisFrame` per axis, carrying the five facts gotcha 44 enumerates: the categories (hence the header levels), the label *tuples* and the inline group headers, the group a position sits in, and each group's collapse state. `record_col` stays off — session-local, keys pending drill edits, correct to keep off. It is one type and not a `row_levels` beside `row_labels`, because two answers to "what are the labels" is the disease (improvise-edp). The rules that used to be written twice are methods on it and are now called from both front ends: `shows_level` (repeat suppression, improvise-jjx's fix), `group_of`, `body_from`, and `header_widths` — the row-header width rule, which existed twice already, inline in `render_grid` and again in `viewport::compute_row_header_width` (improvise-1125). `GridWidget` loses its `&View`: nothing in it reads the view any more. Evidence. `the_whole_frame_is_unchanged` — the exact-buffer characterization over a grid with row groups, column groups, a page axis, multi-level headers and a total row — passes byte for byte, so the TUI is unchanged. The new `tests/grid_frame_agreement.rs` drives the widget into a ratatui buffer and the browser client through a `Snapshot` of the *same* cache, and compares what each draws; re-introducing the slash-join on either side was injected and watched to fail before the test was kept. `tests/web_demo_model.rs::the_web_demo_has_one_category_per_visible_axis` is deleted: it guarded the gap this closes, and a consolidation that keeps its compatibility test is unfinished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e13635638c |
feat(browser): the browser reads the TUI's keymap, and i opens an editor
Follows the keymap move. `keys.rs`'s hand-written per-mode classifier is gone; `resolve` now looks the key up in `Keymaps::default_keymaps()` — the same table the terminal dispatches against — and what is left here is the narrow residue: turning a bound command NAME into a `Command`, for the subset a client holding no `CmdContext` can compose. That is a better seam than the one it replaces. The drift risk was "which key does what": 170 bindings, no direct test, invisible when wrong. It is now "which command names can I execute", which is a list — and `the_normal_mode_keys_that_still_need_the_host_are_these` becomes `the_command_names_that_still_need_the_host_are_these` to match, so a rebinding no longer invalidates it and a new binding for an already-executable command reaches the browser with no change here. Several gaps close by sharing rather than by code. Search-mode typing resolves locally for the first time (the table binds `append-char ["search"]`, and "search" is `ViewState::search_query` — gotcha 11's pair is mirrored and nothing else is). `Esc` resolves as the `Binding::Sequence` it is, all-or-nothing. A modified character in a text-entry mode now types, because the `AnyChar` fallback being reached for is the host's own — the old "unmodified only" rule was a hedge against a table this client no longer guesses at. And a key nothing binds is `Ignored` rather than forwarded into `Host::handle`'s empty vec. What is newly executable, and so what a user gets back: `edit-or-drill` (`i`/`a`), `commit-cell-edit`/`commit-and-advance-right` (Enter/Tab in `Editing`), `enter-mode` (`?`/F1, Esc), `enter-tile-select` (`T`), `toggle-panel-visibility` (Ctrl-F/C/V), `toggle-panel-and-focus` (F/C/V) and `transpose` (`t`) — four of which an independent browser smoke test had just found inert. `i` on an **aggregate** does not go upstream. Every upstream key is a black hole, and a keypress that visibly does nothing is the worst of the three answers, so it answers with a `SetStatus` naming drill. A view-half command is folded into the asker's own mirror and broadcast to nobody, so that message reaches this user and no one else. The write itself is not composed twice: the keyboard commit calls `socket::typed_cell_command`, the function click-to-edit already used, so Enter and a click cannot disagree about what `""` or `"42"` means. ## `CellDisplay::edit_text`, found by lockstep `tests/lockstep.rs` drove `i` through the host and the client over one grid and they disagreed by a comma: the host seeds its edit buffer from `App::display_value_at_cursor` (the stored `1200`) while the client had only `CellDisplay::text`, which has been through `format_f64` (`1,200`). Committing that stores the *string* `"1,200"` and the number is gone — display rounding fed back into the model, which design-principles §4 forbids outright. So the projection is enriched rather than the client taught to un-format: `CellDisplay::edit_text` is `display_value_of` on the wire, and it repairs **click**-to-edit too, which had been seeding its `<input>` from the drawn text all along. ## Making the edit visible `RenderModel::editing` carries what the user has typed so far, or `None` when no edit is open. The decision — is this an editing mode, and which buffer does it type into — is made in Rust; the page draws the string in the selected cell. Without it the mode indicator would read INSERT while every keystroke vanished into a buffer nobody painted. `web/app.js` and `web/style.css` change accordingly, and are **unrun**: there is no browser here (gotcha 48). `node --check` only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
b1f8b1563c |
feat(wasm-client): the client resolves its own cursors (improvise-bpq2)
The user's ruling is that keys become commands in the browser and the keymap is a client-only concept. What made that urgent is not latency: improvise-9x6's `a_key_resolved_on_the_host_moves_state_no_client_can_hear` shows nothing in the projection stream carries the host's own `ViewState`, so host-side resolution **diverges** — and today it does not even do that, because `Host::handle` answers `ClientFrame::Key` with an empty vec. An upstream key is a black hole, not a slow path. Every key moved here is a key that previously did nothing at all in a browser. Three cursors, walked out of `KeymapSet::default_keymaps()` binding by binding rather than command by command: - **Grid** (hjkl, arrows, 0/$/Home/End, G, PageUp/Down, Ctrl-d/u). Needed the projected `GridExtent` to clamp against, plus the window — which is the client's own fact, since it is what the client told the host in its `ClientFrame::Viewport`. `Client::set_viewport` records it; `Client::viewport` stays, works, and is documented as the one that quietly turns this off. - **Panel** (j/k/arrows in all three panels). Needed **nothing**: the ticket lists the category flattening as a gap because re-deriving it is how two ends drift, but improvise-edp already made `viewmodel::flatten_cat_tree` the one home both ends call. It is the shared derivation, not a second one. - **Tile** (h/l/arrows). Needed nothing. Each mirrors its `Cmd` line for line, including where they disagree about a move that goes nowhere — `Move` and `MoveTileCursor` emit anyway, `MovePanelCursor` does not. Unifying that would be a second derivation, which is the thing lockstep testing exists to rule out. The modifier rule is narrowed to what it was always about: `AnyChar` matches under any modifier, but it is bound **only in text-entry modes**, so `Ctrl-d` in Normal has an exact binding and mirroring it is a subset. The old blanket rule stays where it was earned. Evidence: five new lockstep tests drive the client and a real `reduce_key` over the same keypresses and compare `ViewState`, with the client's cache built by the server's own `grid_of`. Three faults injected — clamping against `row_labels.len()`, dropping the scroll offset, counting categories instead of tree rows — and all three fire. `nothing_in_a_non_text_mode_resolves_locally` became false and is replaced by a named list of the Normal-mode keys that still need the host, so closing one has to come here and say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
e77176364c |
refactor(protocol): one home for the client/server frame envelopes
Three envelope vocabularies for two deployments become one pair in
`improvise-protocol::frame`: `ClientFrame` / `ServerFrame`.
The question the ticket said to answer first was whether the WORKER and
the SOCKET want the same vocabulary. They do, and each of the three
differences is a date rather than a decision:
- `Viewport` is deployment-independent. `improvise-ws-server::wire`'s own
argument for it never mentions a socket: the extent is not in
`ViewState`, no effect produces it, so no derivation reaches it, and a
worker hosting the same `Sessions` owes `set_extent` the same answer.
It is the handshake for the same reason on both — a `Snapshot` seeds
the VISIBLE cells.
- `Key` is deployment-independent too. A binding names an intent resolved
against a `CmdContext` a thin client does not hold (gotcha 38); that
says nothing about what the bytes travel over. The socket lacked the
variant because key forwarding was not built on it (improvise-bpq2).
- `Commands(Vec<_>)` batches and a singleton `Command(_)` cannot. The
fan-out must arrive whole and in sequence because a `GridUpdate::Cells`
that overtakes its labels is DROPPED (gotcha 33) — a property of the
fold, not of the transport. An in-process `postMessage` stream can
interleave exactly as a socket can.
The worker pair was also incoherent by omission: written before ADR 001,
it can say `Command::Model { basis, .. }` but has no `Rejected`, so a
worker speaking it would let a client compose a claim it could never be
answered on.
The two mirror tests are deleted, which is the measure of success: with
one definition `improvise_ws_server::ClientFrame` and
`improvise_wasm_client`'s are the SAME TYPE, so a rename or an added
variant is a compile error on both sides by construction rather than a
round trip somebody has to remember to extend by hand. What they were
blind to (a variant added on the server and never mirrored) cannot be
expressed. `tests/browser_round_trip.rs` remains and is now the stronger
statement: the wasm client's serialized frame is handed to a real
`Host::handle`.
`a_frame_named_the_other_envelopes_way_does_not_parse` moves to
`frame.rs` as `the_retired_spellings_do_not_parse`, keeping its
injected-fault discipline: the retired `Bootstrap`/`Command` spellings
must not come back as aliases.
`Host::handle` gains an explicit `ClientFrame::Key => Vec::new()` arm.
Behaviour is unchanged — the frame used to fail to parse and be skipped
by `next_client_msg` — but the gap is now a line of code with
improvise-bpq2 on it instead of a variant that does not exist.
The wasm client's duplicated fold surface goes with the envelopes:
`on_message`/`handle_message`/`apply` were `on_frame`/`handle_frame`/
`fold_frame` without the `Rejected` arm. `web/app.js` calls neither.
1132 passing, 0 failed (1134 before: 5 tests deleted, 3 added).
wasm32-unknown-unknown clean for all six bundle crates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
|
||
|
|
30c4bb6629 |
feat(wasm-client): the browser-side fold and key classifier (improvise-gsw)
A new crate `improvise-wasm-client`: a `ViewState` plus one widget cache each, moved by folding the same `Command` stream the host folds, with a wasm-bindgen surface over JSON. The ticket's premise does not survive contact with the architecture (`doctrine(check_the_ticket_premise)`). It asks `on_key_event` to "resolve via the keymap and dispatch through reduce_view", but a binding names an *intent* resolved against a `CmdContext` — a `Model`, a `GridLayout`, an `App` — while a `Command` is the *resolved mutation* (gotcha 38, improvise-gxi). A thin client holds none of that, so keys travel upstream unresolved and `improvise-command` is a dev-dependency rather than a dependency: the keymap could not be used here even if the bundle could afford `App`, `Workbook`, persistence and the wizard. The one exception is minibuffer text entry, whose target buffer is `AppMode::minibuffer().buffer_key` — the same field the host's `TextEntry` builder reads (gotcha 11), so the local rule is derived from the same fact rather than copied. `tests/lockstep.rs` drives the authoritative host and the client over the same keypress in all seven text-entry modes and asserts the two `ViewState`s agree. Rejected for local resolution, with reasons in `keys.rs`: cursor movement (`GridCache` is viewport-indexed, so a client cannot clamp), panel cursors (the flattening depends on `expanded_cats`), anything modified (`AnyChar` matches under any modifier on the host), and CommandMode's Backspace (the one `TextEntry::rebind`, which leaves the mode). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |