Commit Graph

21 Commits

Author SHA1 Message Date
Edward Langley ec03f950b3 Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks 2026-09-20 14:36:19 -07:00
Edward Langley 89f67afd4c fix(harness): make a vacuous watchdog denial fail, and single-home the window
PR #65's review found this PR had planted a fresh instance of the defect it
exists to repair. Both blocking findings fixed, each demonstrated.

FINDING 1. "a peer that cannot be reached is named, and the watchdog stays out
of it" could not see the property its comment claimed. `shell.arm` runs only
after `await transport.ready`, which never resolves on a socket that never
opens — so the watchdog is never armed there and denying "sent no document"
denies an unreachable event. Reverting PR #21's routing left that clause
passing.

The aggravating half: `context/test-evidence/clients.md` already said so, and
this PR EDITED that paragraph — inserting "runFor(10s) advances the watchdog"
immediately above the sentence explaining it is never armed. Not text the PR
left alone; text the PR walked past mid-sentence.

The clause is gone and the test keeps the naming property it really guards: a
`close` on a socket that never opened must not read as "disconnected". And the
shape cannot return — `outlastTheWatchdog` now asks the page `timesArmed() > 0`
before jumping, so a watchdog denial re-added to that path FAILS BY NAME. It
asks *ever armed* rather than *armed now* on purpose: by the time the disconnect
test jumps, a correct page has already disarmed, which is the property, so the
obvious spelling would have denied the passing case. Building the guard is what
surfaced that distinction; reading had not.

FINDING 2. The spec kept its own `HANDSHAKE_TIMEOUT_MS` beside a comment saying
a silent disagreement "would pass for the wrong reason". It did: the spec at
1000 against the page's 8000 made the suite green over the reverted routing,
because the jump landed inside the window. The constant is now exported from
`web/shell.js` and imported by both, so the disagreement is unconstructible.
Shrinking it in its one home still catches the routing revert — that is the
demonstration the guard did not go quiet.

Filed, not fixed: improvise-78v6 (#61 also deleted the help-overlay spec and
nothing replaced it — a PR about a dropped guard that did not diff the dropping
PR's test names) and improvise-5ray (--check-evidence collects only `### ` and
resolves only Rust fns, so every browser entry's `####` is invisible; appending
one that names nothing exits 0, which is how the dangling heading cleared the
gate for a release).

Verified on the merged tree: node 26/26, bundle 3/3, Playwright 17 passed + 3
skipped, cargo test --workspace 1378 passed, comment-runs clean,
--check-evidence 501 resolving, all cited ids resolve, no .beads diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 14:24:25 -07:00
Edward Langley 1922beca06 fix(tests): a launch default disarms every test that asserts what it supplies
USER RULING (2026-09-20): the category sidebar is the launch default in the
TUI as well as the browser. `improvise model.improv` opening with the
sidebar showing is intended, not collateral. The behaviour is unchanged;
what changes here is that it is now a stated, tested decision.

Three tests asserted `category_panel_open` was TRUE after doing something
to make it true. The default now supplies that, so all three passed
without the code under them working.

**`set_panel_open_and_cursor` is the serious one.** With it vacuous,
`effect::SetPanelOpen`'s Category arm could be replaced by a no-op — `C`
doing nothing in the terminal — and the whole workspace stayed green at
1391 tests. Now closes the panel first and asserts it closed, so the
effect has to move the flag.

**`panel_commands_address_the_named_panel_only` is the subtle one, and my
first repair of it was ALSO vacuous.** Folding `[open:false, open:true]`
and asserting the final state is true still passes against a reducer that
folds nothing: the default already supplies `true`, so neither command
needs to land. Only asserting the FALSE direction discriminates. Caught by
injection reporting NONE failing after the "fix" — it would otherwise have
shipped looking repaired.

The rule this leaves, now in `context/code/clients.md`: **assert the
direction the default cannot supply.**

`tests/tui_launch_state.rs` gives the ruling a guard on the front end it
newly changes — the TUI's launch frame had no test at all. Not a rendered
frame: §7 says not to test `Widget::render` and the repo has no
`TestBackend`, so the target is `App::new`. One of the three walks
`Panel::ALL` comparing `App::new` against `ViewState::default()`, which is
what would catch a TUI-only launch state — the divergence wsaw closed.

