wai-quantum for JavaScript: the engine as the npm package @transaction-science/wai-quantum 0.1.0 #534

Merged
dcharlot merged 1 commit from wai/quantum-npm-package into main 2026-10-29 09:20:31 -04:00
9 changed files with 467 additions and 0 deletions

View file

@ -0,0 +1,77 @@
name: wai-quantum for JavaScript, release tarball
# On a tag wai-quantum-cli-v<version> (the command line's release), builds the npm
# package @transaction-science/wai-quantum (wai/wai-quantum-js) and attaches its
# tarball to the same forge release, with its SHA-256. ci/check.sh builds it, holds
# the tarball to the package's allow-list, runs the smoke test on it unpacked and the
# README's Node example on it installed, before it is attached. Nothing is published
# to a package registry: the tarball is published from the release by hand.
#
# Uploads go to the forge directly over the private network (FORGE_API), never through
# the public tunnel.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
tags: ['wai-quantum-cli-v*']
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
FORGE_API: http://100.64.224.0:3400/api/v1
FORGE_REPO: Transaction-Science/open-standards
jobs:
version:
name: the tag is the command line's version
runs-on: linux
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v5
- id: v
shell: bash
run: |
set -euo pipefail
v="${GITHUB_REF_NAME#wai-quantum-cli-v}"
crate="$(sed -n 's/^version = "\(.*\)"$/\1/p' wai/wai-quantum-cli/Cargo.toml | head -1)"
[ "$v" = "$crate" ] || { echo "::error::the tag says $v, wai-quantum-cli/Cargo.toml says $crate"; exit 1; }
echo "the npm package: $(python3 -I -c 'import json; print(json.load(open("wai/wai-quantum-js/package.json"))["version"])')"
echo "version=$v" >> "$GITHUB_OUTPUT"
tarball:
name: the npm tarball · linux
needs: version
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- 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"
- 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, check and hash
shell: bash
run: bash wai/wai-quantum-js/ci/check.sh "$RUNNER_TEMP/dist"
- name: Attach to the release
shell: bash
env:
FORGE_TOKEN: ${{ github.token }}
run: bash wai/wai-quantum-cli/release/publish.sh "$GITHUB_REF_NAME" "wai-quantum ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*.tgz "$RUNNER_TEMP"/dist/*.tgz.sha256

81
.github/workflows/wai-quantum-js.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: wai-quantum for JavaScript
# The npm package @transaction-science/wai-quantum (wai/wai-quantum-js): the job
# service's engine for WebAssembly (wai-web, feature quantum_engine) with its
# bindings. ci/check.sh builds and packs it; the tarball, unpacked, holds the
# package's allow-list and nothing else; the smoke test runs against what was
# unpacked (a job run with a fresh key and its receipt verified, a forged result
# refused, a program linted and drawn, a refusal with its diagnostic code); and the
# tarball installed into an empty app runs the README's Node example by name.
# Nothing is published.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths:
- 'wai/wai-quantum-js/**'
- 'wai/wai-web/**'
- 'wai/wai-quantum/**'
- 'wai/wai-rs/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-js.yml'
pull_request:
paths:
- 'wai/wai-quantum-js/**'
- 'wai/wai-web/**'
- 'wai/wai-quantum/**'
- 'wai/wai-rs/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-js.yml'
jobs:
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-quantum-js.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches the npm package: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
js-linux:
name: javascript · linux
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- 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; build.sh refuses another.
- 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, pack, unpack, smoke-test and install the package
shell: bash
run: bash wai/wai-quantum-js/ci/check.sh "$RUNNER_TEMP/npm"

View file

@ -6,6 +6,19 @@ wire format may change and such changes are recorded here.
## Unreleased
### wai-quantum for JavaScript: the engine as an npm package
- **`@transaction-science/wai-quantum` 0.1.0** (`wai/wai-quantum-js/`): the job service's engine for WebAssembly
(`wai-web` with its `quantum_engine` feature, carrying the engine of `wai-quantum` 0.7.0) and its bindings, an ES
module with its TypeScript declarations and no dependencies, for a page or for Node. `build.sh` builds it with the
paths remapped and refuses a local path left in what it ships, and packs it. The package keeps its own version line,
as the Python package does.
- **CI:** `wai-quantum-js.yml` builds and packs it, holds the tarball to the package's allow-list, runs
`test/smoke.mjs` on it unpacked (a job run with a fresh key and its receipt verified, a forged result refused, a
program linted and drawn, a refusal with its code) and the README's Node example on it installed by name.
`wai-quantum-js-release.yml` does the same on a `wai-quantum-cli-v*` tag and attaches the tarball and its SHA-256
to the release. Nothing is published to a registry from CI.
### wai-quantum-cli 0.2.2: the registry's line shown as text
This release changes no code. 0.2.1 put the `mcp-name` line in an HTML comment, but the MCP Registry reads the

View file

@ -0,0 +1,87 @@
# @transaction-science/wai-quantum
The WAI quantum job service's engine in WebAssembly, for a page or for Node. A job is planned, run and sealed here exactly as the service plans, runs and seals it. The answer is the service's JSON, signed with a key you hold, and any job's receipt, the service's included, is checked here. Programs are linted, drawn and critiqued before they run.
This is version 0.1.0 of the package. It carries the engine of `wai-quantum` 0.7.0, built from `wai-web` with its `quantum_engine` feature. The package keeps its own version line, as the Python package does. The module also holds the rest of `wai-web`'s browser surface (reading WAI envelopes among it); this page is about the quantum engine.
## Install
```sh
npm install @transaction-science/wai-quantum
```
It is an ES module with its TypeScript declarations, and has no dependencies.
## In a page
`init()` fetches `wai_quantum_bg.wasm` from beside the module. With a bundler that does not serve it there, pass its URL: `init({ module_or_path: url })`.
```js
import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum';
await init();
const engine = new QuantumEngine(crypto.getRandomValues(new Uint8Array(32)), 256);
const job = JSON.parse(engine.run(JSON.stringify({
qasm: 'OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;',
shots: 1000,
})));
console.log(job.method, job.result.counts);
console.log(JSON.parse(engine_verify(JSON.stringify(job))).valid); // true
```
## In Node
Node is given the WebAssembly's bytes:
```js
import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { randomBytes } from 'node:crypto';
const wasm = createRequire(import.meta.url).resolve('@transaction-science/wai-quantum/wai_quantum_bg.wasm');
await init({ module_or_path: await readFile(wasm) });
const engine = new QuantumEngine(randomBytes(32), 256);
const job = JSON.parse(engine.run(JSON.stringify({ qasm: 'OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c = measure q;', shots: 100 })));
console.log(job.result.counts, JSON.parse(engine_verify(JSON.stringify(job))).signer);
```
## What it offers
Requests and answers are JSON strings, the job service's own. A call that cannot be answered returns `{"error": …}`; a program the engine refuses also gives its diagnostic `code` (such as `Q310`) and its `meaning`.
**`new QuantumEngine(seed, memory_mb)`**: an engine that signs with the Ed25519 key whose 32-byte seed is `seed`, and admits a job of at most `memory_mb` MiB. Keep the seed to keep the key.
- `signer_did()`: the key the receipts name, as a `did:key`.
- `run(request)`: a job run and sealed. The answer holds `status`, `method`, `why`, `request` and `result`, with the result's `receipt`. The request is the service's (`qasm`, `shots`, `seed`, `method`, `observables`, `amplitudes`, `noise`, `intent`, `verdict`, `target`, `compile`, `mitigate` and more).
- `plan(request)`: the method a job would run on and why, its memory and work, without running it.
- `compile(request)`: a program compiled (`{"qasm", "epsilon", "level", "basis"}`), with a certificate of every pass, sealed.
- `estimate(request)`: what a fault-tolerant machine would need to run a program, each estimate sealed.
- `qec(op, args)`: error correction (`code`, `decode` or `threshold`), the answer sealed.
- `methods()`: each method, its accuracy, and the most qubits it holds here.
**Functions:**
- `engine_verify(job)`: whether a job's receipt seals its request and result: `{"valid", "key", "signer"}`. Any change to either after sealing makes it false.
- `engine_verdict(job, intent, memory_mb)`: a job's verdict against its intent (or the one given).
- `engine_estimate_verify(answer)`: an estimate's seal, its receipts, and its request re-run to the same figures.
- `engine_lint(src)`: a program read as the engine reads it: `{"ok": true, "qubits", "gates", …}`, or `{"ok": false, "error", "line", "code"}`.
- `engine_draw(src, width)`: the circuit as text (`{"text"}`); `engine_layout(src)` gives it as data for a page to draw.
- `engine_critique(src)`: what the engine can say about a program before it runs: its measures and its smells, each with its lines.
- `engine_equivalent(a, b)`: whether two programs apply the same gates up to a global phase, and how that was decided.
- `engine_repair(src, options)`: a program repaired on the lines its diagnostics flag.
- `engine_shortcuts(src, thorough)`: every exact classical route to simulating a program, with its cost.
- `engine_diagnostic_codes()`: every diagnostic code the reader uses, with its meaning.
- `engine_course(locale)`, and the course's lessons, checks and hints: the circuits course, checked by the engine.
The declarations (`wai_quantum.d.ts`) list every export.
A run in a page or in Node does not meter its energy, and its result says so.
## Elsewhere
The same engine runs as the command line (`wai-quantum`), as the Python package `wai-quantum`, and as the job service, and a job's receipt verifies in each. The source, its schemas and the versioning policy are at <https://git.transaction.science/Transaction-Science/open-standards> under `wai/`; Quantum Studio, an editor built on this package, is at <https://wai.transaction.science/quantum-studio/>.
## Licence
Apache-2.0.

54
wai/wai-quantum-js/build.sh Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Build the npm package @transaction-science/wai-quantum: the quantum job service's
# engine for WebAssembly (wai/wai-web with its quantum_engine feature), its JavaScript
# bindings, and this folder's package.json and README, packed as one tarball:
#
# bash wai/wai-quantum-js/build.sh OUT_DIR
#
# Writes OUT_DIR/package/ (the package's files) and OUT_DIR/<name>-<version>.tgz, and
# prints the tarball's SHA-256. It needs Rust with the wasm32-unknown-unknown target,
# wasm-bindgen 0.2.127 (the version wai-web's lock file pins) and npm. Paths are
# remapped out of the binary, and the build is refused if a local path is left in the
# files it ships.
set -euo pipefail
if [ $# -ne 1 ]; then
echo "usage: bash wai/wai-quantum-js/build.sh OUT_DIR" >&2
exit 2
fi
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
web="$root/wai/wai-web"
mkdir -p "$1"
out=$(cd "$1" && pwd)
sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"; else shasum -a 256 "$1"; fi; }
want=$(sed -n '/^name = "wasm-bindgen"$/{n;s/^version = "\(.*\)"$/\1/p;}' "$web/Cargo.lock")
have=$(wasm-bindgen --version | cut -d' ' -f2)
[ "$have" = "$want" ] || { echo "build.sh: wasm-bindgen is $have; wai-web's lock file pins $want" >&2; exit 1; }
target=${CARGO_TARGET_DIR:-"$web/target"}
sysroot=$(cd "$web" && rustc --print sysroot)
commit=$(cd "$web" && rustc -vV | sed -n 's/^commit-hash: //p')
export RUSTFLAGS="--remap-path-prefix=$root=/open-standards --remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$sysroot/lib/rustlib/src/rust=/rustc/$commit"
(cd "$web" && cargo build --locked --release --target wasm32-unknown-unknown --features quantum_engine)
pkg="$out/package"
rm -rf "$pkg"
mkdir -p "$pkg"
wasm-bindgen --target web --out-dir "$pkg" --out-name wai_quantum "$target/wasm32-unknown-unknown/release/wai_web.wasm"
cp "$here/package.json" "$here/README.md" "$pkg/"
cp "$root/wai/LICENSE" "$pkg/LICENSE"
# Nothing the package ships names the machine that built it.
for leak in "$HOME" /Users/ /home/ /Volumes/ /private/; do
if grep -r -q -a -F "$leak" "$pkg"/wai_quantum*; then
echo "build.sh: '$leak' is in the package's bindings or binary" >&2
exit 1
fi
done
rm -f "$out"/*.tgz
npm pack "$pkg" --pack-destination "$out" --silent > /dev/null
tgz=$(ls "$out"/*.tgz)
echo "wasm: $(wc -c < "$pkg/wai_quantum_bg.wasm" | tr -d ' ') bytes"
sha256 "$tgz"

39
wai/wai-quantum-js/ci/check.sh Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# The npm package, built and checked as an installer would meet it:
#
# bash wai/wai-quantum-js/ci/check.sh OUT_DIR
#
# 1. build.sh builds and packs it into OUT_DIR;
# 2. the tarball, unpacked, holds the package's allow-list and nothing else;
# 3. test/smoke.mjs runs against the unpacked package;
# 4. the tarball is installed into an empty app (npm install <tgz>, nothing else to
# fetch), and test/install.mjs, the README's Node example, runs there by name.
# Leaves the tarball in OUT_DIR, with OUT_DIR/<tarball>.sha256.
set -euo pipefail
here=$(cd "$(dirname "$0")/.." && pwd)
mkdir -p "$1"
out=$(cd "$1" && pwd)
bash "$here/build.sh" "$out"
tgz=$(ls "$out"/*.tgz)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir "$work/unpacked"
tar -xzf "$tgz" -C "$work/unpacked"
have=$(cd "$work/unpacked/package" && find . -type f | sed 's|^\./||' | sort)
want=$(printf '%s\n' LICENSE README.md package.json wai_quantum.d.ts wai_quantum.js wai_quantum_bg.wasm wai_quantum_bg.wasm.d.ts | sort)
if [ "$have" != "$want" ]; then
echo "check.sh: the tarball holds other files than the package's allow-list:" >&2
diff <(echo "$want") <(echo "$have") >&2 || true
exit 1
fi
echo "the tarball: $(echo "$have" | wc -l | tr -d ' ') files, the allow-list exactly"
node "$here/test/smoke.mjs" "$work/unpacked/package"
mkdir "$work/app"
printf '{ "name": "app", "private": true, "type": "module" }\n' > "$work/app/package.json"
(cd "$work/app" && npm install "$tgz" --no-audit --no-fund --silent)
cp "$here/test/install.mjs" "$work/app/"
(cd "$work/app" && node install.mjs)
(cd "$out" && n=$(basename "$tgz") && { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$n"; else shasum -a 256 "$n"; fi; } > "$n.sha256" && cat "$n.sha256")

View file

@ -0,0 +1,40 @@
{
"name": "@transaction-science/wai-quantum",
"version": "0.1.0",
"description": "The WAI quantum engine in WebAssembly: OpenQASM programs planned, run and signed in a page or in Node as the WAI quantum job service runs them, receipts checked, programs linted and drawn.",
"license": "Apache-2.0",
"type": "module",
"main": "./wai_quantum.js",
"module": "./wai_quantum.js",
"types": "./wai_quantum.d.ts",
"exports": {
".": {
"types": "./wai_quantum.d.ts",
"default": "./wai_quantum.js"
},
"./wai_quantum_bg.wasm": "./wai_quantum_bg.wasm",
"./package.json": "./package.json"
},
"files": [
"wai_quantum.js",
"wai_quantum.d.ts",
"wai_quantum_bg.wasm",
"wai_quantum_bg.wasm.d.ts",
"README.md",
"LICENSE"
],
"sideEffects": false,
"homepage": "https://wai.transaction.science/quantum-studio/",
"repository": {
"type": "git",
"url": "git+https://git.transaction.science/Transaction-Science/open-standards.git",
"directory": "wai/wai-quantum-js"
},
"keywords": ["quantum", "openqasm", "webassembly", "simulator", "receipts"],
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,20 @@
// The README's Node example, run from an app that installed the tarball
// (npm install <tgz>): the package found by its name, its WebAssembly by the
// subpath it exports, a job run and its receipt verified.
//
// cd APP && node install.mjs
import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { randomBytes } from 'node:crypto';
import assert from 'node:assert/strict';
const wasm = createRequire(import.meta.url).resolve('@transaction-science/wai-quantum/wai_quantum_bg.wasm');
await init({ module_or_path: await readFile(wasm) });
const engine = new QuantumEngine(randomBytes(32), 256);
const job = JSON.parse(engine.run(JSON.stringify({ qasm: 'OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c = measure q;', shots: 100 })));
const v = JSON.parse(engine_verify(JSON.stringify(job)));
assert.equal(v.valid, true, JSON.stringify(v));
assert.equal(v.signer, engine.signer_did());
assert.equal(Object.values(job.result.counts).reduce((a, b) => a + b, 0), 100, JSON.stringify(job.result.counts));
console.log('installed by name: a job run and its receipt verified', JSON.stringify(job.result.counts));

View file

@ -0,0 +1,56 @@
// The package as an installer leaves it: its bindings and its WebAssembly, loaded in
// Node with nothing else installed.
//
// node wai/wai-quantum-js/test/smoke.mjs PACKAGE_DIR
//
// Prints one line per check and exits non-zero on the first that fails.
import assert from 'node:assert/strict';
import { randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const dir = resolve(process.argv[2] ?? '.');
const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
const wq = await import(pathToFileURL(join(dir, pkg.main)).href);
// In Node the module is given its WebAssembly's bytes; a page lets it fetch them.
await wq.default({ module_or_path: await readFile(join(dir, 'wai_quantum_bg.wasm')) });
console.log('node', process.versions.node, pkg.name, pkg.version);
const BELL = 'OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;';
// A job run with a fresh key, and its receipt checked.
const engine = new wq.QuantumEngine(randomBytes(32), 256);
const signer = engine.signer_did();
assert.match(signer, /^did:key:z6Mk/, signer);
const job = JSON.parse(engine.run(JSON.stringify({ qasm: BELL, shots: 1000, seed: 7 })));
assert.equal(job.status, 'done', JSON.stringify(job).slice(0, 400));
const counts = job.result.counts;
assert.ok(Object.keys(counts).length > 0 && Object.keys(counts).every((k) => k === '00' || k === '11'), JSON.stringify(counts));
const v = JSON.parse(wq.engine_verify(JSON.stringify(job)));
assert.equal(v.valid, true, JSON.stringify(v));
assert.equal(v.signer, signer, JSON.stringify(v));
console.log(`run and verify: ${job.method}, ${Object.values(counts).reduce((a, b) => a + b, 0)} shots, signed by this engine's key`);
// A result changed after it was sealed does not verify.
const forged = structuredClone(job);
const k = Object.keys(forged.result.counts)[0];
forged.result.counts[k] += 1;
assert.notEqual(JSON.parse(wq.engine_verify(JSON.stringify(forged))).valid, true);
console.log('a forged result does not verify');
// A program read and drawn.
const lint = JSON.parse(wq.engine_lint(BELL));
assert.equal(lint.ok, true, JSON.stringify(lint));
assert.equal(lint.qubits, 2, JSON.stringify(lint));
const drawn = JSON.parse(wq.engine_draw(BELL, 80));
assert.ok(typeof drawn.text === 'string' && drawn.text.trim().length > 0, JSON.stringify(drawn));
console.log(`lint and draw: ${lint.qubits} qubits, ${lint.gates} gates; drawn in ${drawn.text.trimEnd().split('\n').length} lines`);
// A program the engine refuses says why, with its diagnostic code.
const WHILE = 'OPENQASM 3.0; qubit[1] q; bit[1] c; c[0] = measure q[0]; while (c[0]) { x q[0]; c[0] = measure q[0]; }';
const refused = JSON.parse(engine.run(JSON.stringify({ qasm: WHILE, shots: 10 })));
assert.ok(typeof refused.error === 'string' && /^Q\d{3}$/.test(refused.code ?? ''), JSON.stringify(refused));
console.log(`a refused program says why: [${refused.code}] ${refused.meaning}`);
console.log('smoke: every check passed');