Verified clean by compiling for wasm32-unknown-unknown before adding.
Not in the browser bundle yet, but a candidate, and staying clean is free
now and expensive to retrofit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
The crate the browser epic hangs off: Command (the wire format),
ViewState, RenderCache/CellDisplay/Snapshot, and reduce_view. It depends
on improvise-core and serde and nothing else -- no IO, no command layer,
no terminal -- and improvise-command now depends on it.
ViewState, ViewCursor, ViewFrame, AppMode, Panel, MinibufferConfig,
MinibufColor, HELP_PAGE_COUNT and the key types move here. Everything
that moved is re-exported from ui::app and command::key, so no existing
path changed. The three tickets before this one are what made the move
mechanical: h0b evicted the unportable fields, a2x added the derives,
8uz took the cursor out of the shared View.
Command's client->server half is not invented. It is exactly the set of
EffectKind::ViewOnly effects whose target is the portable slice -- h0b
had already sorted the portable state from the host-local, so the
enumeration was done. Effects touching the wizard, staged drill edits,
storage or the layout cache are absent by construction. The other half
carries projections: already-formatted display values and their labels.
One thing had to be added rather than moved. reduce_view runs on a client
that holds no Workbook, and ViewState::cursors is keyed by view name, so
ViewState needs its own active_view. That makes two copies of "which view
is active"; App::switch_to_view and App::install_workbook are the only
writers, and the_two_active_view_names_never_diverge guards it. The 8uz
characterization test caught the desync the moment it appeared, because
it was calling workbook.switch_view directly.
Whether the two are *allowed* to diverge -- two clients on different
views -- is improvise-g0f, deliberately left open. It changes the shape
of improvise-cqq, and nothing here forecloses either answer.
reduce_view has 12 tests including a determinism check: folding the same
stream twice gives the same state, which is what stops the server's
mirror drifting from the client it mirrors. Every Command round-trips
through JSON. The crate is in the CI wasm job.
Closes improvise-cqi
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn
kb_report/0 and kb_consistent/0 enumerate kb_error/1 solutions across four
classes: domain integrity (layers, concerns and dependency edges name real
crates), layering (no inverted crate dependency), rule soundness (rules must
REJECT a sentinel drawn from no declared domain), and functional keys (a
predicate declared functional/1 must not carry conflicting values for one
key), plus cross-references between recorded work.
Wired into CI as a third job, since kb.pl is executable knowledge and
deserves checking like code.
Proving the checker fires found two bugs in it. depends_transitively/2 was a
naive transitive closure, so illegal_dependency/2 — whose entire purpose is
detecting an inverted edge — looped forever on exactly that input; it now
carries a visited set. And the :- discontiguous declarations added in the
previous commit are a syntax error in trealla, which wants the parenthesized
form; the MCP server had silently tolerated them, so validating kb.pl through
the session alone had hidden it.
Every check class is now verified by injecting the fault it targets and
confirming it is reported, then reverting. A consistency check that has only
ever said "consistent" has demonstrated nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn
The audit could not start because the Nix toolchain shipped no wasm32 std and
there is no rustup here. Adding targets = ["wasm32-unknown-unknown"] to the
flake's rustToolchain was the whole blocker: all four bundle crates —
improvise-core, improvise-formula, improvise-io, improvise-command — then
check clean on the first try with no feature flags and no source changes.
The root improvise crate does not build for wasm because crossterm does not.
That is correct: it is the terminal binary, not part of the browser bundle,
so the CI job names the four bundle crates rather than checking the workspace.
Adds .github/workflows/ci.yml with that wasm job plus a host job running fmt,
clippy -D warnings and the test suite — there was no test or lint CI before,
only cargo-dist's release workflow.
A check gate cannot catch what merely panics at runtime, and the audit's real
finding was that two of the ticket's six named hazards are real while four are
not. Instant::now() panics on wasm yet compiles, so improvise-command now uses
web-time, which re-exports std::time off-wasm. std::fs likewise compiles and
then fails in a browser; StartImportWizard and LoadModel still bypass the
Storage trait, filed as improvise-fuj because the fix needs a decision on how
a wasm build injects its backend.
Verified by compilation on both targets and by the native suite. Nothing here
has been run on a wasm host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn