bdce1636bb
improvise-9x6: the server half of the standalone static-web deployment (epic improvise-tm6). `improvise-worker-server` hosts the authoritative `App`, the command pipeline, the projection layer and VFS persistence behind `self.onmessage`/`self.postMessage`. It is small because everything it needs already existed: the frames are `improvise_protocol::frame`'s (improvise-xks6), the session loop is `Host` (previous commit), the basis check is `Sessions::apply`/`Checked` (improvise-ondj), and persistence is `Storage` + `OpfsFs` (improvise-6mq, i34). What is genuinely new is three things. `vfs.rs` — how a document gets INTO a browser's empty filesystem. Loading is unchanged: it is `persistence::load_from` on a `Storage`, the same call the native binary makes. Seeding is the new problem, and it is a `Storage::write`, so a fetched demo, an upload and a saved document take the same parse path and a seed never clobbers an edit. `list` is the document picker a browser has no OS file manager for. `worker.rs` — the loop: one port, one client, the `Viewport` handshake (dropped rather than closed, since there is no connection to close), and the filter from `Addressed`. Every decision is here and every test points at it. `bindings.rs` — `cfg(wasm32)`, decision-free, and the only part no test here can reach: it awaits `OpfsFs::open`/`attach`/`refresh`/`commit_deletes` and forwards strings. Two gaps found and reported rather than papered over. No client on any transport can ask its host to save — `effect::Save` is `ViewOnly`, so the wire vocabulary derived from the model effects cannot name it — and a wire `ModelCommand::SetCell` never sets `model_state.dirty`, because dirtiness is raised by a separate `MarkDirty` effect the key path emits. So the worker persists on its own behalf, keyed off the command variant rather than off a flag that is never set; `touches_document` carries the argument. `ClientFrame::Key` is still answered with nothing. A worker CAN call `reduce_key` — it holds the `App` — but nothing in the projection stream carries the host's own `ViewState`, so a half-wired arm diverges rather than under-delivers. `a_key_resolved_on_the_host_moves_state_no_client_can_hear` demonstrates it against a real `App` and `Sessions` (improvise-bpq2). 22 tests, all host-side: 16 in the crate and 6 in `tests/post_message.rs`, which drives the worker with nothing but the JSON a `MessagePort` carries. Bundle: 1,334,892 bytes raw, 496,965 gzipped — 485 KB against a 3 MB budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
172 lines
7.3 KiB
YAML
172 lines
7.3 KiB
YAML
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-protocol
|
|
run: cargo check -p improvise-protocol --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
|
|
# The browser client itself. This is the one crate for which a wasm
|
|
# build is the *point* rather than a property worth keeping, so a
|
|
# failure here is not a portability regression but a broken deliverable
|
|
# (improvise-gsw).
|
|
- name: improvise-wasm-client
|
|
run: cargo check -p improvise-wasm-client --target wasm32-unknown-unknown
|
|
# The worker half of the standalone deployment (improvise-9x6). Same
|
|
# terms as the client: a wasm build is the point, so a failure here is a
|
|
# broken deliverable rather than a portability regression.
|
|
- name: improvise-worker-server
|
|
run: cargo check -p improvise-worker-server --target wasm32-unknown-unknown
|
|
# Not in the browser bundle yet — nothing depends on improvise-gp. Gated
|
|
# anyway because it is a candidate (fitting a tree in the browser) and
|
|
# staying wasm-clean is free now, expensive to retrofit (improvise-z5eg).
|
|
- name: improvise-gp
|
|
run: cargo check -p improvise-gp --target wasm32-unknown-unknown
|
|
|
|
kb:
|
|
name: Knowledge base consistency
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: cachix/install-nix-action@v27
|
|
# kb.pl is executable knowledge, so it gets checked like code: rules
|
|
# must reject what they should, domains must close, and functional keys
|
|
# must not carry conflicting values. See kb_report/0 in the file.
|
|
- name: kb_consistent
|
|
run: |
|
|
nix shell nixpkgs#trealla --command tpl prolog-sessions/kb.pl \
|
|
-g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GitHub Pages delivery (improvise-d31).
|
|
#
|
|
# Two jobs on purpose. `pages-build` runs on every push and pull request, so
|
|
# a change that breaks `web/build.sh` or the bundle fails on the PR that made
|
|
# it — the build is a gate. `pages-deploy` runs only on a push to main and
|
|
# `needs` the two check jobs above, so nothing that failed `cargo test` or the
|
|
# wasm32 gate can reach the published site. The deploy depends on those checks
|
|
# rather than repeating them (improvise-d31 AC 2).
|
|
#
|
|
# Not path-filtered, deliberately. The bundles are `improvise-wasm-client` and
|
|
# `improvise-worker-server` plus, between them, most of the workspace — the
|
|
# worker bundle is the whole App: core, formula, io, protocol and command. A
|
|
# filter
|
|
# that meant "skip when the bundle cannot have changed" would have to name
|
|
# most of the workspace, and the failure mode of getting it wrong is a stale
|
|
# site with no signal that it is stale. A redundant build is the cheaper error.
|
|
pages-build:
|
|
name: Browser bundle + static site
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
- uses: Swatinem/rust-cache@v2
|
|
# Pinned release binary rather than `cargo install` (a three-minute
|
|
# source build) or a third-party action. wasm-pack runs its own bundled
|
|
# `wasm-opt`, which is why improvise-wasm-client's Cargo.toml names
|
|
# `--enable-bulk-memory`: without it the optimizer rejects the
|
|
# `memory.copy` LLVM emits regardless.
|
|
# v0.15.0 is what `nix develop` supplies, and the bundle was built and
|
|
# measured against it locally (346 KB, 120 KB gzipped). Pinning the same
|
|
# version is what makes that local run evidence about this one.
|
|
- name: Install wasm-pack
|
|
env:
|
|
WASM_PACK_VERSION: v0.15.0
|
|
run: |
|
|
set -eu
|
|
url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
|
curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \
|
|
"wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack"
|
|
wasm-pack --version
|
|
|
|
# The same script a developer runs locally, so there is one answer to
|
|
# "how is the bundle built" rather than two that can drift.
|
|
- name: Build the wasm bundle
|
|
run: ./web/build.sh
|
|
|
|
# `web/` is the source directory: it also holds build.sh and .gitignore,
|
|
# which have no business being served. Assemble what ships.
|
|
- name: Assemble the site
|
|
run: |
|
|
set -eu
|
|
mkdir -p site
|
|
cp web/index.html web/app.js web/style.css web/worker.js site/
|
|
cp -rf web/pkg site/pkg
|
|
cp -rf web/worker-pkg site/worker-pkg
|
|
rm -f site/pkg/.gitignore site/pkg/package.json
|
|
rm -f site/worker-pkg/.gitignore site/worker-pkg/package.json
|
|
find site -type f | sort
|
|
du -sh site
|
|
|
|
- uses: actions/upload-pages-artifact@v3
|
|
with:
|
|
path: site
|
|
|
|
pages-deploy:
|
|
name: Deploy to GitHub Pages
|
|
needs: [test, wasm, pages-build]
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pages: write
|
|
id-token: write
|
|
# A superseded deploy is allowed to finish rather than being cancelled:
|
|
# `deploy-pages` cancelled midway can leave the Pages deployment in a
|
|
# half-applied state, and the next push republishes anyway.
|
|
concurrency:
|
|
group: pages
|
|
cancel-in-progress: false
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
steps:
|
|
# `enablement: true` asks the API to turn Pages on with source =
|
|
# GitHub Actions. It succeeds only if the repository allows the workflow
|
|
# token to do so; if it does not, this step is where the run fails and
|
|
# the fix is the repository setting, not this file (improvise-3gy).
|
|
- uses: actions/configure-pages@v5
|
|
with:
|
|
enablement: true
|
|
- id: deployment
|
|
uses: actions/deploy-pages@v4
|