**The view-row click test was vacuous for the property it is named for.**
`Default` is index 0 and holds `view_panel_cursor`, so clicking it made
`index` and the cursor the same number. Injecting the cursor-vs-index
fault into `click_view_row` passed the contract suite 3/3 — the exact
fault the ticket, the evidence and the Rust test exist for was invisible
to the browser half. Now switches to Default by keyboard, then clicks
Plan, and restores the two assertions the relocation had dropped.
Inherited from the deleted `pointer.spec.js`, not introduced here.

Filed rather than fixed: improvise-56ek (`SetPanelOpen` maps a Panel to a
field twice, in two crates — each copy needed repairing independently, and
the harness passing against a no-op'd effect is correct rather than a gap)
and improvise-qf57 (the formula row's dropped `toHaveCount(0)`).
improvise-4ibz is closed by the ruling.

Every claim here proved by injection under `--no-fail-fast`, which is the
method note: `cargo test --workspace` is fail-fast and understates blast
radius.

Gates: 1394 Rust tests, 26 + 3 Node, 16 Playwright (3 WebKit OPFS skips),
clippy -D warnings, fmt, eight wasm crates, comment runs, 516 evidence
entries. `.beads/` untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:12:44 -07:00
Edward Langley 692aa0b1d3 docs(clients): compare test time, not two invocations' wall clocks
The eleven deleted per-widget tests and the three contract tests replacing
them were compared at 3.3s against 3.4s, which reads as a near-tie. It is not
one: those were two separate `npx playwright test` invocations, each paying its
own cold start (~0.6s and ~1.9s), so the wall clocks measured startup as much
as tests and flattered the replacements. The per-test sums are 2.7s against
1.55s.

The conclusion is unchanged and has more margin: eleven tests at 2.7s cannot
buy a twenty-second saving, so the deletion of the eleven bought shape and the
two real-timer watchdog waits bought the speed. It also explains, without
needing anything else, why the contaminated baseline's clock barely moved —
1.55s of contract tests, warm inside a full suite, lands under a second against
a 36s total.

Same discipline as the row above it in the table, applied to the other side of
the comparison: two numbers are only a comparison when they were measured the
same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:06:00 -07:00
Edward Langley 481f1da444 docs(clients): measure the baseline on a clean tree, and quote only that
Third correction to one number, and this time by measurement rather than
arithmetic. `1af51c9` reached by a FULL checkout — which removes a tracked
file the target does not have, where `checkout -- <paths>` leaves it — then
built and run twice on a private port, and the same for this branch in the
same session:

  before  23 chromium, 26 passed + 3 skipped, 36.2s / 36.3s
  after   14 chromium, 17 passed + 3 skipped, 15.1s / 14.9s

Three things that changes.

The `before` row was internally inconsistent: it paired a corrected chromium
count (23) with the contaminated run count beside it (29), and 23 + 3 firefox
is 26. A row whose columns do not add up is a row built from two trees. The
note now says the columns must agree, because that identity is what caught
this every time it was caught at all.

`32.8s` is gone. It was not measured by anyone who could hand me its
provenance — I attributed it to the reviewer, who had only ever counted
`1af51c9` statically and never ran a full pre-PR suite — and it does not
reproduce here. Quoting an unattributable number inside the paragraph that
exists to explain an unattributable number was the wrong repair.

And the contamination's cost was the COUNT, not the clock: the contaminated
run read 29 against the clean tree's 26, but 36.9s against 36.2s, because the
three extra tests cost under a second between them. A wall clock absorbs a
contaminated tree without a trace; an arithmetic identity does not. That is
now the stated lesson, and the same error surviving three corrections — into
PR #61's body as "26 chromium", then through a fix that corrected one column
and left its neighbour — is the worked example of `clients.md`'s own rule that
a fact stated in more than one place is changed by a grep, not an edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:05:04 -07:00
Edward Langley bf01469cf8 docs(clients): label the test counts, and correct a contaminated baseline
Review of the fix found the speed table quotes one number where there are two:
every spec but `opfs.spec.js` is Chromium-only, so a count read off a run is
three larger than the count of tests in the files, with three more skipped. The
table now has a chromium column and an all-projects column, and says so — the
same "two numbers, not one" the Rust suite learned in design-principles §7.

Chasing that ambiguity turned up a worse error in the number itself. The
`before` figure was measured with `git checkout 1af51c9 -- harness web` inside
the working branch, which restores TRACKED files and leaves untracked ones
where they are — so `contract.spec.js` stayed behind and the "baseline" ran
main's 23 tests plus three of the branch's own. It read 29/36.9s, which looked
entirely plausible. The honest figure, measured on a clean tree, is 23/32.8s.
A baseline taken by checking paths out over your own work measures a tree that
never existed, and that is now written down where the number is.

Also records the probe for the open question the faked clock left. A peer that
opens, stays silent past the watchdog window and is closed afterwards produces
a byte-identical `#status` transcript under `page.clock` and under a real
nine-second wait — four states, same order, same text. So `runFor` does not
flatten that ordering. What stays uncovered is an event landing strictly INSIDE
the collapsed window, which a synchronous `runFor` cannot deliver and which is
a race the page promises nothing about. Stated rather than left to be
re-derived; the probe itself is not kept, being a characterization of
Playwright's clock rather than of this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:01:16 -07:00
Edward Langley 57a25807af Merge gh/main (e8562f0) into fix/web-panel-clicks
PR #61 landed while this branch was open. It deleted
`harness/tests/pointer.spec.js` and `harness/tests/widgets.spec.js`, split
`web/app.js` into app/shell/serial, and added a Node suite under `web/tests/`.
Two conflicts, both resolved deliberately rather than by reflex.

**`harness/tests/pointer.spec.js` — modify/delete, resolved as DELETE.** The
habitual "keep both sides" would resurrect a file #61 removed on purpose.
`contract.spec.js` states its own scope as the RenderModel→DOM boundary and
the pointer's half of the JS→wasm one, so that is where these assertions
belong. Merged in, rather than restored:

- the category sidebar is open at launch and `C` still hides it;
- a first tile click selects and does NOT touch the document, a second cycles;
- the view row and formula row clicks improvise-iioi added, which
  `contract.spec.js` had no entry for at all.

**`context/test-evidence/clients.md` — content, resolved as BOTH SIDES.** The
two `pointer.spec.js` subsections are retargeted at the file that exists, with
a note saying why restoring the deleted one would have been wrong. The two
older `#### … the file's own scope` sections keep their history and gain a
forward pointer, because a reader following them would otherwise hunt for a
file that is gone.

**`web/tests/bundle/wasm_surface.test.js` now names `click_formula` and
`click_view`.** That test is an exact `deepEqual` over the page's `client.*`
calls and its comment says a new call "has to come and say so". It did: it
failed on the merge, which is the mechanism working. It could not have been
satisfied earlier — it did not exist when this branch was cut.

Two review findings fixed on top, neither a live bug:

- **The tile guarantee was overclaimed.** `T` leaves `tile_cat_idx` at 0, so a
  click on the LEFTMOST tile cycles on the first gesture. "A click never
  mutates on its own" and "a second click cycles" are both false against that
  probe. The behaviour is right — a click does what `Enter` does there — so the
  prose now states the implemented rule, and
  `t_then_a_click_on_the_leftmost_tile_cycles_it_immediately` names the case
  and asserts the neighbouring tile answers the same gesture differently.
- **`switch_view_at`'s `Option` had no guard on the keyboard side.** Removing
  the `unwrap_or_default()` makes `Enter` in an empty view panel `Upstream` —
  gotcha 47's black hole — and passed the whole workspace. That would make
  membership of the host-needed list state-dependent, which is the one property
  that list denies. One test closes it.

Gates on the merged tree: 1391 Rust tests, 26 + 3 Node tests, 16 Playwright
(3 WebKit OPFS skips), clippy -D warnings, fmt, harness fmt + wasm32 clippy,
eight wasm crates, comment runs and 513 evidence entries clean. `.beads/`
matches gh/main exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:00:19 -07:00
Edward Langley 9863d96eef fix(harness): restore the close-routing guard PR #61 dropped, on a faked clock
Adversarial review of PR #61 found one real coverage loss, and it is the
defect class that PR's own body names: an assertion that survives the deletion
of the thing it was about. `shell.spec.js` lost "a socket that opened and then
dropped reads as disconnected, and stays that way" and both MutationObserver
negative assertions. Reverting PR #21's routing half in `web/app.js` — the
socket `error` and `close` arms calling `setShell` instead of `fail` — left the
node suite 26/26 and Playwright 13/13 green.

`shell.test.js` cannot cover it, and why is the whole boundary argument:
"a loud failure DISARMS the watchdog" tests `createShell` in isolation and
cannot see whether `app.js` ROUTES the close event through `fail()`. That
routing is the half gotcha 48 says keeps getting lost; this was the third time,
after two earlier mis-confirmations that simulated the `opened` flag and not
the routing.

Both tests are back under their original names, so the two `####` headings in
context/test-evidence/clients.md name live tests again. They cost ~75ms each
rather than ~9s, because `page.clock` fakes the PAGE's timers: the page, the
socket, the listeners and the `setTimeout` call are all real and only the
passage of time is simulated. That is not a weakening, and the injections are
the proof in both directions — if `runFor` did not advance the page's timers, a
broken routing could not surface the watchdog's message and they would pass:

  - both arms reverted to `setShell`  -> "…and stays that way" fails
  - close arm only                    -> "…and stays that way" fails
  - close arm drops the `opened` flag -> "…the watchdog stays out of it" fails

Three injections, three outcomes, none of them the unit test.

The helper guards its own premise, because it turned out not to: `runFor`
without `clock.install()` does nothing AND throws nothing, so a test that
forgot the install asserted "the watchdog never spoke" against a watchdog that
never ran — 6/6 green. `outlastTheWatchdog` now requires the page's own
`Date.now()` to have moved, and that omission fails by name.

Also: `render_surface.test.js` matched `model.<field>` over source INCLUDING
comments, so un-painting a field and mentioning it in a comment passed —
demonstrated against the pre-fix version, then fixed and re-demonstrated.

And the PR's speed claim is corrected where it is documented. The eleven
deleted per-widget tests cost 3.3s between them against 3.4s for the three
contract tests replacing them: they were free. Essentially all of 36.9s -> 15.5s
was two real-timer waits, one of them this guard. The wait was the cost, not
the test.

Filed, not fixed: improvise-igxp (the help overlay's rows are asserted as
present, never as content — inherited verbatim from the test contract.spec.js
replaced) and improvise-4ktt (the js job's bundle-freedom is a shell-glob
convention, not a gate). The `####`-vs-`###` blind spot in --check-evidence,
which is why the dangling heading survived the gate, is a comment on
improvise-r0qa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:59:06 -07:00
Edward Langley 50e4a37214 fix(web): a click is all of its intents, never mutates, and the panel opens
Four user rulings and three review findings, all in the pointer's one
resolver.

improvise-hkr8 — a click is all of its intents or none of them. Each
`click_*` now returns `Option<Vec<Command>>` and `click` does the single
`unwrap_or_default`. The `None` that matters is `BasisCache::claim`
answering that no basis has been published: the click used to keep its
navigation prefix, drop its activation and report success, while the
keyboard handled the identical `None` correctly as `KeyOutcome::Upstream`.

USER RULING — a tile click SELECTS; a second click on the already-selected
tile CYCLES. Every click in the UI is now non-mutating by default. The
argument that earns the two keystrokes is not tidiness: a cycle is
`LayoutGeometry`, so it bumps `cursor_epoch` and resets EVERY connected
client's cursor (gotcha 24) — the easiest input to trigger by accident was
wired to the one class of change that reaches other people. The divergence
from `Enter`, which still cycles in one keystroke, is chosen and recorded.
"Already selected" is mode AND index, because `tile_cat_idx` is 0 before
anything selects anything and tile 0 would otherwise cycle on first click.

USER RULING — the category panel is open at launch, as the shared
`ViewState::default()` rather than a browser special case. A browser-only
default is "the page overrides the flag", which is the same defect as the
"the page ignores the flag" bug improvise-wsaw fixed. This changes the TUI
too: `src/draw.rs` draws the panel off the same field.

Three review findings, all "a guard that would not catch the drift it is
named for":

- The formula-row `assert!` could never fail — the `assert_eq!` above it
  pins the same vector and aborts first. Replaced by an assertion aimed at
  the keymap itself, which is where the fact lives; the reviewer had shown
  that adding `Enter` to FormulaPanel left 1381 tests green while the click
  and the keyboard disagreed.
- `click_view_row` re-spelled `SwitchView + ChangeMode` inline instead of
  sharing a helper, so a change forced into the keyboard arm by lockstep
  left the click behind silently. Both now call `switch_view_at`.
- `context/test-evidence/clients.md` documented the behaviour PR #59 ships
  as one of the injected faults that disproved it, and had no entry for
  #59's new test. Both fixed; the checker's one-directional blind spot that
  hid it is filed as improvise-lyyc.

Every change is proved by injecting the fault: six injections, six distinct
failure signatures, none passing for another's reason.

Gates: 1389 workspace tests (was 1381), clippy -D warnings, fmt, harness
fmt, all eight wasm crates, 27/27 Chromium harness, 189 cited ticket IDs
resolve, comment runs and evidence headings clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:45:31 -07:00
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