name: wai-web wasm32 # The browser sink (wai/wai-web) is the same Rust as the native reference, # compiled to wasm32-unknown-unknown. This job builds it and runs, under node: # # - demo/verify_int_synth_wasm.mjs against the committed conformance corpus # wai/neural-conformance/ (not the demo's staged copies): the wasm32 # int_synth decode must hash to the corpus's frozen native hashes; # - demo/verify_generate_wasm.mjs against wai/wai-rs/tests/vectors/generate/ and # wai/score-conformance/: generateSpectral at every pinned size (sizes.json, # instr-v0.json's object and every object it accepts) and ScoreSink over every # score vector must give the SHA-256 the native `wai generate` and `wai score # render` wrote, so a page checking the browser's output against those pins can; # - demo/verify_menu.mjs: every capability the browser sink advertises is # registered in wai/registry/capabilities.json as a payload or companion # capability, none is a candidate (SPEC §5), and the wasm build's own # registry agrees with the export on every candidate; # - demo/verify_pack.mjs: the browser writers `pack` and `packMulti` refuse # every candidate the export lists, in every place an envelope can name one, # and a manifest that names a member twice in one object; # - demo/verify_int_world_wasm.mjs against wai/wai-rs/tests/vectors/int_world{,2}/: # the WorldModel binding, wai.world.model's integer renderer, must render every # frame of both committed world vectors to the SHA-256 their sidecars state; # - demo/verify_staged_wasm.mjs against the staged-delivery corpus # wai/staged-conformance/: every case replayed through the wasm32 StageSink # must give the reports, final units and stage hashes the native sink wrote; # - demo/verify_measure.sh against the staged-measurement corpus # wai/staged-measure-conformance/: every figure of the wasm32 measurement # binding equals the corpus and the native tool's byte for byte, and the # session claims it seals are verified natively and by the independent # Python verifier with the same verdicts; # - a byte comparison of the demo's staged int_synth copies with the corpus; # - the crate's wasm_tests under wasm-bindgen-test-runner, and its native unit # tests on the host. # # The full-scale int_hyper wasm decode needs vectors regenerated from a # pretrained model; wai-sota-vectors.yml runs it after regenerating them. # One run per branch at a time. A push supersedes the run before it. # # There is one `macos` runner and it is the dev Mac, so every macOS job in this # repo queues on it. With no concurrency group, a session pushing four versions # of a branch queued four complete sets of runs and every one of them had to # drain: on 2026-10-05 a PR's 21 contexts settled none in 70 minutes behind a # backlog of 25 runs, and the queue grew faster than it cleared. Only the newest # commit's verdict is wanted, so the older runs are cancelled. # # Verified on this forge (Forgejo 16.0.5) in `ci-test` before being added here: # the key parses, and a newer push moves an older run to `cancelled` even while # it is still queued, which is the case that matters. # # `cancel-in-progress: true` also applies to `main`. That is deliberate and # safe here: main advances by merge commits whose tree contains the commits # before them, so the newer run covers the older one's changes. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: pull_request: paths: - 'wai/wai-web/**' - 'wai/wai-rs/**' # wai-rs's integer decoders depend on the integer operator set. - 'wai/wai-ops/**' - 'wai/wai-quantum/**' # quantum::tests and quantum_toolchain::tests read the quantum corpora. - 'wai/quantum-conformance/**' - 'wai/neural-conformance/**' - 'wai/staged-conformance/**' - 'wai/staged-measure-conformance/**' - 'wai/int-payload-conformance/**' - 'wai/claim-conformance/**' - 'wai/score-conformance/**' - '.github/workflows/wai-web-wasm.yml' schedule: - cron: '45 5 * * *' jobs: # Does this pull request's own diff touch the paths below? This forge matches a # workflow's `paths:` against the push, so a merge of main into a branch fires the # workflow whatever the pull request changes; every job here waits on this answer. # A push to main, a schedule, or anything the script cannot determine answers # `true` (scripts/pr-touches-paths.py). relevant: name: does this change touch it runs-on: linux-arm64 outputs: run: ${{ steps.gate.outputs.run }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - id: gate run: | set -euo pipefail r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-web-wasm.yml \ "${{ github.event.pull_request.base.sha }}" \ "${{ github.event.pull_request.head.sha }}") echo "this change touches wai-web-wasm: $r" echo "run=$r" >> "$GITHUB_OUTPUT" wasm32: needs: relevant if: needs.relevant.outputs.run == 'true' name: wai-web wasm32 build · corpus, menu and writers runs-on: linux timeout-minutes: 45 steps: - uses: actions/checkout@v5 # The docker runner image ships no Rust. - name: Install Rust 1.98.0 with the wasm32 target shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 rustup target add wasm32-unknown-unknown echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the # crate must match exactly. - name: wasm-bindgen CLI 0.2.127 shell: bash run: cargo install --locked wasm-bindgen-cli --version 0.2.127 - uses: actions/setup-node@v4 with: node-version: '22' - name: Build wai-web for wasm32 (node bindings) shell: bash working-directory: wai/wai-web run: | set -euo pipefail cargo build --locked --release --target wasm32-unknown-unknown wasm-bindgen --target nodejs --out-dir demo/pkg-node --out-name wai_web \ target/wasm32-unknown-unknown/release/wai_web.wasm # The JS surface itself (pick, companionsOf, verifyPin, rendition() and # companion() refusing what is never presented or does not match its digest, # the held-sets conversion, and the WAI2 parse at 32-bit usize) runs only on # wasm32 (`mod wasm_tests`). wasm-bindgen-test-runner, installed above at the # version Cargo.lock pins, runs it under Node. The count check turns a test # that stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web JS surface (wasm32 tests under Node) shell: bash working-directory: wai/wai-web run: | set -euo pipefail CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ cargo test --locked --target wasm32-unknown-unknown 2>&1 | tee "$RUNNER_TEMP/wai-web-wasm.log" grep -q "test result: ok. 4 passed; 0 failed" "$RUNNER_TEMP/wai-web-wasm.log" # The job service's engine in a page (src/quantum_engine.rs, feature # quantum_engine, which only the IDE's bundle carries): a job planned, run and # signed in wasm32 under Node, its receipt checked there and a changed result # refused, every method a tab holds run with no clock and no threads, a # program's critique, equivalence check and variants, and its classical # shortcuts with a contraction search. The count is the four tests above and # its four. - name: The job engine in a page (wasm32 tests under Node) shell: bash working-directory: wai/wai-web run: | set -euo pipefail CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log" grep -q "test result: ok. 8 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log" # Its native half: the service's form and receipt, any angle on the state # vector, what does not run saying why, plan, lint, draw and methods as JSON, # critique, equivalence and variants as JSON, the classical shortcuts, the # workbench run and sealed in the page, and a program's history recorded and # blamed there. - name: The job engine in a page (native unit tests) shell: bash working-directory: wai/wai-web run: | set -euo pipefail cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log" grep -q "test result: ok. 8 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log" # The crate's native unit tests, run on the host: the pure-Rust halves of the # bindings, which wasm_tests does not run. They decode the committed integer # payloads to their sidecars' SHA-256 (int_payload::tests) and check that the # writers `pack` and `packMulti` refuse a payload that is not the format SPEC §5 # pins its integer capability to, and a manifest that names a member twice (tests); the # staged-delivery binding's pure half replays the staged corpus and reads a layer's # header and cost (stage::tests); the measurement binding's pure half evaluates the # staged-measurement corpus to its goldens and seals a page-recorded session equal # to the native tool's (measure::tests). The quantum sink opens every file of # both quantum corpora to their manifests' hashes and capability and refuses # every reject file with its listed class (quantum::tests), and the quantum IDE # builds its catalog circuits in its gateset to the same manifests, stores no # parameter on a gate other than P, and shows no hash for an invalid circuit; # its import reads a program as the job service does and refuses one that # measures and then acts, its replies stay valid JSON whatever an error says, # and it signs with the page's own key once given one (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as # an estimate, never a reading (video_ott::tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests shell: bash working-directory: wai/wai-web run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" grep -q "test result: ok. 35 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web run: node demo/verify_int_synth_wasm.mjs ./pkg-node/wai_web.js ../neural-conformance # decodeWiv1, decodeWih1, decodeWia1 and decodeWis1 on the committed integer # payloads give the SHA-256 their sidecars state, and a JSON form is refused. - name: wasm32 integer payload decode equals the native sidecars shell: bash working-directory: wai/wai-web run: node demo/verify_int_payload_wasm.mjs ./pkg-node/wai_web.js ../wai-rs/tests/vectors # WorldModel, wai.world.model's integer renderer (integer-operators §13.5, # informative), renders each committed world vector, one agent and two, frame by # frame, to the SHA-256 its sidecar states for every frame and for the clip. - name: wasm32 integer world renderer equals the native sidecars shell: bash working-directory: wai/wai-web run: node demo/verify_int_world_wasm.mjs ./pkg-node/wai_web.js ../wai-rs/tests/vectors # The generator and the score synth in wasm32 against the native pins: every size # sizes.json and instr-v0.json pin (the object's four listed sizes, and the first # listed size of each object at a bound of form v0), and every score vector's # pcm_sha256, mixdown hash and score hash. Pages that make these pieces in the # browser hold their output to such pins, made natively. - name: wasm32 generator and score synth equal the native pins shell: bash working-directory: wai/wai-web run: node demo/verify_generate_wasm.mjs ./pkg-node/wai_web.js ../wai-rs/tests/vectors/generate ../score-conformance # Every staged-delivery case replayed through the wasm32 StageSink: each integer # base decoded in wasm32 to its stage0.bin with its pin, and every report, final # unit, stage hash and clip hash equal to what the native sink wrote in case.json. # Then the binding's guards: bases that declare huge clips refused under a cost # limit with linear memory checked, a base past the chain budget, base facts a # page cannot raise (fallback, tier, claimed), addBase spending its base, and a # seeded fuzz that must never trap. - name: wasm32 staged-delivery sink equals the native corpus shell: bash working-directory: wai/wai-web run: node demo/verify_staged_wasm.mjs ./pkg-node/wai_web.js ../staged-conformance # The staged-measurement binding (src/measure.rs) over wai/staged-measure-conformance/: # every fixture's evaluation, the refusal titles, the counted objects, the quality # pairs and every simulator case equal the corpus, and the evaluations equal the native # `wai_stage_measure eval --json` byte for byte; every session the wasm32 build seals # is the native tool's claim, commits to the golden output tree, and gets the same # verdicts from the wasm32 verifier, `wai_claim_vectors verdicts` and claim_verify.py. # It uses the package built above; a missing artifact fails the step. - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install the Ed25519 library (claim_verify.py) shell: bash run: python -m pip install cryptography - name: wasm32 staged measurement equals the native tool and the corpus shell: bash working-directory: wai/wai-web run: bash demo/verify_measure.sh ./demo/pkg-node/wai_web.js - name: The browser menu is registered and names no candidate shell: bash working-directory: wai/wai-web run: node demo/verify_menu.mjs ./pkg-node/wai_web.js ../registry/capabilities.json - name: The browser writers refuse every candidate shell: bash working-directory: wai/wai-web run: node demo/verify_pack.mjs ./pkg-node/wai_web.js ../registry/capabilities.json - name: The demo's staged int_synth vectors are byte copies of the corpus shell: bash working-directory: wai run: | set -euo pipefail cmp wai-web/demo/int_synth/expected_hashes.json neural-conformance/expected_hashes.json for n in gradient bloom facade photo; do for f in latent.bin int_synth.json; do cmp "wai-web/demo/int_synth/$n/$f" "neural-conformance/vectors/$n/$f" done done