b872923331e3ea0bcafbe94a26a7eb01ee29a9d7
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b872923331 |
fix(improvise-2mqz): export the requesting session view
Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com> |
||
|
|
e0f04ee334 |
feat(improvise-2mqz): add wire forms for SaveAs and ExportCsv downloads
This commit implements the download flow for SaveAs and ExportCsv per the USER RULING on improvise-2mqz. The correct shape is: - A request frame asks the host for BYTES - ServerFrame returns the bytes (or Rejected) - Browser triggers downloads via Blob + <a download> Key changes: Protocol layer (improvise-protocol): - Add Request::DownloadDocument and Request::DownloadCsv variants - Add ClientFrame::DownloadDocument and ClientFrame::DownloadCsv - Add ServerFrame::DocumentBytes and ServerFrame::CsvBytes - Add KeyOutcome::LocalThenRequest variant for operations that need both local fold (mode change, buffer clear) AND a server request - Update stranding() to unstrand SavePrompt and ExportPrompt modes - Update keymap to use rebind() instead of commit() for commit-save and commit-export (because LocalThenRequest can't be in a Sequence) Host layer (improvise-command): - Handle new ClientFrames in host.rs returning bytes or Rejected - Add Rejection::ExportFailed variant for CSV export failures - Update CommitSave and CommitExport to clear their buffers - Make render_csv() public in improvise-io for host use WASM client layer (improvise-wasm-client): - Add PendingDownload type with filename, bytes, and kind - Store pending downloads for JavaScript retrieval via take_pending_download() Browser layer (web/app.js): - Add triggerDownload() function using Blob + <a download> - Poll for pending downloads after frame handling This unstrands SavePrompt and ExportPrompt which were previously refused by stranding() due to missing wire forms. Refs: improvise-2mqz Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com> |
||
|
|
253b84b2da |
fix(protocol): a refused frame is put back by the fold that made it (improvise-nxah)
`Client::key` folds a locally-resolved batch before sending it, so the page redraws on the keystroke. A refusal left those folds in place: measured against a real `Host`, a refused commit left the client one row down with its edit buffer cleared while the host's mirror of that same client was still at the origin holding the user's typing. What rolls back is `ViewState` — the thing the fold produces — never a cache. A `Command::Model` is a no-op in `reduce_view`, so the optimistic fold never touched the caches, and the bug report's own evidence (`page_coords`) cannot diverge this way: it moves only under `Projection::Grid`. What diverges is the mode, cursor, status and buffers that ride along with the model command. The rollback needed a positive acknowledgement first, which the protocol did not have: with only `Rejected` on the wire, a client holding two frames in flight cannot tell which one an answer addresses. `ServerFrame::Accepted` closes that — emitted once per accepted mutating frame, last, after everything the frame produced — the same shape as the basis-publication gap ADR 001 originally left. Two existing facts make the rule usable rather than a convention: only `Command::Model` can be refused, so a frame carrying none is answered by neither and needs no entry; and `Rejection::refused_an_edit` already separates the two `Save` refusals, so a failed write cannot consume the answer owed to an edit. A refusal drops one frame and replays the rest, because a single saved snapshot is unsound under pipelining — it would discard a later frame the host applied. This does not do improvise-3al1's force, and it does not contradict it: the 2026-09-21 wave recorded the two tickets as contradictory on the ground that a rollback would wipe the staged typing. It does the opposite — the typing frames carry no model command, are never refused, and have already settled, so rolling back the commit restores the buffer to the typing. Asserted, not inferred. Reasoning: context/code/clients.md, context/code/protocol.md §4. Evidence: context/test-evidence/clients.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
8e24e4011e |
chore(protocol): cap comments in basis.rs, frame.rs, reduce.rs, keymap.rs
The three basis tiers, the one-envelope-pair derivation, the ModelCommand count and the TextEntry spine move to context/code/protocol.md §§2,4-6. Three comments were wrong about the code and are corrected rather than relocated (recorded in §9): - ClientFrame::Key claimed "the host runs it through the keymap against the authoritative App". Host::handle returns an empty vec for that arm -- improvise-bpq2 moved resolution to the client and updated one copy of the argument, not all of them. Fourth sibling of gotcha 48. - every_command_round_trips_through_json named CellKey and CellValue as payload types "the view half never carried". ModelCommand has carried no CellKey since ADR 002, and CellValue is in the view half as SetYanked. - buffer_of said "the seven modes that carry a MinibufferConfig". Eight do, since SavePrompt; seven is the TextEntry::new call-site count, which is the exact confusion repo-map gotcha 11 warns about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
22d43b2b30 |
Merge pull request #23 from fiddlerwoaroof/wave/a-grid-frame
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Browser bundle + static site (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
WU-A: the grid frame on the wire (improvise-anr9, improvise-s1xw absorbed, improvise-1125). row_labels/col_labels become one AxisFrame per axis carrying the five facts gotcha 44 enumerated, so the browser draws multi-level headers, repeat suppression, group spans and collapse indicators instead of Food/Alice. Includes the user's P1: a cell's identity is cats zipped with its tuple, and invalidate_relabelled compared tuples alone — so an axis swap left every CellDisplay and column total standing under a coordinate that no longer named them, and because grid_updates diffs against the framed mirror, the producer then sent nothing at all. A client wrong forever. |
||
|
|
7b04752c4c |
docs(protocol): the record-tier fix left its own stale sibling
The second review pass cleared the code and found that **243b9a9 landed its own instance of the rule this PR enforced in three other files**: it gave `CheckedBatch` a new "two of the three tiers hoist, and the third does not" section and left the sibling paragraph in `frame.rs` — the **protocol-facing** doc, the one a client author reads instead of `improvise-command` — saying the basis is answered once and later commands are simply applied. A client author reading only that concludes their later mutations are unchecked, which is the exact over-generalisation the fix commit was correcting one crate over. Three files grepped, the fourth written fresh and not grepped. It now says what is true: the two document tiers are answered once, the record tier is asked of every mutation, and the exemption is narrower than "later commands are not checked" — a record that *neither exists nor is claimed* is one the frame is creating, and only that pair is exempt. A record that exists is answered for however the claim is written. Also fixed, flagged by both reviewers and still live: `frame.rs` claimed "a frame is applied whole or not at all" fifteen lines above "all or nothing is the contract and is not yet delivered". The first is true of a **refusal** and false of an apply-time failure, which is the documented hole. The qualifier is now on the first, and the last paragraph names itself as the other way a frame can stop half-way. And one line on the `frame_basis` test helper: it always includes a record where `BasisCache::claim` omits an unknown one, so pointed at a record that does not exist it would fabricate `(hash, 0)` — a claim no real client could compose, and exactly the input that flips the create exemption. No current use is wrong; the doc now says to name only records that exist. Doc-only. 1291 passing; clippy, fmt and the wasm gate clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90ea26e7fa |
feat(command): one basis per frame, then the chain runs in order
The user has settled the contract, and it rejects the "at most one document
mutation per frame" rule this branch shipped. Verbatim: *"a sequence of
commands sent to the server should assume that it's played in order from
start to end and that each command sees the state after the previous command
runs"*, sent as *"a single transaction from a given base"*. Four clauses:
1. A frame states **one basis**, describing the base the client composed
against.
2. That basis is checked **once**, against that base, before anything runs.
3. Commands are applied **in order**, and command N sees the state command
N-1 left. Data dependence between them is expected, not an error.
4. **All or nothing.**
This commit lands clauses 1–3 and removes the rejected rule. **Clause 4 is
not implemented** and is documented as an open hole rather than a closed one;
the design note on PR #26 covers what rollback costs.
**Per-command basis checking is not merely awkward, it is incapable**, which
is the finding that makes clause 3 necessary rather than preferable. A client
cannot claim a counter for a record its own chain is about to create, because
`BasisCache::claim` filters to records the cache has been told about — so
`[AddItem("East"), SetCell(East)]`, the archetypal chain, is refused
`RecordGone`/`RecordUnstated` by a per-command check *in any order*. The base
describes what the client **saw**; the chain's internal coherence is the
transaction's job, not the basis check's.
So `CheckedBatch::against` answers the basis on the first command that
carries one and admits the rest through a private
`Checked::within_an_answered_frame`. Private is what stops it being a bypass:
from outside the module the only doors are still `Checked::against` and
`CheckedBatch::against`.
**Two things the rejected rule got wrong, kept as the lesson.** *Arity is not
a property of a variant* — a future `Command::RunScript` is one variant
performing unbounded mutations, so no count over the vocabulary can ever be
right. And *the transaction boundary was never the frame*: because one
command may already be a chain, clause 4 is needed inside a command as much
as across a frame of them, which is why the design note puts the mechanism at
`App::apply_effects`.
`mutates_the_document` is **deleted rather than kept**, which is a deliberate
deviation from the instruction to keep it: with the rule gone it has no
caller, and dead production code behind an `#[allow]` is worse than none. The
reason it was made exhaustive — a `matches!` answers `false` for a future
variant in silence — is preserved in the `CheckedBatch` doc, where clause 4
will read it when the classifier returns.
`Rejection::FrameNotAtomic` goes with the rule; nothing can produce it. The
taxonomies on `Rejection` and `ServerFrame::Rejected` return to two classes
and name the third that arrives with rollback.
Evidence, both faults injected:
- `a_data_dependent_chain_lands_in_order` — `[AddItem("East"), SetCell(East)]`
through a real `Host`. Restoring the per-command check fails it, which is
the whole point. It asserts **both** commands landed, because asserting
only the first would pass against a host that dropped the second.
- `a_frame_whose_later_mutation_fails_is_not_yet_undone` characterizes the
open hole with `[RemoveItem("North"), SetCell(North)]`: the removal lands,
the write resolves to nothing and `model_effects` returns an **empty**
batch, and the frame is reported accepted having applied half of itself.
Both the partial application and the silence are open. **This test inverts
when clause 4 lands**; until then its job is to fail loudly if anyone
believes the hole is closed.
- The worker's `a_partly_refused_batch_…` fixture moved its injected
staleness to the **first** command, since a later one no longer carries an
answer of its own. Removing the injection still fails it.
The hole is unreachable from any client in this workspace — the only producer
of a multi-command frame is `keys::commit`, one mutation among view commands
— which is what makes it safe to document rather than rush.
Merged `main` (
|
||
|
|
cded59acb7 |
docs: the rule widened and its public statement did not follow
`ClientFrame::Commands` — the doc a third transport reads first — still said "at most one `Command::Model`" after the rule became "at most one document mutation", which understates it by exactly the case the previous commit was fixing, and would have led a new client straight into it. Found by grepping the rule's own wording rather than by a test, which is gotcha 48's third paragraph for the third time on this branch: when a fact is stated in more than one place, changing it is a grep, not an edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
120286a44b |
fix(command): a frame carries at most one document mutation
Copilot's second review of PR #26 found that the previous commit fixed the **example** and not the **rule**, and it is right. Refusing only the document-*replacing* case left the general one open, and the general one is worse because it is silent: [RemoveItem("North"), SetCell(the North record)] Both commands pass the pre-frame check. `RemoveItem` applies and deletes the record. `model_effects` then resolves the already-answered `SetCell` against the mutated document, finds nothing, and returns an **empty effect batch**. The frame is reported as accepted having applied only its first command, and the client believes its edit landed. Worse, that is a **regression the batch check introduced**: under the old per-command check the `SetCell` would have been refused with `Rejection::RecordGone`, which is the correct answer. So the rule is now the count rather than a list of dangerous variants: **a frame may carry at most one `Command::Model`**, refused with `Rejection::FrameNotAtomic` (which replaces `DocumentReplacedMidFrame`, and `replaces_the_document` goes with it). **This costs nothing real, and that is the argument for it rather than an excuse.** Two model mutations in one frame could never both be honestly based: `BasisCache::claim` hands back what the server last *published*, and the server publishes between batches and never during one, so the second command's claim necessarily describes a document that stops existing when the first lands. Refusing the pair is that incoherence said out loud. The batch shape that actually exists — `SetSelected` plus the offsets `viewport_effects` returns, optionally around one edit — is untouched, because a view command carries no basis and cannot invalidate one. `Command::SwitchView` is deliberately **not** counted although it moves the document: it carries no basis at all (ADR 001 names that hole and leaves it open) and a record surrogate is view-independent, so a switch cannot invalidate an edit beside it. Counting it would reject useful frames for staleness their own siblings caused — the mistake in the other direction, and the one that made per-command re-checking wrong to begin with. With the count in place, atomicity stops being an argument: at most one command in a frame can be refused, and it is refused before anything runs. Also from the same review, all correct: - `App::batch_aborted` named `Sessions::apply_save`, a function that no longer exists; the consumer is `Host::save`. That matters more than a stale name, because the singleton-batch identity that makes the flag readable at all is only true at *that* call site. - The "two classes" taxonomy on `Rejection` and the "two causes" one on `ServerFrame::Rejected` each sat immediately above an enum that could report a third. Both now enumerate three — staleness, a request the host cannot honour, and a frame that cannot be answered as one unit — and say that the variants rather than the sentence are the list to match. Evidence: `a_frame_carrying_two_document_mutations_is_refused_whole` drives **both** shapes — the structural one Copilot named and the `[LoadModel, TransposeAxes]` one the previous commit handled — because a guard written for one shape is exactly what got this wrong twice (`doctrine(one_arm_of_a_symmetric_rule_is_evidence_about_that_arm_only)`). Two faults injected and watched to fail: disabling the count fails the refusal arm, and widening it to `> 0` fails the **accepting** arm, which drives one mutation surrounded by the view commands a resolved keypress really batches with it. 1206 passing (the baseline moved to 1198 when `main` merged in); clippy, fmt and the wasm gate clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
373965c596 |
fix(protocol): saving is a frame, and a frame may not replace its own basis
Copilot's review of PR #26 found two real defects, both of which this branch
introduced, and both of which are the same mistake in different clothes: an
argument that held for the case I tested and not for the case I did not.
**1. `Command::Save` put a runtime-fallible operation inside a transactional
batch.** `Host::commands` guarantees a frame is applied whole or not at all,
but `apply_save` could still answer `Applied::Rejected { SaveFailed }` from
*inside* that loop — so `[Save, SetCell]` reported a failed write and then
applied the edit anyway, and `[SetCell, Save]` reported "refused" for a
frame that had fully applied. A client reading `ServerFrame::Rejected` could
not tell which. My own doc on `apply_checked` said it "cannot return
`Applied::Rejected`", and that was simply false.
**Saving is now `ClientFrame::Save`, not `Command::Save`.** That is the
honest home for it and I would argue it even without the bug: a `Command` is
something the shared state folds, and saving mutates no shared state and no
client's slice — `reduce_view` had nothing to fold, and `Checked` had no
basis to answer for, so both arms were shims. As a frame it sits beside
`ClientFrame::Viewport`, which is the other session input no `Command`
carries, and **it cannot be composed with a mutation at all**: the batching
question stops being a rule and becomes unconstructible
(`doctrine(an_invariant_is_made_unrepresentable_not_validated)`). The user's
ruling was "add Save to the wire"; it is on the wire, one layer out.
`Host::save` answers both refusals directly, so `Checked` goes back to being
about ADR 001 staleness and nothing else. The behaviour a client sees is
unchanged: `NoDocumentPath` decided before anything runs, `SaveFailed` read
off `App::last_batch_aborted` after a batch that is exactly `[Save]`, both
addressed to the asker alone, success silent.
**2. The documented "residue" was under-stated, and the argument only covered
the command I had tested.** A frame whose earlier command replaces the
document leaves its later ones checked against the outgoing one. I argued
that was harmless because a `SetCell` names its cell by surrogate (ADR 002)
and an outgoing surrogate resolves to nothing in the incoming document. True
— and it covers exactly one command. `TransposeAxes` names no surrogate, so
`[LoadModel, TransposeAxes]` passed every check against the outgoing `App`
and then reshaped a freshly loaded workbook under a basis describing the
previous one: precisely the cross-document mutation the generation tier
exists to reject.
Re-checking the remainder is not available as a fix — it rejects *after* the
load has landed, which is the partial application the batch check exists to
prevent. So `CheckedBatch::against` **refuses the frame whole**, with
`Rejection::DocumentReplacedMidFrame`. `replaces_the_document` is an
exhaustive match beside `records_named`, so a new `WholeWorkbook` variant
fails the build until it classifies itself.
This is the one place here where the *variant* is the right question rather
than "did the thing move" (gotchas 24, 39): a client composing a frame that
**may** invalidate its own basis half way through has composed an incoherent
claim whether or not the load succeeds.
**3. Copilot also caught three stale documents**, two already fixed in
|
||
|
|
7cf6dd78eb |
docs: two comments this branch made false, found by grep rather than by a test
`doctrine(a_fact_stated_twice_is_changed_by_grep)` — gotcha 48's third paragraph, which this branch has now paid for twice. Both of these are arguments that lived beside the code they described and stayed behind when the code moved. `ClientFrame::Commands` still told every future host that the batch is applied in order and abandoned at the first rejection, and named the hole that leaves in ADR 001 as filed rather than closed. That is the *variant's own doc* — the first thing somebody implementing a third transport reads — and it now says the opposite, because the frame is the unit of atomicity. `ServerFrame::Rejected` said a refusal means a moved basis, which is now one of its two causes. `keys.rs` explained that `transpose` deliberately does not mirror the `mark_dirty` its `Cmd` emits, because dirtiness is host-local bookkeeping with no wire variant. There is no `mark_dirty` any more, so the comment described a gap that has stopped existing — the most expensive kind of stale comment, because it reads as a known limitation rather than as history. No behaviour change. 1197 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
301a74821a |
feat(protocol): the grid's frame on the wire, as AxisFrame
`GridCache::row_labels`/`col_labels` were `Vec<String>` built by `GridLayout::row_label`, which slash-joins a multi-level tuple — the exact rendering `two_row_categories_produce_cross_product_labels` asserts must not appear. So the protocol carried a form of the labels the TUI itself considered wrong, and `improvise-wasm-client/src/render.rs` drew precisely that form: the browser showed `Food/Alice` where the TUI showed two header columns, with no groups and no collapse indicators (gotcha 44). Both fields become one `AxisFrame` per axis, carrying the five facts gotcha 44 enumerates: the categories (hence the header levels), the label *tuples* and the inline group headers, the group a position sits in, and each group's collapse state. `record_col` stays off — session-local, keys pending drill edits, correct to keep off. It is one type and not a `row_levels` beside `row_labels`, because two answers to "what are the labels" is the disease (improvise-edp). The rules that used to be written twice are methods on it and are now called from both front ends: `shows_level` (repeat suppression, improvise-jjx's fix), `group_of`, `body_from`, and `header_widths` — the row-header width rule, which existed twice already, inline in `render_grid` and again in `viewport::compute_row_header_width` (improvise-1125). `GridWidget` loses its `&View`: nothing in it reads the view any more. Evidence. `the_whole_frame_is_unchanged` — the exact-buffer characterization over a grid with row groups, column groups, a page axis, multi-level headers and a total row — passes byte for byte, so the TUI is unchanged. The new `tests/grid_frame_agreement.rs` drives the widget into a ratatui buffer and the browser client through a `Snapshot` of the *same* cache, and compares what each draws; re-introducing the slash-join on either side was injected and watched to fail before the test was kept. `tests/web_demo_model.rs::the_web_demo_has_one_category_per_visible_axis` is deleted: it guarded the gap this closes, and a consolidation that keeps its compatibility test is unfinished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c848d93255 |
refactor(protocol): ClientFrame batches, because one keypress is several commands
A cursor key resolved on a client is `SetSelected` plus the scroll
offsets that follow it (`viewport_effects`), and a host that folded the
selection but not the offset projects that client the wrong window. They
must arrive whole, so `ClientFrame::Command(Command)` becomes
`Commands(Vec<Command>)` — the same argument `ServerFrame::Commands`
already carries, and the same conclusion: `Commands(vec![c])` expresses
everything a singleton variant could, so there is one variant, not two.
`ClientFrame::command(c)` is the constructor every existing call site now
spells.
`Host::commands` applies a batch in order and abandons it at the first
rejection: the commands after a refused one were composed against a
picture the document has just contradicted, so applying them invents a
basis nobody claimed. That does leave a hole ADR 001's "never applied,
not even partially" does not have — a refused batch keeps its earlier
commands — which no client in this workspace can reach and which is
stated on the variant rather than hidden.
`the_retired_spellings_do_not_parse` grows the client half. It is the
sharper case of the three: the singleton was this crate's own spelling
until this commit, and `{"Command":{...}}` differs from
`{"Commands":[{...}]}` by a pair of brackets — exactly what a permissive
alias would paper over. Both new host tests had their fault injected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
|
||
|
|
e77176364c |
refactor(protocol): one home for the client/server frame envelopes
Three envelope vocabularies for two deployments become one pair in
`improvise-protocol::frame`: `ClientFrame` / `ServerFrame`.
The question the ticket said to answer first was whether the WORKER and
the SOCKET want the same vocabulary. They do, and each of the three
differences is a date rather than a decision:
- `Viewport` is deployment-independent. `improvise-ws-server::wire`'s own
argument for it never mentions a socket: the extent is not in
`ViewState`, no effect produces it, so no derivation reaches it, and a
worker hosting the same `Sessions` owes `set_extent` the same answer.
It is the handshake for the same reason on both — a `Snapshot` seeds
the VISIBLE cells.
- `Key` is deployment-independent too. A binding names an intent resolved
against a `CmdContext` a thin client does not hold (gotcha 38); that
says nothing about what the bytes travel over. The socket lacked the
variant because key forwarding was not built on it (improvise-bpq2).
- `Commands(Vec<_>)` batches and a singleton `Command(_)` cannot. The
fan-out must arrive whole and in sequence because a `GridUpdate::Cells`
that overtakes its labels is DROPPED (gotcha 33) — a property of the
fold, not of the transport. An in-process `postMessage` stream can
interleave exactly as a socket can.
The worker pair was also incoherent by omission: written before ADR 001,
it can say `Command::Model { basis, .. }` but has no `Rejected`, so a
worker speaking it would let a client compose a claim it could never be
answered on.
The two mirror tests are deleted, which is the measure of success: with
one definition `improvise_ws_server::ClientFrame` and
`improvise_wasm_client`'s are the SAME TYPE, so a rename or an added
variant is a compile error on both sides by construction rather than a
round trip somebody has to remember to extend by hand. What they were
blind to (a variant added on the server and never mirrored) cannot be
expressed. `tests/browser_round_trip.rs` remains and is now the stronger
statement: the wasm client's serialized frame is handed to a real
`Host::handle`.
`a_frame_named_the_other_envelopes_way_does_not_parse` moves to
`frame.rs` as `the_retired_spellings_do_not_parse`, keeping its
injected-fault discipline: the retired `Bootstrap`/`Command` spellings
must not come back as aliases.
`Host::handle` gains an explicit `ClientFrame::Key => Vec::new()` arm.
Behaviour is unchanged — the frame used to fail to parse and be skipped
by `next_client_msg` — but the gap is now a line of code with
improvise-bpq2 on it instead of a variant that does not exist.
The wasm client's duplicated fold surface goes with the envelopes:
`on_message`/`handle_message`/`apply` were `on_frame`/`handle_frame`/
`fold_frame` without the `Rejected` arm. `web/app.js` calls neither.
1132 passing, 0 failed (1134 before: 5 tests deleted, 3 added).
wasm32-unknown-unknown clean for all six bundle crates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
|