Commit Graph

12 Commits

Author SHA1 Message Date
Edward Langley 36ade35240 test(web): a Node suite between the boundaries, Playwright only on them
E2E and integration tests are rare and pin boundary contracts; unit tests
assume those contracts and test between them (user, 2026-09-20). The browser
half has four boundaries — RenderModel→DOM, JS→wasm, page→peer, worker→OPFS —
and everything between them needed no browser at all.

`web/shell.js` and `web/serial.js` are the seams: pure modules that RETURN A
DESCRIPTION rather than take a DOM double. `gridPlan(model)` answers every
question `gridTable` used to answer while creating elements — the data-row
index across group headers, what a group header spans, a null group label
against an empty one, an empty edit buffer against no editor — and `app.js` is
now a loop that builds elements from it. Painting stays untested by Node; what
to paint stopped being untestable.

26 tests in `node --test web/tests` (0.12s, no Rust toolchain, no wasm bundle,
no npm install), plus three in `web/tests/bundle/` that read the generated
.d.ts and so run in the browser job. Playwright goes from 29 tests/36.9s to
16/15.5s on one machine: `widgets.spec.js` and `pointer.spec.js` are gone,
replaced by `contract.spec.js`, which asserts that EVERY field the render model
projects reaches the screen. The four widgets those specs were written for were
one failure at one boundary, and one assertion catches the fifth.

It caught it: `doc_name`/`doc_dirty`/`doc_stored_as` reach no element
(improvise-5jna, already open). They are named in `UNPAINTED` with the ticket,
so the list is the debt written down rather than an exemption.

Thirteen faults injected into the Node suite and eight into the served page,
each watched to fail the test it names; the last of those was strengthened
because the first version passed. `web/assemble.sh` grew an import-completeness
guard whose own first version was blind to the multi-line import it exists for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:33:49 -07:00
Edward Langley 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>
2026-09-20 12:15:14 -07:00
Edward Langley 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>
2026-09-20 11:48:26 -07:00
Edward Langley 7b2e9d8606 docs(clients): the editable-is-the-id list still named two kinds
A fact stated twice is changed by a grep, and this copy was missed. The
"four decisions that are silent when wrong" list in context/code/clients.md
enumerated CellId::Record and CellId::Aggregate and then said None is "not
aggregate" — written when those were the only two kinds. There are now four,
and a reader of that list would have concluded a Derived or a Create cell must
be a None.

Found by grepping for the claim after PR #47 put web/demo.improv on the site's
front door, which is also how the remaining stale copies were located: two in
prolog-sessions/kb.pl and two in context/repo-map.md, both orchestrator-only
files, reported as text in the hand-back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:42:00 -07:00
Edward Langley 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>
2026-09-20 11:18:17 -07:00
Edward Langley 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>
2026-09-20 11:05:17 -07:00
Edward Langley 19bb5d3b22 refactor(protocol): ClientState and key resolution are the protocol's (improvise-0yq8)
`resolve`/`step` and `ClientState` lived in `improvise-wasm-client`, and
nothing about either was a browser fact: both depend only on `ViewState`, the
protocol widget caches and the announced window, all already protocol types.
The move costs no new dependency and makes design-principles §5 true rather
than aspirational — a new front end now needs exactly two things, its own
event → `KeyEvent` mapping and painting.

- `state.rs` → `improvise-protocol/src/client.rs`
- `keys.rs::{resolve, step, KeyOutcome}` → `improvise-protocol/src/resolve.rs`
- `socket.rs::{typed, typed_cell_command}` → `resolve.rs`, since `step` calls it
- `BrowserKey` stays in `improvise-wasm-client/src/keys.rs`: it *is* the
  browser half of the two things a front end still owns
- `render_model` becomes a free function, `ClientState` being foreign now

`tests/lockstep.rs` is kept deliberately. It drives `reduce_key` — the host's
`Cmd`/`Effect` path — against `resolve`; those are still two derivations in two
crates, one unreachable from a browser, so the move changed which crate
`resolve` is spelled in and not that it is a mirror.
`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`
does not fire.

1311 tests pass, unchanged from the baseline: this commit moves code and
changes no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:56:33 -07:00
Edward Langley 3d3bec43e0 docs(web,harness): cap the JavaScript and the browser harness
web/app.js's transport/watchdog/frameCost arguments and web/worker.js's two
ordering rules — including the one nothing here can test, that an `async`
message listener does not serialize — are now prose in context/code/clients.md.
The harness keeps its own section: why a worker, why a report, the engine
coverage and what `wipe` deliberately cannot clear.

harness/tests/*.spec.js's comments were the record of what each assertion
proves; they are in context/test-evidence/clients.md under the exact test
titles, including the `test.fail` measurement that could NOT become a CI guard
because its trigger is the host's network stack, and the closed-invalid
improvise-xps8 report that a simulated fix appeared to confirm.

ws-server/src/main.rs's ```text fence was prose claiming the doctest
exemption, not a runnable doctest; the synopsis is now two lines and the full
form is in the prose file. No fences remain in this unit.

Verified: node --check on every JS file touched; the non-comment diff for both
'*.rs' and '*.js' is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:02:03 -07:00
Edward Langley 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>
2026-09-20 09:57:39 -07:00
Edward Langley c386e34a2b docs(wasm-client): move the key-resolution doctrine to context/
keys.rs's 78-line module doc, the 55-line strands_the_user argument and the
per-arm reasoning move to context/code/clients.md; the ten test narratives —
including the two Copilot findings on PR #27 and the "an arm asserting
something false certifies what it was added to rule out" lesson — move to
context/test-evidence/clients.md, keyed by test name.

The named list of command names that still need the host stays in the test, as
gotcha 47 requires; the prose now says so explicitly.

No behaviour change: comments only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:54:05 -07:00
Edward Langley 3970ae30b5 docs(ws-server): move the multi-document argument to context/code/clients.md
registry.rs's grain/addressing/lifecycle argument, transport.rs's two socket
rules and the MVP-limit note, lib.rs's layer split and host.rs's "why it moved"
are now prose. The five check_the_checker narratives and the socket test's
scope statement move to context/test-evidence/clients.md.

Two comment-only repairs found while reading: Registry::handle's doc comment
was attached to the #[cfg(test)] `model` helper above it, and `connection`'s
doc comment sat above a `//` note that detached it from the item. Both now
sit on the item they describe.

The ```text usage fence in main.rs is kept — it is a synopsis, not a doctest,
and the checker exempts it; the prose around it is capped instead.

No behaviour change: comments only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:51:09 -07:00
Edward Langley 625f4acb80 docs(worker-server): move the browser-host argument to context/code/clients.md
The 121-line module doc on worker.rs, vfs.rs's seeding and flat-root rules,
bindings.rs's async-seam argument and the crate lib.rs coverage statement now
live in context/code/clients.md; the four bug narratives in the tests move to
context/test-evidence/clients.md, keyed by test name.

No behaviour change: comments only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:49:04 -07:00