diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..529790ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test and lint (host) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo clippy --workspace --all-targets -- -D warnings + - run: cargo test --workspace + + wasm: + name: wasm32 check (browser bundle crates) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + # Only the crates that go into the browser bundle. The root `improvise` + # crate is the terminal binary and depends on crossterm, which does not + # build for wasm — that is correct, not a gap (improvise-ywd). + - name: improvise-core + run: cargo check -p improvise-core --target wasm32-unknown-unknown + - name: improvise-formula + run: cargo check -p improvise-formula --target wasm32-unknown-unknown + - name: improvise-io + run: cargo check -p improvise-io --target wasm32-unknown-unknown + - name: improvise-command + run: cargo check -p improvise-command --target wasm32-unknown-unknown diff --git a/Cargo.lock b/Cargo.lock index 67077212..3ca15bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,6 +798,7 @@ dependencies = [ "serde_json", "tempfile", "unicode-width", + "web-time", ] [[package]] @@ -2115,6 +2116,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wezterm-bidi" version = "0.2.3" diff --git a/context/repo-map.md b/context/repo-map.md index 33e69806..474187d5 100644 --- a/context/repo-map.md +++ b/context/repo-map.md @@ -249,6 +249,20 @@ docs/design-notes.md Product vision & non-goals --- +## Wasm + +The browser bundle is `improvise-core`, `improvise-formula`, `improvise-io` +and `improvise-command`. All four `cargo check --target +wasm32-unknown-unknown` clean with no feature flags (improvise-ywd), gated by +the `wasm` job in `.github/workflows/ci.yml`. The root `improvise` crate is +the terminal binary and does **not** build for wasm — it depends on +crossterm; that is correct, not a gap. + +`cargo check` passing is not the same as running. Two known runtime hazards +it cannot see: `Instant::now()` panics on wasm (hence `web-time` in +`improvise-command`), and any `std::fs` call compiles but fails in a browser +— see improvise-fuj for the two effects that still bypass `Storage`. + ## CLI ``` diff --git a/crates/improvise-command/Cargo.toml b/crates/improvise-command/Cargo.toml index 179c3ae2..e5bd95c6 100644 --- a/crates/improvise-command/Cargo.toml +++ b/crates/improvise-command/Cargo.toml @@ -13,6 +13,9 @@ improvise-io = { path = "../improvise-io" } anyhow = "1" serde_json = "1" unicode-width = "^0.2" +# Instant::now() panics on wasm32-unknown-unknown; web_time re-exports +# std::time off-wasm and uses performance.now() on it (improvise-ywd). +web-time = "1" [dev-dependencies] tempfile = "3" diff --git a/crates/improvise-command/src/ui/app.rs b/crates/improvise-command/src/ui/app.rs index 3e42894b..f558d117 100644 --- a/crates/improvise-command/src/ui/app.rs +++ b/crates/improvise-command/src/ui/app.rs @@ -3,7 +3,10 @@ use anyhow::Result; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Duration; +// Not std::time::Instant: that panics on wasm32-unknown-unknown, and this +// crate is part of the browser bundle (improvise-ywd). +use web_time::Instant; use std::rc::Rc; diff --git a/flake.nix b/flake.nix index 217e77fa..e34b292e 100644 --- a/flake.nix +++ b/flake.nix @@ -25,6 +25,10 @@ rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = ["rust-src" "clippy" "rustfmt" "llvm-tools-preview"]; + # The browser deployment targets wasm; without this, the wasm audit + # (improvise-ywd) and the OPFS backend (improvise-i34) cannot even be + # checked, let alone fixed. + targets = ["wasm32-unknown-unknown"]; }; cargoNix = import ./Cargo.nix { diff --git a/prolog-sessions/kb.pl b/prolog-sessions/kb.pl index f27afb7a..1a08f837 100644 --- a/prolog-sessions/kb.pl +++ b/prolog-sessions/kb.pl @@ -188,3 +188,21 @@ invariant(prefix_is_an_address,'ViewState::transient_prefix stores the (KeyCode, decision('improvise-jc8',flip_the_arc,'Arc moved out of ViewState and back into KeymapSet, which already owns every keymap. Motivated by a hard dependency cycle -- Keymap is in improvise-command, which will depend on improvise-protocol once reduce_full lives there -- not by serde.'). work_done('improvise-jc8',refactor,'ViewState no longer holds Arc; four characterization tests written before the change pinned gg/ZZ/prefix-consumption/records-inheritance, plus two new guards for flatness and parent resolution.'). location(prefix_resolution,'crates/improvise-command/src/command/keymap.rs','KeymapSet::prefix_keymap, SetTransientPrefix'). + +% ── Wasm compatibility (improvise-ywd, audited 2026-08-30) ────────────────── +wasm_bundle_crate('improvise-core'). +wasm_bundle_crate('improvise-formula'). +wasm_bundle_crate('improvise-io'). +wasm_bundle_crate('improvise-command'). +not_in_wasm_bundle('improvise', 'terminal binary; depends on crossterm'). +wasm_checks_clean(all_bundle_crates, no_feature_flags_needed). +% Hazards a compile check cannot see. +wasm_runtime_hazard(instant_now, 'panics on wasm32-unknown-unknown', fixed_with(web_time)). +wasm_runtime_hazard(std_fs, 'compiles on wasm but fails in a browser', tracked_as('improvise-fuj')). +wasm_non_hazard(dirs, 'declared in root Cargo.toml but used nowhere'). +wasm_non_hazard(chrono_local_now, 'never called'). +wasm_non_hazard(std_env, 'only in a test'). +wasm_non_hazard(flate2, 'compiles for wasm as-is'). +invariant(compile_check_is_not_runtime_proof,'the wasm CI gate is cargo check, so it catches nothing that merely panics at runtime: Instant and std::fs both COMPILE for wasm32-unknown-unknown'). +work_done('improvise-ywd',audit,'added wasm32-unknown-unknown to the flake toolchain, which was the real blocker; all four bundle crates then checked clean with zero source changes. Swapped Instant for web_time and added the CI wasm job.'). +location(wasm_ci,'.github/workflows/ci.yml','wasm job').