29cdab1a4a049d260dc966bca132a08f180a50e1
37 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
29cdab1a4a | Merge remote-tracking branch 'gh/main' into worktree-agent-a97453f7fa65d51b9 | ||
|
|
01d48f2ce9 |
Merge remote-tracking branch 'gh/main' into fix/ws-origin-check
# Conflicts: # context/test-evidence/clients.md |
||
|
|
cead3210e5 |
fix: two Origin headers state none a policy can answer for
Review of the previous commit. `HeaderMap::get` is first-wins, and the deployment README recommends puts a reverse proxy in front of the socket — one that APPENDS `Origin` rather than setting it leaves two, and answering from the first lets the second be anything at all. No browser sends two, so two is not a state any policy can answer for and the handshake is refused, exactly as an `Origin` that is not text already was. Guarded by two_origins_state_none_this_policy_can_answer_for, proved by restoring the first-wins read, which fails that test alone across the workspace. The test also asserts the allowed origin BY ITSELF is still allowed, so the refusal is demonstrably the second header rather than the first. Second review finding: a_handshake_from_a_foreign_origin_is_refused matched "403" in a Display string, which cannot tell a refused upgrade from a dropped connection. It now matches Error::Http and asserts the status. Proved by changing forbidden() to UNAUTHORIZED, which fails it and a_refusal_is_a_forbidden_response. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d100639348 |
fix: check Origin at the handshake, so a page cannot drive someone's session
improvise-kc7n. A websocket handshake is exempt from the same-origin policy — there is no CORS preflight — so any page a user visited could open a socket to improvise-ws-server from that user's own allowed address and read and edit every document served. An address check cannot see it: the request genuinely comes from an allowed client, and only Origin says what is driving it. Loopback is the least protected case, having no proxy in front of it at all. `src/origin.rs` is the decision and `transport::admitted` its one caller, which refuses the upgrade with a 403 rather than accepting and ignoring. The default allows pages served from loopback — which is the shipped local recipe, page on `http://localhost:8080` and socket on another loopback port — and refuses everything else; `--allow-origin` names the deployed page, which reaches the server through a TLS proxy and is not loopback. An absent Origin is allowed and decided rather than defaulted: no browser omits it, so absent is a non-browser client, and refusing those would break this crate's own tests while buying nothing that --bind does not already buy. Five edits, five guards, each reverted alone against the named test: - origin.rs permits() present arm -> a_foreign_origin_is_refused - origin.rs permits() None arm -> a_handshake_with_no_origin_is_accepted - origin.rs is_loopback() -> a_handshake_from_a_loopback_page_is_accepted - transport.rs handshake callback -> a_handshake_from_a_foreign_origin_is_refused - main.rs --allow-origin wiring -> allow_origin_widens_the_policy_and_nothing_else_does `transport::serve` takes the policy rather than defaulting one, so a host cannot forget to pass it. The argument is context/code/clients.md §Origin and the evidence context/test-evidence/clients.md; README no longer says the server never looks, and the proxy's Origin allow-list is now defence in depth. This is not authentication — it answers what page is driving a client, never who is connecting. improvise-kpnr is untouched and still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
a69b0bca86 |
test: the third edit of improvise-0x9u's repair, and two evidence corrections
`close` restores the DRAWN cell text rather than the seed, and reverting that one line left the whole suite green while `18400` sat where `18,400` had been on the shipped demo. Asserted after the second click, proved by that revert. improvise-o31u's desktop arm cannot fail for the reason its evidence gave: both named mechanisms were measured and neither moves it (`#cast-pivot` is 966px at 1280, so an 880px floor is inert wherever it is placed). The entry now says it is forward-looking, and credits the phone arm with the fault it actually catches. Plus three one-liners: UNCALLED's source-scan blindness, the gzip -9 proxy in the improvise-cl7y table, and why `view-back` is not one of its five siblings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
8af37d44a1 |
feat(ci): two conventions become gates (improvise-w8ih, improvise-4ktt)
The stamp gate is scripts/assemble-stamp.sh, taking FILE PATTERN pairs, and scripts/assemble-stamp-test.sh is its check_the_checker. Deleting the final `test -z "$missing"` makes four of its eight cases fail; before the extraction that same deletion was invisible, both misses printing while assemble.sh exited 0. It is a separate script because exercising it inside assemble.sh would need a fabricated web/pkg, which is a second answer to "what does the site assemble from" (improvise-vvl3). Wired into the `build` job, first, because it needs nothing and guards a step in that job. web/tests/bundle_freedom.test.js refuses a flat web/tests/ file that names the bundle directory -- the constraint keeping the `js` job at seconds rather than ~4m30s, stated in prose and checked by nothing. It scans its own filename too, so it cannot become the place a violation hides, and its companion asserts web/tests/bundle/ still holds a test that DOES use the bundle: a scoping rule is uninteresting if nothing falls outside it. Both faults were injected and watched to fail; evidence in context/test-evidence/tooling.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
9e37213cd4 |
fix(docs): re-record the four casts from a script, with no shell to leak
improvise-2oxt. All four casts under docs/casts/ opened and closed on `(0) Mac:<hostname> ~gf/u/edwlan/improvise` — a real hostname and home directory on what improvise-kh8 made the site root — and `formulas.cast` and `import.cast` showed that prompt as their visible OPENING frame. drill.cast also predated the records columns entirely, showing a single `Value` column where the current build draws a column per measure plus `+cat` and `+`. The repair is structural, not careful. `docs/record-casts.py` drives `improvise` on a pty it owns and writes asciicast v2 itself, so `improvise` is the pty's ONLY child and there is no shell for a prompt to leak from — nothing to remember to clean, and nothing that comes back when somebody re-records on another machine. Three other things stopped being accidents with it: the geometry is an input (120x37, the numbers docs/index.html already spelled, so they did not change), the pacing is a per-step table, and the header carries no `env` (which held $SHELL) and no `timestamp` (the recording machine's clock). Two decisions that each cost a re-record. No `:q` is typed at the end: the player loops, so the last frame is also the frame before the first, and a minibuffer reading `:q` is what it would show — the process is signalled instead. And each scenario had to be ARRANGED so the thing it names is visible: the drill cast drilled into a cell with no records behind it, and the pivot cast pressed `c` on a category already on the column axis. Both recorded cleanly and demonstrated nothing — true frames about the wrong thing — and neither was visible from the keystroke list. That is the method worth keeping: a cast can be replayed through a small ANSI reconstruction and READ AS TEXT, so "what does frame N show" has an answer rather than a guess. It is how the `+cat`/`+` columns in the new drill.cast were confirmed present, and how both dud scenarios were caught. The guard is `landing.spec.js`, asserted over the cast FILES: no header carries `env` or `timestamp`, the geometry is the one the page configures, and no chunk contains an OSC window-title sequence — the leak's signature, emitted by a shell once per prompt with the cwd in it and by `improvise` never. Asserted over the file rather than the rendering because a player draws a prompt as ordinary text, so a screenshot assertion would have to know what the hostname had been. BOTH clauses were injected separately, because the `env` one would otherwise mask the OSC one: the old cast fails on `env`, and the old cast with `env` stripped fails on the title sequence. docs/index.html loses the `p.dated` note admitting the drill cast was out of date, which is now false. Tests: 33 harness + 3 skipped (was 28 + 3). Rust and node unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
47a070fec4 |
docs(clients): three "free" bindings priced, and what a parsing client costs
improvise-4bwa and improvise-cl7y. Neither can be CLOSED from this crate — both
land in `improvise-protocol` and `improvise-command`, which another agent owns
in this wave — so what lands here is the part that was actually missing: the
design of each, and a measurement nobody had taken.
improvise-4bwa. `view-back` is DECLINED in writing, which its own acceptance
criteria allow and ask for. It is not an enrichment at all: `ViewNavigate
{ forward: false }` emits `ApplyAndClearDrill` first, whose operand is
`drill_state.pending_edits` — session-local residue routed with no `SessionId`
— so a client that mirrored it would commit somebody else's uncommitted typing,
the exact thing improvise-3x3u removed. Blocked on ADR 001 layer 2.
The sharper reading, which the ticket did not have: `mirror_the_cmd_line_for
_line` is a rule about RESOLVERS, and a `Cmd` whose first effect is
unaddressable is not a `Cmd` a thin client may mirror at all. The doctrine does
not conflict with the decline — it does not reach it.
The other two are recorded with their shapes. `toggle-prune-empty` is one bool
on a per-view projection; the `ModelCommand` is composable today and it is the
SENTENCE beside it that a client cannot write, so mirroring it diverges on the
status line in half of all presses. `delete-formula-at-cursor` needs
`FormulaCache`'s ELEMENT TYPE to carry the target and its category — a `targets`
vector beside `formulas` would be two answers to "what is in the formula panel",
improvise-edp's disease and the one improvise-anr9 had to pay a type change to
undo.
improvise-cl7y. Its note leaves the dependency route's cost unmeasured, so it is
measured: `improvise-formula` added to this crate, `parse_formula` called from
an exported method so nothing is stripped, bundle built both ways.
today 503,854 raw 191,613 gzipped
with improvise-formula 634,515 raw 238,973 gzipped
cost +130,661 +47,360
(+25.9%) (+24.7%)
A quarter of the download for a status string — evidence for the other route,
the host addressing a status back to the composer as `ClientFrame::Save`'s
rejection already does. What the number does not settle is the more interesting
half: a parsing client is a SECOND DERIVATION of whether a formula is well
formed, agreeing with the host only while both call the same function. So it is
a floor, not the price of a feature.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
852e3f8659 |
fix(web): a selection driven below the fold scrolls itself into view
improvise-jq8l. `web/app.js` called `scrollIntoView` NOWHERE, so a selection moved past the bottom of `#grid` stayed there, invisible, with no way back but scrolling by hand. What the ticket did not predict: this is NOT a projection bug and NOT a `row_offset` bug, and the acceptance criteria's talk of reconciling the absolute selection against the announced window describes work `render.rs` already does. The row had been sent. `#grid` has `overflow: auto`, `ROW_PX`/`COL_PX` UNDER-estimate (a real row measures 28.6px against the assumed 26) and inline group rows are deliberately not charged for at all, so the page renders more rows than its own box can show and the overflow is the DOM's. `block: "nearest"` is the whole option: it scrolls only when the element is out of view, and it scrolls the nearest container rather than the document. The expensive half was the fixture. Neither shipped document can reach the state — `web/demo.improv` is four rows, `harness/fixtures/grouped.improv` is eight, and a probe measured BOTH at `scrollHeight === clientHeight` in a 484px box. The assertion would have passed against a page that never scrolls, which is why the spec asserts the overflow exists before it presses a key. `harness/fixtures/tall.improv` is thirty rows in three groups; measured against the unfixed page it gives 105px of overflow with the selected cell 96px below the visible bottom and `scrollTop` still 0. Tests: 25 chromium harness (was 24), Rust and node unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
628f43121d |
feat(web): the title bar is painted, a click survives an editor, and the twin door is gone
improvise-5jna, improvise-0x9u, improvise-seik — three browser-page defects with one shape: a thing that looks done from every check except running it. improvise-5jna. `doc_name`/`doc_dirty`/`doc_stored_as` were projected by z754 and painted by nothing. `render_surface.test.js` could not see it — it is a SUBSTRING SEARCH over the page's source, so a field named by a function nothing calls reads as painted, which is also how `model.minibuffer` shipped dead. So the guard is a harness assertion that loads the page and reads `#doc-name`, proved by writing it first (`element(s) not found`). The joining is `shell.js::documentTitle`, the TUI's own `name (stored_as) [+]` minus the chrome; the `[+]` is a node test, not a harness one, because this deployment persists on every accepted batch and clears the flag in the same round trip, so the marker is transient. UNPAINTED is now empty. improvise-0x9u. From an EDITING state a cell click did nothing at all: the mousedown moved focus, the input's blur ran `close`, `close` repainted, `replaceChildren` destroyed the `<td>` the mousedown landed on, and no `click` was ever dispatched. The old spec pressed Escape between the two clicks, so it only ever clicked from a non-editing page — this repo's characteristic defect. Worth recording: moving to `mousedown` is NECESSARY AND NOT SUFFICIENT, and the first attempt failed the same assertion with the same symptom. Removing a focused `<input>` fires `blur` SYNCHRONOUSLY, so `close` rebuilt the table from inside itself and detached the `<td>` again. `close` therefore no longer repaints, the `blur` arm checks `closed` first, and the handler `preventDefault()`s so no focus change happens at all — otherwise the default action focuses `#grid` after `input.focus()` and blurs the new editor. `close` restores the DRAWN text, since nothing repaints behind it any more. improvise-seik. `Client::viewport` was a wasm-bindgen door with no caller, kept for a reason `web/app.js` falsified in the same wave. It is one letter from `set_viewport` and fails SILENTLY — identical frame, forgotten window, every cursor key an upstream black hole. Deleted, along with the exported `init()` free function that duplicated `new Client()`. The guard is not the deletion: `wasm_surface.test.js` now reads the generated `.d.ts` and requires every declared method to be one `app.js` calls or to be named in `UNCALLED` with its reason. Proved by injecting a spare `static viewport(...)` into the built `.d.ts` and watching it fail by name. Tests: 1405 Rust (unchanged), 27 node + 4 bundle (was 26 + 3), 24 chromium harness (was 23). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
be01339039 |
Merge gh/main (4e48a6e, PRs #70 and #72) into wave/browser-keys
Two doc conflicts, both pure appends at the same place, both resolved by keeping BOTH sides: - `context/code/clients.md`'s boundary table: #72 added the help-overlay row where this branch added the key→screen row. Six became seven, not six with one of them replaced. - `context/test-evidence/clients.md`: both branches appended entries. `README.md`, `harness/build.sh`, `harness/playwright.config.js` and `.gitignore` are byte-identical to main's — this branch never touched them, so #72's measured engine-coverage claims are intact rather than restored stale. The check that matters here is NOT the count. #72 added three spec files while this branch added one, so a total would have masked a loss. Diffed harness test NAMES across the merge instead: 16 before, 23 after, **nothing lost**. 1398 + main = 1405 Rust passing; harness 28 passed + 3 skipped; node 26. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
4e48a6ecb4 |
Merge pull request #70 from fiddlerwoaroof/wave/effect-classification
fix(command): a counter is bumped on an observed change, not a declared intent |
||
|
|
4aa4e88ce4 |
fix(protocol): Ignored is a black hole too, and a decline cannot shadow working code
PR #71 review, blocking finding — and it was demonstrated rather than argued. `KeyOutcome` has TWO silent variants and the new guard watched one. It asserted `resolve(..) == Upstream(event)`, but `KeyOutcome::of(vec![])` returns `Ignored`, which puts nothing on the screen either. Degrading only the three declines no browser spec presses — add-record-row, delete-formula-at-cursor, view-back — to `Step::Declined(Vec::new())` left 1397 Rust, 19 harness and 26 node tests passing. Three of the six commands this change exists to make audible could go completely silent with the whole evidence base green. The generalisation is the part to keep. `KeyOutcome::of` turning an empty `Local` into an honest `Ignored` is good design and also a LAUNDERING step: every wrong answer that composes nothing arrives wearing the variant that is usually innocent. A guard's discriminator has to be STATE — did anything the user can see change — never the shape of the return value. That is why the `+cat` injection failed in `both_ends`, which compares documents and cursors, and the decline injection passed everywhere, where nothing compared a screen. Widening it costs ten entries, all one boundary case: `move-panel-cursor` mirrors a `Cmd` that returns nothing when the cursor does not move. `furnished` now carries three of everything with all three panel cursors seated at index 1, so the residue is zero rather than parked in the named list. Seating them at 0 and calling the `Up` results expected would have hidden the real defect in the same list as the boundary case. `nothing_declined_is_also_composed` closes the other direction, which was a doc comment. `step` reaches `declined` first, so an arm in `composed` for a declined name is unreachable — a future ticket implementing `view-back` would leave the browser saying "not available yet" over working code with nothing noticing. It is derived from `declined` itself over every `(name, args)` bound in every mode, parents and prefixes and sequence steps included, so a seventh decline needs no edit here. It found a real overlap on its first run: `enter-mode` is answered by both. Rather than move the check, both halves now hold the rule — `composed` returns `None` for a stranded mode — so removing one fails towards `Upstream` rather than towards a dead room. `Keymap::parent()` added so a test can walk the fallback chain. 1397 → 1398 Rust; harness 19 + 3 skipped; node 26. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
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
|
||
|
|
f9580a33af |
docs: what the harness now reaches, and the evidence for each assertion
The engine-coverage and measurement passages in context/code/clients.md, the boundary table (four boundaries become six), and README's caveat -- which named the grouped and multi-level DOM as never executed, and it now is. A fact stated in more than one place is a grep, not an edit (gotcha 48). context/test-evidence/clients.md records, per test, what fault was injected to prove it can fail: the blanked help keys, the five grid-frame injections, the 404'd casts and the dead front-door link, and the mis-wired item-row click -- plus the two-command test-name diff that found that last one. Playwright: 26 passed + 3 skipped, 34.4s / 33.7s (was 17 + 3, 15.1s); the landing page's cast playback is most of the difference. node --test web/tests 26, cargo test --workspace 1394, both unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
beef2587ad |
fix(command): a counter is bumped on an observed change, not a declared intent
`EffectKind` says what an effect *means* to do. `layout_version` and
`model_state.dirty` were believing it, and failed in both directions.
improvise-pdvm: AddItem/AddItemInGroup/RemoveItem declare CellData and
reshape the grid. Since improvise-9mo9 they call reset_cursor, so
cursor_epoch moved while layout_version did not -- one event moving one of
two counters that answer the same question, and a client told its cursor is
invalid while its layout basis tier still reads current.
improvise-75ld: a refused AddCategory (MAX_CATEGORIES) and a *duplicate* one
both bumped layout_version and raised dirty. The duplicate is the reachable
silent case -- Model::add_category returns Ok for a name it already has, so
it does not even abort -- and it refused every other client's next batch with
Rejection::LayoutMoved and made the worker rewrite the whole OPFS document.
Both are answered the way record_versions and cursor_epoch already are:
observe. Two fingerprints are taken around a batch that contains any
EffectKind::Model(_).
view_shape one view's DRAWN shape (axes, effective items minus this
view's hidden ones, collapsed groups with something drawn
under them, page selection, prune_empty). Per view name, so
it answers both "did anything move" and "which views moved".
document_config everything a LayoutGeometry effect can write that the
file would carry. Raises dirty. A different hash on purpose:
a number format and a hidden ghost are bytes on disk and not
shape.
No `Effect::kind()` declaration changed, so Dirty::of (gotcha 35) and
record_versions (gotcha 40) are untouched. What moved is which reader
believes the declaration: CellData and WholeWorkbook still raise dirty off
it, in an exhaustive match so a fourth ModelChange must answer for itself.
improvise-nynk: Sessions::adopt_cursor_epoch reset every mirror and returned
one Broadcast::ToAll, which improvise-jytm made wrong -- a reshape of view A
threw away the cursor of a client on view B. The trigger stays the epoch
moving (gotcha 24); only the audience narrows, to the observed reshaped-view
set, as one Broadcast::To per affected client. An empty set means "not
known", never "nobody", and cursor_invalidated is a named function with both
arms tested so that fail-safe cannot be tidied away.
improvise-56ek: SetPanelOpen and SetPanelCursor matched a Panel to a field
twice, in two crates. One home now -- ViewState::panel_open_mut /
panel_cursor_mut and their read halves -- and no compatibility test between
them, because there is no second copy to agree with.
1400 tests pass (baseline 1394: +7 guards, -1 replaced). Every guard proved
by injecting the fault it names; the injections and what each one broke are
in context/test-evidence/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
ec03f950b3 | Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks | ||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |