diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml
new file mode 100644
index 000000000..3d1f43079
--- /dev/null
+++ b/.github/workflows/wai-quantum-cli.yml
@@ -0,0 +1,151 @@
+name: wai-quantum command line
+
+# The wai-quantum command line (wai/wai-quantum-cli): its tests and clippy with
+# warnings as errors, with and without `submit`; its smoke checks (smoke.sh: lint,
+# plan, run, verify and a forged result refused, key, draw, MCP over stdio, the
+# research tools); and the WASI component, whose output under wasmtime equals the
+# native build's byte for byte. On macOS, also with the GPU state vector.
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'wai/wai-quantum/**'
+ - 'wai/wai-quantum-cli/**'
+ - 'rust-toolchain.toml'
+ - '.github/workflows/wai-quantum-cli.yml'
+ pull_request:
+ paths:
+ - 'wai/wai-quantum/**'
+ - 'wai/wai-quantum-cli/**'
+ - 'rust-toolchain.toml'
+ - '.github/workflows/wai-quantum-cli.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-cli.yml \
+ "${{ github.event.pull_request.base.sha }}" \
+ "${{ github.event.pull_request.head.sha }}")
+ echo "this change touches the wai-quantum command line: $r"
+ echo "run=$r" >> "$GITHUB_OUTPUT"
+
+ cli-linux:
+ name: command line · 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 clippy and the wasm32-wasip2 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 --component clippy
+ rustup default 1.98.0
+ rustup target add --toolchain 1.98.0 wasm32-wasip2
+ echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+ - name: Tests and clippy, with and without submit
+ shell: bash
+ working-directory: wai/wai-quantum-cli
+ run: |
+ set -euo pipefail
+ cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log"
+ grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
+ cargo clippy --locked --release --all-targets -- -D warnings
+ cargo clippy --locked --release --all-targets --no-default-features -- -D warnings
+ - name: The smoke checks
+ shell: bash
+ working-directory: wai/wai-quantum-cli
+ run: |
+ set -euo pipefail
+ cargo build --locked --release
+ bash smoke.sh ./target/release/wai-quantum
+ # The release binary, pinned by version and SHA-256, rather than a build from source.
+ - name: Install wasmtime 49.0.2
+ shell: bash
+ run: |
+ set -euo pipefail
+ [ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; }
+ if ! command -v xz >/dev/null 2>&1; then
+ SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
+ $SUDO apt-get update -qq
+ $SUDO apt-get install -y -qq xz-utils
+ fi
+ v=49.0.2
+ f="wasmtime-v$v-x86_64-linux.tar.xz"
+ curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \
+ "https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f"
+ echo "a4d6e9e3a5a60f527cf7793d674c48930c80c2e8977995b8a275cad3254b9322 $RUNNER_TEMP/$f" | sha256sum -c -
+ tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP"
+ echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH"
+ # The same source as a WASI component: a circuit's state hash, a run's
+ # result and a receipt signed with the published key are the native
+ # build's, byte for byte.
+ - name: The WASI component gives the native build's bytes
+ shell: bash
+ working-directory: wai/wai-quantum-cli
+ run: |
+ set -euo pipefail
+ cargo build --locked --release --target wasm32-wasip2 --no-default-features
+ w=target/wasm32-wasip2/release/wai-quantum.wasm
+ n=target/release/wai-quantum
+ printf 'OPENQASM 3.0; qubit[3] q; h q[0]; cx q[0], q[1]; t q[2]; cx q[1], q[2];' > "$RUNNER_TEMP/p.qasm"
+ for args in "lab run ghz -n 5" "lab receipt ghz -n 3 --demo-key"; do
+ $n $args > "$RUNNER_TEMP/native.txt" 2> /dev/null
+ wasmtime run "$w" $args > "$RUNNER_TEMP/wasi.txt" 2> /dev/null
+ cmp "$RUNNER_TEMP/native.txt" "$RUNNER_TEMP/wasi.txt" || { echo "::error::$args differs under wasmtime"; diff "$RUNNER_TEMP/native.txt" "$RUNNER_TEMP/wasi.txt" || true; exit 1; }
+ done
+ $n run --demo-key --json --shots 100 < "$RUNNER_TEMP/p.qasm" 2> /dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); j["result"].pop("elapsed_ms"); j["result"]["receipt"].pop("digest"); j["result"]["receipt"].pop("signature"); print(json.dumps(j, sort_keys=True))' > "$RUNNER_TEMP/native.json"
+ wasmtime run "$w" run --demo-key --json --shots 100 < "$RUNNER_TEMP/p.qasm" 2> /dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); j["result"].pop("elapsed_ms"); j["result"]["receipt"].pop("digest"); j["result"]["receipt"].pop("signature"); print(json.dumps(j, sort_keys=True))' > "$RUNNER_TEMP/wasi.json"
+ cmp "$RUNNER_TEMP/native.json" "$RUNNER_TEMP/wasi.json" || { echo "::error::a run's result differs under wasmtime"; exit 1; }
+ echo "the WASI component's output equals the native build's"
+
+ cli-macos:
+ name: command line · macos
+ needs: relevant
+ if: needs.relevant.outputs.run == 'true'
+ runs-on: macos
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v5
+ # launchd's PATH omits cargo on the macOS host runner.
+ - name: Put cargo on PATH
+ shell: bash
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+ - name: Tests and clippy, with the GPU state vector
+ shell: bash
+ working-directory: wai/wai-quantum-cli
+ run: |
+ set -euo pipefail
+ nice -n 10 cargo test --locked --release --features metal 2>&1 | tee "$RUNNER_TEMP/test.log"
+ grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
+ nice -n 10 cargo clippy --locked --release --all-targets --features metal -- -D warnings
+ - name: The smoke checks, and a run on the GPU
+ shell: bash
+ working-directory: wai/wai-quantum-cli
+ run: |
+ set -euo pipefail
+ nice -n 10 cargo build --locked --release --features metal
+ bash smoke.sh ./target/release/wai-quantum
+ printf 'OPENQASM 3.0; qubit[12] q; bit[12] c; h q; rx(0.3) q[0]; cx q[0], q[1]; c = measure q;' > "$RUNNER_TEMP/gpu.qasm"
+ ./target/release/wai-quantum run "$RUNNER_TEMP/gpu.qasm" --method statevector_single --shots 200 --demo-key --json > "$RUNNER_TEMP/gpu.json" 2> /dev/null
+ python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); r=j["result"]; assert r["accuracy"]["class"]=="single" and sum(r["counts"].values())==200, r' "$RUNNER_TEMP/gpu.json"
+ echo "statevector_single ran on $(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"]["accuracy"]["device"])' "$RUNNER_TEMP/gpu.json")"
diff --git a/.github/workflows/wai-quantum-studio.yml b/.github/workflows/wai-quantum-studio.yml
new file mode 100644
index 000000000..3b801bf68
--- /dev/null
+++ b/.github/workflows/wai-quantum-studio.yml
@@ -0,0 +1,137 @@
+name: wai-quantum studio and desktop
+
+# Quantum Studio (wai/quantum-studio), the interface for the browser and the
+# desktop, and the desktop app around it (wai/wai-quantum-desktop):
+# - the Studio built against the engine's browser package (wai-web, feature
+# quantum_engine) and run in a browser by test/studio.test.mjs: linted, drawn and
+# planned as it is typed, run here with its receipt checked here, an error marked
+# on its line, any angle on the state vector, Ctrl+Enter, and the program and the
+# history kept across a reload; nothing scrolls sideways at a phone's width;
+# - the desktop app's tests and clippy with warnings as errors, and `check`: a job
+# run in the app's worker process, sealed in the app with a key that never leaves
+# it, and verified, with no window.
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'wai/quantum-studio/**'
+ - 'wai/wai-quantum-desktop/**'
+ - 'wai/wai-quantum/**'
+ - 'wai/wai-quantum-cli/**'
+ - 'wai/wai-web/**'
+ - 'wai/wai-rs/**'
+ - 'rust-toolchain.toml'
+ - '.github/workflows/wai-quantum-studio.yml'
+ pull_request:
+ paths:
+ - 'wai/quantum-studio/**'
+ - 'wai/wai-quantum-desktop/**'
+ - 'wai/wai-quantum/**'
+ - 'wai/wai-quantum-cli/**'
+ - 'wai/wai-web/**'
+ - 'wai/wai-rs/**'
+ - 'rust-toolchain.toml'
+ - '.github/workflows/wai-quantum-studio.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-studio.yml \
+ "${{ github.event.pull_request.base.sha }}" \
+ "${{ github.event.pull_request.head.sha }}")
+ echo "this change touches the studio or the desktop app: $r"
+ echo "run=$r" >> "$GITHUB_OUTPUT"
+
+ studio:
+ name: studio · browser
+ 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.
+ - 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: The engine's browser package
+ shell: bash
+ working-directory: wai/wai-web
+ run: |
+ set -euo pipefail
+ cargo build --locked --release --target wasm32-unknown-unknown --features quantum_engine
+ wasm-bindgen --target web --out-dir "$RUNNER_TEMP/pkg" --out-name wai_quantum_ide \
+ target/wasm32-unknown-unknown/release/wai_web.wasm
+ - name: Build the Studio and run it in a browser
+ shell: bash
+ working-directory: wai/quantum-studio
+ env:
+ WAI_E2E_CHANNEL: ''
+ STUDIO_SKIP_SERVICE: '1'
+ run: |
+ set -euo pipefail
+ npm ci --no-audit --no-fund
+ npx playwright install --with-deps chromium
+ node build.mjs --pkg "$RUNNER_TEMP/pkg"
+ node test/studio.test.mjs
+
+ desktop:
+ name: desktop app · macos
+ needs: relevant
+ if: needs.relevant.outputs.run == 'true'
+ runs-on: macos
+ timeout-minutes: 90
+ steps:
+ - uses: actions/checkout@v5
+ # launchd's PATH omits cargo and node on the macOS host runner.
+ - name: Put cargo and node on PATH
+ shell: bash
+ run: |
+ echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+ echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
+ # The app embeds the Studio's build, so it is built first.
+ - name: Build the Studio
+ shell: bash
+ working-directory: wai/quantum-studio
+ run: |
+ set -euo pipefail
+ npm ci --no-audit --no-fund
+ node build.mjs
+ - name: Tests, clippy, and a job through the worker with no window
+ shell: bash
+ working-directory: wai/wai-quantum-desktop
+ run: |
+ set -euo pipefail
+ nice -n 10 cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log"
+ grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
+ nice -n 10 cargo clippy --locked --release --all-targets -- -D warnings
+ nice -n 10 cargo build --locked --release
+ ./target/release/wai-quantum-desktop check
diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml
index dfe99c844..5b2260d8b 100644
--- a/.github/workflows/wai-web-wasm.yml
+++ b/.github/workflows/wai-web-wasm.yml
@@ -149,6 +149,28 @@ jobs:
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, and every method a tab holds run with no clock and no threads. The
+ # count is the four tests above and its two.
+ - 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. 6 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, and plan, lint, draw and methods as JSON.
+ - 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. 4 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
@@ -161,8 +183,10 @@ jobs:
# 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
- # (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as
+ # 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
@@ -171,7 +195,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log"
- grep -q "test result: ok. 32 passed; 0 failed" "$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
diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md
index 2b36338ac..61d6785c1 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -6,6 +6,63 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
+### wai-quantum 0.6.11 and wai-quantum-cli 0.1.0: one job engine for the service, the command line and a browser tab
+
+Additive, apart from two refusals and an admission noted below. Every pinned digest is unchanged.
+
+- **`wai_quantum::engine`** (feature `quantum_engine`) is the job service's engine, moved into the library.
+ - `catalog` plans a job onto the strongest method it admits, and says why.
+ - `run` carries the plan out.
+ - `dynamic` runs programs that measure in the middle.
+ - `receipt` is the service's receipt form: a result sealed over the request and the result in their RFC 8785 canonical text.
+ - `job` takes a job end to end and checks it.
+ - `draw` writes a program's circuit as text.
+
+ The service imports all of these. A job now gives the same result and the same receipt form whether it runs on the service, on the command line or in a browser tab.
+- **The engine builds for `wasm32-unknown-unknown`.** There a result carries no `elapsed_ms`, since a tab has no clock std can read. The multi-seed cut-rank order search runs its seeds one after another and finds the same orders.
+- **A noise channel the model does not know is refused** (`catalog::NOISE_CHANNELS`). It was read as no noise, so a misspelt channel ran the circuit noiseless.
+- **The job's memory margin is counted once for a program that measures in the middle on the state vector,** as it is for trajectories. It was counted once per state, and the service refused a qubit it could hold.
+- **`quantum_receipt::did_key`** gives an Ed25519 key's `did:key`, held to the W3C method's example.
+- **`wai-quantum-cli` 0.1.0** is a new crate whose binary is `wai-quantum`. Its commands:
+ - `run`: planned and run as the service runs it, signed with the user's key;
+ - `plan`, `lint` and `draw`;
+ - `submit` to the service;
+ - `verify`, for any receipt this tool, the service or `lab` signs;
+ - `key`, `methods` and `completions`;
+ - `mcp`, which serves these tools to an agent over stdio;
+ - `lab`, which holds the earlier command's research tools.
+
+ It depends on the library alone. The `wai-quantum` bin and the `cli_keys` feature leave wai-rs.
+- **wai-web, feature `quantum_engine`:** `QuantumEngine` (run, plan, methods and signer), `engine_lint`, `engine_draw` and `engine_verify`, for a page. Tested in wasm32 under Node on every method a tab holds.
+
+### wai-quantum 0.6.10: one OpenQASM reader for the service, the command line and the browser IDE
+
+Additive, apart from the old line reader's refusals below. Every pinned digest is unchanged.
+
+- **`wai_quantum::qasm`** (feature `quantum_qasm`) is the job service's OpenQASM reader,
+ moved into the library unchanged. The service, `wai-quantum run --qasm` and the browser
+ IDE's import now read a program the same way. Before, the IDE read two registers as
+ one, skipped a gate on a whole register, and read past what it did not know.
+- **`qasm::dyadic_circuit`** gives the exact state vector's circuit for a program whose
+ measurements all come at the end, and refuses one that measures and then acts, resets
+ after a gate or uses `if`, with the reason.
+- **`quantum_toolchain::from_qasm`**, the older line reader, refuses what it cannot take
+ instead of dropping the line: more than one register, `reset`, gate definitions,
+ `if`/`while`/`for`, a gate after a measurement, two statements on one line. Each
+ refusal names `wai_quantum::qasm`.
+- **The IDE binding** (`wai-web`) builds its JSON with `serde_json`, so a quote or a
+ control character in a program or an error no longer breaks a reply.
+ `set_signing_key`, `public_key_hex`, `signer_did` and `key_is_demo` let a page sign
+ receipts with a key of its own; without one it signs with the published demo key and
+ says so.
+- **The command line** signs with `--key FILE`, `$WAI_QUANTUM_KEY`, or this user's key at
+ `$XDG_CONFIG_HOME/wai/quantum-key` (or `~/.config/wai/quantum-key`). That key is made
+ from the operating system's random source on first use, with mode 0600.
+ `--demo-key` keeps the published key, with a note that its receipts show a result is
+ unaltered, not who signed it.
+- **A receipt from the command line or the IDE names its signer by its key's `did:key`**
+ (`wai::signer_id::did_key`), where it used to carry a label naming no key.
+
### wai-quantum 0.6.9: cut-rank amplitudes with the circuit's own global phase
Additive. Every pinned digest is unchanged.
diff --git a/wai/README.md b/wai/README.md
index a3115e807..333266736 100644
--- a/wai/README.md
+++ b/wai/README.md
@@ -207,39 +207,44 @@ reproduces ([quantum-sim](extensions/quantum-sim.md) §8).
### Quickstart
-```bash
-# native
-cargo build --release --no-default-features \
- --features "quantum_toolchain,quantum_receipt" --bin wai-quantum
+The command line is [`wai-quantum-cli`](wai-quantum-cli/README.md): OpenQASM
+programs run here as the quantum job service runs them, each result naming its
+method and accuracy and signed with this user's key.
-./target/release/wai-quantum list # algorithm catalog
-./target/release/wai-quantum run ghz -n 5 # simulate + hash + outcomes
-./target/release/wai-quantum run grover -n 4 -p 9 # amplitude amplification
-./target/release/wai-quantum qasm bell # emit OpenQASM 3.0
-./target/release/wai-quantum qir bell # emit QIR (LLVM IR)
-./target/release/wai-quantum qir qft -n 4 | \\
- ./target/release/wai-quantum run --qir # and read it back
-./target/release/wai-quantum receipt ghz -n 3 | ./target/release/wai-quantum verify
-./target/release/wai-quantum run qft -n 5 --gateset v1 # the frozen wai.quantum.circuit
+```bash
+cd wai-quantum-cli && cargo build --release # or: cargo install wai-quantum-cli
+W=./target/release/wai-quantum
+
+$W run bell.qasm --shots 1000 -o "Z0 Z1" # planned as the service plans it
+$W run bell.qasm --json > job.json && $W verify job.json
+$W plan prog.qasm; $W lint prog.qasm; $W draw prog.qasm
+$W submit prog.qasm # on the job service
+$W lab list # the algorithm catalogue
+$W lab run ghz -n 5 # simulate + hash + outcomes
+$W lab qasm bell # emit OpenQASM 3.0
+$W lab qir qft -n 4 | $W lab run --qir # QIR out, and back in
+$W lab receipt ghz -n 3 | $W verify
+$W lab run qft -n 5 --gateset v1 # the frozen wai.quantum.circuit
```
-Circuits are `wai.quantum.circuit2` unless `--gateset v1` asks for
+Circuits built by `lab` are `wai.quantum.circuit2` unless `--gateset v1` asks for
`wai.quantum.circuit`, which reproduces the circuit and statevector hashes
earlier releases gave.
### Same source, at the edge
```bash
-cargo build --release --target wasm32-wasip2 --no-default-features \
- --features "quantum_toolchain,quantum_receipt" --bin wai-quantum
+cd wai-quantum-cli && cargo build --release --target wasm32-wasip2 --no-default-features
-wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm run ghz -n 5
+wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm lab run ghz -n 5
+wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm lab receipt ghz -n 3 --demo-key
```
Both print the identical `state-hash`. A receipt sealed in cloud WASM verifies
-natively and vice-versa, and the receipt bytes themselves are identical — the
-signature travels because the computation is reproducible, not because the two
-runtimes agreed to trust each other.
+natively and vice-versa, and with the same key (here the published demo key)
+the receipt bytes themselves are identical: the signature travels because the
+computation is reproducible, not because the two runtimes agreed to trust each
+other.
### As a serverless function
diff --git a/wai/quantum-studio/.gitignore b/wai/quantum-studio/.gitignore
new file mode 100644
index 000000000..75a0c418b
--- /dev/null
+++ b/wai/quantum-studio/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+test/screenshots/
diff --git a/wai/quantum-studio/README.md b/wai/quantum-studio/README.md
new file mode 100644
index 000000000..a39ef3879
--- /dev/null
+++ b/wai/quantum-studio/README.md
@@ -0,0 +1,61 @@
+# Quantum Studio
+
+An editor for OpenQASM programs, with three places to run them:
+
+- **this browser**, through the quantum job service's engine compiled to WebAssembly, in a worker;
+- **this computer**, through the same engine built natively, in the [desktop app](../wai-quantum-desktop/);
+- **the job service**, over HTTP.
+
+Every place returns the same job: the request, the method and why it was chosen, the result, and its receipt. The Studio shows them the same way and checks every receipt on the machine it runs on. The same request with the same seed gives the same counts in each place.
+
+The editor:
+
+- highlights OpenQASM;
+- completes the standard gates and the program's own registers;
+- marks the engine's diagnostics on their lines;
+- draws the circuit and shows the plan as you type.
+
+Ctrl+Enter (⌘+Enter on macOS) runs the program, and Escape cancels a run. The program and the history of runs are kept on the device. "Copy link" puts the program in a URL fragment, which no server sees.
+
+## Hosting it
+
+Build it with `npm ci && node build.mjs`. That writes `dist/`:
+
+| File | What it is |
+|---|---|
+| `studio.js` | the interface, bundled |
+| `worker.js` | the engine's worker |
+| `studio.css` | the Studio's styles |
+| `tokens.css` | the site's design tokens, for when it stands alone |
+| `index.html` | the page that stands alone |
+
+A host mounts the Studio on an element:
+
+```html
+
+
+```
+
+`data-engine-pkg` names the engine's browser package: wai-web built with the feature `quantum_engine`. `node build.mjs --pkg DIR` copies such a package into `dist/pkg/` for `index.html`. On the desktop, `window.__TAURI__` is present and the Studio uses the native engine instead.
+
+## Testing it
+
+```sh
+node build.mjs --pkg PATH/TO/PKG && node test/studio.test.mjs
+```
+
+The test runs the Studio in the installed browser:
+
+- lint, draw and plan as the program is typed;
+- a run in the browser, with its receipt checked there;
+- an error marked on its line;
+- a rotation run on the state vector;
+- the same job on the live service giving the counts run here, shot for shot;
+- Ctrl+Enter;
+- the program and the history surviving a reload;
+- the layout at a phone's width.
+
+`STUDIO_SKIP_SERVICE=1` leaves the service step out. It saves screenshots of both colour schemes in `test/screenshots/`.
diff --git a/wai/quantum-studio/build.mjs b/wai/quantum-studio/build.mjs
new file mode 100644
index 000000000..e5dd90937
--- /dev/null
+++ b/wai/quantum-studio/build.mjs
@@ -0,0 +1,22 @@
+// Build the Studio into dist/: studio.js (the bundled interface), worker.js
+// (the engine's worker), studio.css, tokens.css and index.html. The engine
+// package itself is not bundled: a host serves it and names it in the mount
+// element's data-engine-pkg.
+//
+// node build.mjs [--pkg DIR] (--pkg copies an engine package into dist/pkg/)
+import { build } from 'esbuild';
+import { copyFileSync, cpSync, mkdirSync, rmSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const here = dirname(fileURLToPath(import.meta.url));
+const dist = join(here, 'dist');
+rmSync(dist, { recursive: true, force: true });
+mkdirSync(dist, { recursive: true });
+const common = { bundle: true, format: 'esm', target: ['es2022'], minify: true, legalComments: 'eof', logLevel: 'warning' };
+await build({ ...common, entryPoints: [join(here, 'src/main.js')], outfile: join(dist, 'studio.js') });
+await build({ ...common, entryPoints: [join(here, 'src/worker.js')], outfile: join(dist, 'worker.js') });
+for (const f of ['studio.css', 'tokens.css', 'index.html']) copyFileSync(join(here, 'src', f), join(dist, f));
+const at = process.argv.indexOf('--pkg');
+if (at > 0) cpSync(process.argv[at + 1], join(dist, 'pkg'), { recursive: true });
+console.log(`built ${dist}`);
diff --git a/wai/quantum-studio/package-lock.json b/wai/quantum-studio/package-lock.json
new file mode 100644
index 000000000..0b453354b
--- /dev/null
+++ b/wai/quantum-studio/package-lock.json
@@ -0,0 +1,694 @@
+{
+ "name": "wai-quantum-studio",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "wai-quantum-studio",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@codemirror/autocomplete": "6.20.4",
+ "@codemirror/commands": "6.11.1",
+ "@codemirror/language": "6.13.1",
+ "@codemirror/lint": "6.9.7",
+ "@codemirror/search": "6.7.2",
+ "@codemirror/state": "6.7.6",
+ "@codemirror/view": "6.43.15",
+ "@lezer/highlight": "1.2.5"
+ },
+ "devDependencies": {
+ "esbuild": "0.27.4",
+ "playwright": "1.60.0"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.20.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.4.tgz",
+ "integrity": "sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz",
+ "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.7.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz",
+ "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/streamparser": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
+ "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.42.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz",
+ "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.37.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.7.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz",
+ "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/streamparser": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz",
+ "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==",
+ "license": "MIT"
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.43.15",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz",
+ "integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.7.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz",
+ "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz",
+ "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.11.tgz",
+ "integrity": "sha512-UwgZcl1R66H2hDzBrA7kWyYmX6s8p5TQ+e/t7CuvgCtBEPxbEPaHYHu+pJBvTvDotEJpuQkJjbXuHiw5Cixxgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz",
+ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
+ "license": "MIT"
+ },
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.4",
+ "@esbuild/android-arm": "0.27.4",
+ "@esbuild/android-arm64": "0.27.4",
+ "@esbuild/android-x64": "0.27.4",
+ "@esbuild/darwin-arm64": "0.27.4",
+ "@esbuild/darwin-x64": "0.27.4",
+ "@esbuild/freebsd-arm64": "0.27.4",
+ "@esbuild/freebsd-x64": "0.27.4",
+ "@esbuild/linux-arm": "0.27.4",
+ "@esbuild/linux-arm64": "0.27.4",
+ "@esbuild/linux-ia32": "0.27.4",
+ "@esbuild/linux-loong64": "0.27.4",
+ "@esbuild/linux-mips64el": "0.27.4",
+ "@esbuild/linux-ppc64": "0.27.4",
+ "@esbuild/linux-riscv64": "0.27.4",
+ "@esbuild/linux-s390x": "0.27.4",
+ "@esbuild/linux-x64": "0.27.4",
+ "@esbuild/netbsd-arm64": "0.27.4",
+ "@esbuild/netbsd-x64": "0.27.4",
+ "@esbuild/openbsd-arm64": "0.27.4",
+ "@esbuild/openbsd-x64": "0.27.4",
+ "@esbuild/openharmony-arm64": "0.27.4",
+ "@esbuild/sunos-x64": "0.27.4",
+ "@esbuild/win32-arm64": "0.27.4",
+ "@esbuild/win32-ia32": "0.27.4",
+ "@esbuild/win32-x64": "0.27.4"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.60.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz",
+ "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==",
+ "license": "MIT"
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/wai/quantum-studio/package.json b/wai/quantum-studio/package.json
new file mode 100644
index 000000000..8cc6ffbe2
--- /dev/null
+++ b/wai/quantum-studio/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "wai-quantum-studio",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Quantum Studio: an editor for OpenQASM programs, run in this browser, on the desktop or on the quantum job service by the same engine, every result naming its method and accuracy and its receipt checked where it is shown. One interface for the browser and the desktop app.",
+ "license": "Apache-2.0",
+ "type": "module",
+ "scripts": {
+ "build": "node build.mjs",
+ "test": "node test/studio.test.mjs"
+ },
+ "dependencies": {
+ "@codemirror/autocomplete": "6.20.4",
+ "@codemirror/commands": "6.11.1",
+ "@codemirror/language": "6.13.1",
+ "@codemirror/lint": "6.9.7",
+ "@codemirror/search": "6.7.2",
+ "@codemirror/state": "6.7.6",
+ "@codemirror/view": "6.43.15",
+ "@lezer/highlight": "1.2.5"
+ },
+ "devDependencies": {
+ "esbuild": "0.27.4",
+ "playwright": "1.60.0"
+ }
+}
diff --git a/wai/quantum-studio/src/engine.js b/wai/quantum-studio/src/engine.js
new file mode 100644
index 000000000..8c9a0ca41
--- /dev/null
+++ b/wai/quantum-studio/src/engine.js
@@ -0,0 +1,130 @@
+// Where a job runs. Three engines, one interface, one engine underneath:
+//
+// tab the job service's engine compiled for the browser, in a worker
+// service the job service itself, over HTTP
+// desktop the same engine built natively, in the desktop app
+//
+// Each takes the service's request JSON and gives the service's job JSON
+// (request, plan and result with its receipt), so the Studio shows a job the
+// same way wherever it ran. Lint, draw and verify run locally: in the tab, or
+// natively on the desktop.
+
+const KEY_ITEM = 'wai.quantum-ide.signing-key'; // one key per browser, shared with the IDE and the console
+
+function browserSeed() {
+ try {
+ const h = localStorage.getItem(KEY_ITEM);
+ if (h && /^[0-9a-f]{64}$/.test(h)) return Uint8Array.from(h.match(/../g), (b) => parseInt(b, 16));
+ } catch (e) {}
+ const seed = crypto.getRandomValues(new Uint8Array(32));
+ try { localStorage.setItem(KEY_ITEM, Array.from(seed, (b) => b.toString(16).padStart(2, '0')).join('')); } catch (e) {}
+ return seed;
+}
+
+export class TabEngine {
+ constructor(pkgUrl, workerUrl) {
+ this.pkgUrl = pkgUrl;
+ this.workerUrl = workerUrl;
+ this.memoryMb = navigator.deviceMemory && navigator.deviceMemory < 4 ? 512 : 1024;
+ this.where = 'this browser';
+ this.worker = null;
+ this.waiting = new Map();
+ this.next = 1;
+ }
+ call(op, data = {}) {
+ if (!this.worker) {
+ this.worker = new Worker(this.workerUrl, { type: 'module' });
+ this.worker.onmessage = (ev) => {
+ const w = this.waiting.get(ev.data.id);
+ if (!w) return;
+ this.waiting.delete(ev.data.id);
+ if (ev.data.ok) w.resolve(ev.data.value); else w.reject(new Error(ev.data.error));
+ };
+ this.worker.onerror = (ev) => {
+ for (const w of this.waiting.values()) w.reject(new Error(ev.message || 'the engine did not load in this browser'));
+ this.waiting.clear();
+ this.worker = null;
+ };
+ this.seed = browserSeed();
+ }
+ const id = this.next++;
+ return new Promise((resolve, reject) => {
+ this.waiting.set(id, { resolve, reject });
+ this.worker.postMessage({ id, op, pkg: this.pkgUrl, seed: Array.from(this.seed), memoryMb: this.memoryMb, ...data });
+ });
+ }
+ run(request) { return this.call('run', { request }); }
+ plan(request) { return this.call('plan', { request }); }
+ methods() { return this.call('methods'); }
+ lint(qasm) { return this.call('lint', { qasm }); }
+ draw(qasm, width) { return this.call('draw', { qasm, width }); }
+ verify(job) { return this.call('verify', { job }); }
+ async signer() { return (await this.call('signer')).did; }
+ cancel() {
+ this.worker?.terminate();
+ this.worker = null;
+ for (const w of this.waiting.values()) w.reject(new Error('cancelled'));
+ this.waiting.clear();
+ }
+}
+
+export class ServiceEngine {
+ constructor(base) {
+ this.base = base.replace(/\/+$/, '');
+ this.where = 'the service';
+ this.current = null;
+ this.stopped = false;
+ }
+ async json(res) {
+ const body = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(body.error || `the service answered ${res.status}`);
+ return body;
+ }
+ async run(request, onProgress) {
+ this.stopped = false;
+ const sub = await this.json(await fetch(`${this.base}/v1/jobs`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request) }));
+ this.current = sub.id;
+ let wait = 300;
+ for (;;) {
+ if (this.stopped) throw new Error('cancelled');
+ const job = await this.json(await fetch(`${this.base}/v1/jobs/${encodeURIComponent(sub.id)}`));
+ onProgress?.(job);
+ if (job.status === 'done') { job.request ??= request; this.current = null; return job; }
+ if (job.status !== 'queued' && job.status !== 'running') { this.current = null; throw new Error(job.error || `the job ${job.status}`); }
+ await new Promise((r) => setTimeout(r, wait));
+ wait = Math.min(2000, wait * 1.5);
+ }
+ }
+ async methods() { return this.json(await fetch(`${this.base}/v1/methods`)); }
+ async describe() { return this.json(await fetch(`${this.base}/v1`)); }
+ cancel() {
+ this.stopped = true;
+ if (this.current) fetch(`${this.base}/v1/jobs/${encodeURIComponent(this.current)}`, { method: 'DELETE' }).catch(() => {});
+ }
+}
+
+export class DesktopEngine {
+ constructor() {
+ this.where = 'this computer';
+ this.invoke = window.__TAURI__.core.invoke;
+ }
+ // The native engine answers with JSON text, as the tab's does, so both read alike.
+ async call(cmd, args) {
+ const v = JSON.parse(await this.invoke(cmd, args));
+ if (v && v.error) throw new Error(v.error);
+ return v;
+ }
+ run(request) { return this.call('run_job', { request: JSON.stringify(request) }); }
+ plan(request) { return this.call('plan_job', { request: JSON.stringify(request) }); }
+ methods() { return this.call('methods', {}); }
+ // A lint names a problem as {ok: false, error}, an answer and not a failure.
+ async lint(qasm) { return JSON.parse(await this.invoke('lint', { qasm })); }
+ draw(qasm, width) { return this.call('draw', { qasm, width }); }
+ verify(job) { return this.call('verify', { job: JSON.stringify(job) }); }
+ async signer() { return (await this.call('signer', {})).did; }
+ saveJob(name, job) { return this.call('save_job', { name, job: JSON.stringify(job, null, 2) }); }
+ saveProgram(name, text) { return this.call('save_program', { name, text }); }
+ cancel() { this.call('cancel', {}).catch(() => {}); }
+}
+
+export const onDesktop = () => typeof window !== 'undefined' && !!window.__TAURI__?.core?.invoke;
diff --git a/wai/quantum-studio/src/examples.js b/wai/quantum-studio/src/examples.js
new file mode 100644
index 000000000..2da08e704
--- /dev/null
+++ b/wai/quantum-studio/src/examples.js
@@ -0,0 +1,58 @@
+// Worked examples, each a whole job and what it shows.
+const H = 'OPENQASM 3.0;\ninclude "stdgates.inc";\n';
+
+export const EXAMPLES = [
+ {
+ id: 'bell',
+ title: 'Bell pair',
+ shows: 'Two qubits entangled: only 00 and 11 come up, and ⟨Z0 Z1⟩ is exactly 1.',
+ job: { qasm: `${H}qubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n`, shots: 1000, observables: ['Z0 Z1', 'X0 X1'] },
+ },
+ {
+ id: 'ghz',
+ title: 'GHZ state, 40 qubits',
+ shows: 'A Clifford circuit past any state vector, run exactly on the stabilizer tableau: all zeros or all ones.',
+ job: { qasm: `${H}qubit[40] q;\nbit[40] c;\nh q[0];\n${Array.from({ length: 39 }, (_, i) => `cx q[${i}], q[${i + 1}];`).join('\n')}\nc = measure q;\n`, shots: 1000 },
+ },
+ {
+ id: 'rotation',
+ title: 'A rotation',
+ shows: 'Any angle runs on the double-precision state vector: ⟨Z0⟩ = cos 0.3 and ⟨Y0⟩ = −sin 0.3.',
+ job: { qasm: `${H}qubit[1] q;\nrx(0.3) q[0];\n`, observables: ['Z0', 'Y0'], amplitudes: ['0', '1'] },
+ },
+ {
+ id: 'qft',
+ title: 'Quantum Fourier transform',
+ shows: 'The Fourier transform of a basis state on three qubits: every outcome equally likely, the information in the amplitudes\' phases.',
+ job: {
+ qasm: `${H}qubit[3] q;\nx q[2];\nh q[0];\ncp(pi/2) q[1], q[0];\ncp(pi/4) q[2], q[0];\nh q[1];\ncp(pi/2) q[2], q[1];\nh q[2];\nswap q[0], q[2];\n`,
+ amplitudes: ['000', '001', '010', '011'],
+ shots: 2000,
+ },
+ },
+ {
+ id: 'teleport',
+ title: 'Teleportation',
+ shows: 'A state moved from qubit 0 to qubit 2 by measurement and correction: ⟨Z2⟩ = cos 0.7, ⟨X2⟩ = sin 0.7.',
+ job: {
+ qasm: `${H}qubit[3] q;\nbit[3] c;\nry(0.7) q[0];\nh q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nh q[0];\nc[0] = measure q[0];\nc[1] = measure q[1];\nif (c[1]) x q[2];\nif (c[0]) z q[2];\nc[2] = measure q[2];\n`,
+ observables: ['Z2', 'X2'],
+ shots: 2000,
+ },
+ },
+ {
+ id: 'grover',
+ title: 'Grover search, 3 qubits',
+ shows: 'Two rounds of amplitude amplification find the marked item 101 with probability 0.945.',
+ job: {
+ qasm: `${H}qubit[3] q;\nbit[3] c;\nh q;\n${'x q[1];\nh q[2];\nccx q[0], q[1], q[2];\nh q[2];\nx q[1];\nh q;\nx q;\nh q[2];\nccx q[0], q[1], q[2];\nh q[2];\nx q;\nh q;\n'.repeat(2)}c = measure q;\n`,
+ shots: 2000,
+ },
+ },
+ {
+ id: 'noisy-bell',
+ title: 'A noisy Bell pair',
+ shows: 'The same pair on a device whose gates go wrong at random: 01 and 10 appear, and ⟨Z0 Z1⟩ falls below 1.',
+ job: { qasm: `${H}qubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n`, shots: 2000, observables: ['Z0 Z1'], noise: { depolarizing_1q: 0.01, depolarizing_2q: 0.03, readout: 0.02 } },
+ },
+];
diff --git a/wai/quantum-studio/src/history.js b/wai/quantum-studio/src/history.js
new file mode 100644
index 000000000..0a1875b00
--- /dev/null
+++ b/wai/quantum-studio/src/history.js
@@ -0,0 +1,53 @@
+// The jobs run in the Studio, kept in this browser (or the desktop app's own
+// store), newest first. Where storage is off, the history lasts the visit.
+const DB = 'wai-quantum-studio';
+const STORE = 'jobs';
+const MAX = 200;
+let memory = [];
+
+function open() {
+ return new Promise((resolve, reject) => {
+ const r = indexedDB.open(DB, 1);
+ r.onupgradeneeded = () => r.result.createObjectStore(STORE, { keyPath: 'key' });
+ r.onsuccess = () => resolve(r.result);
+ r.onerror = () => reject(r.error);
+ });
+}
+
+function tx(db, mode, f) {
+ return new Promise((resolve, reject) => {
+ const t = db.transaction(STORE, mode);
+ const out = f(t.objectStore(STORE));
+ t.oncomplete = () => resolve(out?.result ?? out);
+ t.onerror = () => reject(t.error);
+ });
+}
+
+// `entry`: {key, saved (ms), where, title, job}.
+export async function save(entry) {
+ memory = [entry, ...memory.filter((e) => e.key !== entry.key)].slice(0, MAX);
+ try {
+ const db = await open();
+ await tx(db, 'readwrite', (s) => s.put(entry));
+ const all = await list();
+ if (all.length > MAX) await tx(db, 'readwrite', (s) => all.slice(MAX).forEach((e) => s.delete(e.key)));
+ } catch (e) {}
+}
+
+export async function list() {
+ try {
+ const db = await open();
+ const all = await tx(db, 'readonly', (s) => s.getAll());
+ return all.sort((a, b) => b.saved - a.saved);
+ } catch (e) {
+ return memory;
+ }
+}
+
+export async function clear() {
+ memory = [];
+ try {
+ const db = await open();
+ await tx(db, 'readwrite', (s) => s.clear());
+ } catch (e) {}
+}
diff --git a/wai/quantum-studio/src/index.html b/wai/quantum-studio/src/index.html
new file mode 100644
index 000000000..e5977c176
--- /dev/null
+++ b/wai/quantum-studio/src/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Quantum Studio
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wai/quantum-studio/src/main.js b/wai/quantum-studio/src/main.js
new file mode 100644
index 000000000..b89eef0dd
--- /dev/null
+++ b/wai/quantum-studio/src/main.js
@@ -0,0 +1,412 @@
+// Quantum Studio: an editor for OpenQASM programs, run on this device or on
+// the quantum job service by the same engine. Every result names its method,
+// why, and how accurate it is, and carries a receipt checked here. One
+// interface for the browser and the desktop app.
+//
+// The host gives a mount element:
+//
+// and, on the desktop, window.__TAURI__ (the native engine).
+import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection } from '@codemirror/view';
+import { EditorState, Compartment } from '@codemirror/state';
+import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
+import { syntaxHighlighting, HighlightStyle, bracketMatching, indentOnInput } from '@codemirror/language';
+import { tags as t } from '@lezer/highlight';
+import { linter, lintGutter, forceLinting } from '@codemirror/lint';
+import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import { openqasm, complete } from './qasm-lang.js';
+import { TabEngine, ServiceEngine, DesktopEngine, onDesktop } from './engine.js';
+import { EXAMPLES } from './examples.js';
+import * as History from './history.js';
+
+const METHOD_NAMES = {
+ exact: 'Exact state vector', statevector: 'State vector, double precision', statevector_single: 'State vector on the GPU, single precision',
+ stabilizer: 'Stabilizer tableau', pauli: 'Sparse Pauli dynamics', mps: 'Matrix product state', tensor_network: 'Tensor network',
+ cutrank: 'Cut-rank sum', density_matrix: 'Density matrix', trajectories: 'Trajectories',
+};
+const ACCURACY = {
+ 'bit-exact': 'The same bits on every machine.',
+ exact: 'No approximation beyond double-precision rounding.',
+ single: 'No approximation beyond single-precision rounding, about 10⁻⁷ per gate.',
+ bounded: 'An approximation, each value with a proven bound on its error.',
+ estimate: 'A statistical estimate, each value with its standard error.',
+ heuristic: 'An approximation, with the standard estimate of its error but no bound.',
+};
+const NOISE = [['depolarizing_1q', 'one-qubit depolarizing'], ['depolarizing_2q', 'two-qubit depolarizing'], ['amplitude_damping', 'amplitude damping'], ['dephasing', 'dephasing'], ['readout', 'readout flips']];
+const DRAFT = 'wai.quantum-studio.draft';
+
+const el = (tag, attrs = {}, ...kids) => {
+ const e = document.createElement(tag);
+ for (const [k, v] of Object.entries(attrs)) {
+ if (k === 'class') e.className = v;
+ else if (k === 'text') e.textContent = v;
+ else if (k.startsWith('on')) e.addEventListener(k.slice(2), v);
+ else if (v === true) e.setAttribute(k, '');
+ else if (v !== false && v != null) e.setAttribute(k, v);
+ }
+ for (const k of kids.flat()) if (k != null) e.append(k);
+ return e;
+};
+const ms = (t) => (t < 1 ? 'under 1 ms' : t < 10000 ? `${Math.round(t)} ms` : `${(t / 1000).toFixed(1)} s`);
+const compact = (b) => (b.length <= 24 ? b : `${b.slice(0, 8)}…${b.slice(-8)} (${b.length} bits)`);
+const split = (s) => s.split(/[,\n]/).map((x) => x.trim()).filter(Boolean);
+
+function mount(root) {
+ const desktop = onDesktop();
+ const local = desktop ? new DesktopEngine() : new TabEngine(root.dataset.enginePkg, root.dataset.worker || new URL('worker.js', import.meta.url).href);
+ const service = root.dataset.service ? new ServiceEngine(root.dataset.service) : null;
+ let where = 'local';
+ let current = null; // {job, where, took}
+ let running = null;
+ let serviceKey = null;
+ let mySigner = null;
+
+ // ---- the frame ----
+ const status = el('span', { class: 'qs-status', role: 'status', 'aria-live': 'polite' });
+ const runBtn = el('button', { class: 'qs-btn qs-primary', id: 'qs-run', type: 'button', title: 'Run (Ctrl or ⌘ + Enter)', text: '▶ Run' });
+ const cancelBtn = el('button', { class: 'qs-btn', id: 'qs-cancel', type: 'button', hidden: true, text: 'Cancel' });
+ const whereLocal = el('button', { class: 'qs-seg on', type: 'button', 'aria-pressed': 'true', 'data-where': 'local', text: desktop ? 'This computer' : 'This browser' });
+ const whereService = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'service', text: 'The service', disabled: !service });
+ const examples = el('select', { class: 'qs-select', id: 'qs-example', 'aria-label': 'Load an example' },
+ el('option', { value: '', text: 'Examples…' }), ...EXAMPLES.map((e) => el('option', { value: e.id, text: e.title })));
+ const header = el('header', { class: 'qs-top' },
+ el('div', { class: 'qs-brand' }, el('span', { class: 'qs-mark', 'aria-hidden': 'true', text: '⟨ψ⟩' }), el('h1', { text: 'Quantum Studio' })),
+ examples,
+ el('div', { class: 'qs-where', role: 'group', 'aria-label': 'Run on' }, whereLocal, whereService),
+ runBtn, cancelBtn);
+
+ // ---- the program ----
+ const lintBadge = el('span', { class: 'qs-badge', id: 'qs-lint', text: '…' });
+ const fileIn = el('input', { type: 'file', accept: '.qasm,.txt,text/plain', hidden: true, 'aria-hidden': 'true' });
+ const shows = el('p', { class: 'qs-shows', id: 'qs-shows' });
+ const editorHost = el('div', { class: 'qs-editor', id: 'qs-editor' });
+ const shareBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Copy link', title: 'A link with the program in its fragment, which no server sees' });
+ const openBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Open' });
+ const saveBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Save' });
+
+ const field = (id, label, attrs = {}) => {
+ const input = el(attrs.tag || 'input', { class: 'qs-input', id, ...attrs, tag: null });
+ return [input, el('label', { class: 'qs-field', for: id }, el('span', { text: label }), input)];
+ };
+ const [shots, shotsF] = field('qs-shots', 'Shots', { type: 'number', min: '0', step: '1', placeholder: '1024', inputmode: 'numeric' });
+ const [seed, seedF] = field('qs-seed', 'Seed', { type: 'number', min: '0', step: '1', placeholder: '1', inputmode: 'numeric' });
+ const method = el('select', { class: 'qs-input', id: 'qs-method' }, el('option', { value: 'auto', text: 'auto', title: 'The strongest method the program admits' }));
+ const methodF = el('label', { class: 'qs-field', for: 'qs-method' }, el('span', { text: 'Method' }), method);
+ const [obs, obsF] = field('qs-obs', 'Observables', { placeholder: 'Z0 Z1, 0.5 X2', autocomplete: 'off', spellcheck: 'false' });
+ const [amps, ampsF] = field('qs-amps', 'Amplitudes', { placeholder: '000, 111', autocomplete: 'off', spellcheck: 'false' });
+ const noiseInputs = NOISE.map(([k, label]) => field(`qs-noise-${k}`, label, { type: 'number', min: '0', max: '1', step: '0.001', placeholder: '0', 'data-noise': k }));
+ const jobBox = el('div', { class: 'qs-job' },
+ el('div', { class: 'qs-row' }, shotsF, seedF, methodF),
+ el('div', { class: 'qs-row' }, obsF, ampsF),
+ el('details', { class: 'qs-noise' }, el('summary', { text: 'Noise' }), el('div', { class: 'qs-row' }, ...noiseInputs.map((n) => n[1]))));
+ const program = el('section', { class: 'qs-pane qs-program', 'aria-labelledby': 'qs-program-h' },
+ el('div', { class: 'qs-head' }, el('h2', { id: 'qs-program-h', text: 'Program' }), lintBadge, el('span', { class: 'qs-grow' }), openBtn, saveBtn, shareBtn, fileIn),
+ shows, editorHost, jobBox);
+
+ // ---- the circuit ----
+ const circuit = el('pre', { class: 'qs-circuit', id: 'qs-circuit', tabindex: '0', 'aria-label': 'The circuit, drawn as text' });
+ const circuitPane = el('section', { class: 'qs-pane qs-circ', 'aria-labelledby': 'qs-circuit-h' },
+ el('div', { class: 'qs-head' }, el('h2', { id: 'qs-circuit-h', text: 'Circuit' }), el('span', { class: 'qs-grow' }), el('span', { class: 'qs-note', id: 'qs-plan' })), circuit);
+
+ // ---- the result ----
+ const state = el('span', { class: 'qs-badge', id: 'qs-state', text: 'ready' });
+ const resultBody = el('div', { class: 'qs-result', id: 'qs-result' }, el('p', { class: 'qs-empty', text: 'Run the program to see its result here. Counts are keyed by the classical bits, highest bit first.' }));
+ const resultPane = el('section', { class: 'qs-pane qs-res', 'aria-labelledby': 'qs-result-h' },
+ el('div', { class: 'qs-head' }, el('h2', { id: 'qs-result-h', text: 'Result' }), state), resultBody);
+
+ // ---- the history ----
+ const historyList = el('ol', { class: 'qs-history', id: 'qs-history' });
+ const historyPane = el('section', { class: 'qs-pane qs-hist', 'aria-labelledby': 'qs-history-h' },
+ el('div', { class: 'qs-head' }, el('h2', { id: 'qs-history-h', text: 'History' }), el('span', { class: 'qs-grow' }),
+ el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Clear', onclick: async () => { await History.clear(); showHistory(); } })),
+ historyList);
+
+ const footer = el('footer', { class: 'qs-foot' }, el('span', { class: 'qs-signer', id: 'qs-signer' }), status);
+ root.classList.add('qs');
+ root.replaceChildren(header, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, resultPane, historyPane)), footer);
+
+ // ---- the editor ----
+ const style = HighlightStyle.define([
+ { tag: t.keyword, color: 'var(--qs-kw)' },
+ { tag: t.typeName, color: 'var(--qs-type)' },
+ { tag: t.function(t.variableName), color: 'var(--qs-gate)', fontWeight: '600' },
+ { tag: t.number, color: 'var(--qs-num)' },
+ { tag: t.atom, color: 'var(--qs-num)' },
+ { tag: t.standard(t.variableName), color: 'var(--qs-type)' },
+ { tag: t.string, color: 'var(--qs-str)' },
+ { tag: t.comment, color: 'var(--text-muted)', fontStyle: 'italic' },
+ { tag: t.operator, color: 'var(--text-secondary)' },
+ ]);
+ const lintAt = async (view) => {
+ const text = view.state.doc.toString();
+ let v;
+ try { v = await local.lint(text); } catch (e) { lintBadge.textContent = 'engine not loaded'; lintBadge.className = 'qs-badge bad'; return []; }
+ if (v.ok) {
+ lintBadge.textContent = `${v.qubits} qubits · ${v.gates} gates${v.dynamic_steps != null ? ' · measures in the middle' : ''}`;
+ lintBadge.className = 'qs-badge ok';
+ return [];
+ }
+ lintBadge.textContent = 'problem'; lintBadge.className = 'qs-badge bad';
+ const doc = view.state.doc;
+ const line = v.line && v.line <= doc.lines ? doc.line(v.line) : doc.line(doc.lines);
+ return [{ from: line.from, to: Math.max(line.to, line.from), severity: 'error', message: v.error }];
+ };
+ const theme = EditorView.theme({
+ '&': { height: '100%', fontSize: '13px', backgroundColor: 'var(--bg-base)', color: 'var(--text-primary)' },
+ '.cm-content': { fontFamily: 'var(--font-mono)', caretColor: 'var(--accent)' },
+ '.cm-gutters': { backgroundColor: 'var(--bg-surface)', color: 'var(--text-muted)', borderRight: '1px solid var(--border)' },
+ '.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--accent) 6%, transparent)' },
+ '.cm-activeLineGutter': { backgroundColor: 'color-mix(in srgb, var(--accent) 10%, transparent)' },
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'color-mix(in srgb, var(--accent) 25%, transparent) !important' },
+ '.cm-tooltip': { backgroundColor: 'var(--bg-elevated)', border: '1px solid var(--border-subtle)', color: 'var(--text-primary)' },
+ });
+ const view = new EditorView({
+ parent: editorHost,
+ state: EditorState.create({
+ doc: '',
+ extensions: [
+ lineNumbers(), highlightActiveLineGutter(), highlightActiveLine(), history(), drawSelection(), indentOnInput(), bracketMatching(), closeBrackets(),
+ highlightSelectionMatches(), openqasm, syntaxHighlighting(style), autocompletion({ override: [complete] }),
+ linter(lintAt, { delay: 300 }), lintGutter(), theme,
+ EditorView.contentAttributes.of({ 'aria-label': 'The OpenQASM program' }),
+ keymap.of([{ key: 'Mod-Enter', run: () => { run(); return true; } }, { key: 'Escape', run: () => { if (running) { cancel(); return true; } return false; } },
+ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...completionKeymap, indentWithTab]),
+ EditorView.updateListener.of((u) => { if (u.docChanged) edited(); }),
+ ],
+ }),
+ });
+ const text = () => view.state.doc.toString();
+ const setText = (s) => view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: s } });
+
+ // ---- the job ----
+ function request() {
+ const job = { qasm: text() };
+ const n = parseInt(shots.value, 10); if (Number.isFinite(n) && n > 0) job.shots = n;
+ const s = parseInt(seed.value, 10); if (Number.isFinite(s) && s >= 0 && seed.value !== '') job.seed = s;
+ const o = split(obs.value); if (o.length) job.observables = o;
+ const a = split(amps.value); if (a.length) job.amplitudes = a;
+ const noise = {};
+ for (const [input] of noiseInputs) { const v = parseFloat(input.value); if (Number.isFinite(v) && v > 0) noise[input.dataset.noise] = v; }
+ if (Object.keys(noise).length) job.noise = noise;
+ if (method.value !== 'auto') job.method = method.value;
+ return job;
+ }
+ function fill(job) {
+ setText(job.qasm || '');
+ shots.value = job.shots ?? ''; seed.value = job.seed ?? '';
+ obs.value = (job.observables || []).join(', '); amps.value = (job.amplitudes || []).join(', ');
+ for (const [input] of noiseInputs) input.value = job.noise?.[input.dataset.noise] ?? '';
+ method.value = [...method.options].some((o) => o.value === job.method) ? job.method : 'auto';
+ if (job.noise && Object.keys(job.noise).length) jobBox.querySelector('details').open = true;
+ }
+
+ // ---- drawing and planning, as the program changes ----
+ let drawTimer = null;
+ function edited() {
+ shows.textContent = '';
+ clearTimeout(drawTimer);
+ drawTimer = setTimeout(async () => {
+ const src = text();
+ try { localStorage.setItem(DRAFT, src); } catch (e) {}
+ try {
+ const d = await local.draw(src, Math.max(40, Math.floor(circuit.clientWidth / 7.8) || 80));
+ circuit.textContent = d.text ?? '';
+ circuit.classList.toggle('stale', !!d.error);
+ if (d.error) circuit.textContent = d.error;
+ } catch (e) { circuit.textContent = e.message; circuit.classList.add('stale'); }
+ plan();
+ }, 250);
+ }
+ async function plan() {
+ const p = document.getElementById('qs-plan');
+ if (where !== 'local') { p.textContent = ''; return; }
+ try {
+ const pl = await local.plan(request());
+ p.textContent = `${METHOD_NAMES[pl.method] || pl.method} · ${(pl.memory_bytes / 2 ** 20).toFixed(0)} MiB at most`;
+ p.title = pl.why;
+ } catch (e) { p.textContent = e.message.length > 90 ? e.message.slice(0, 90) + '…' : e.message; p.title = e.message; }
+ }
+
+ // ---- running ----
+ function setStatus(s, cls = '') { status.textContent = s; status.className = 'qs-status ' + cls; }
+ function busy(on) { runBtn.disabled = on; cancelBtn.hidden = !on; }
+ async function run() {
+ if (running) return;
+ const job = request();
+ const eng = where === 'service' ? service : local;
+ running = eng; busy(true);
+ state.textContent = 'running'; state.className = 'qs-badge run';
+ setStatus(`Running on ${eng.where}…`);
+ const t0 = performance.now();
+ try {
+ const body = await eng.run(job, (j) => setStatus(`On the service: ${j.status}…`));
+ const took = performance.now() - t0;
+ current = { job: body, where: eng.where, took };
+ await show(current);
+ setStatus(`Done on ${eng.where} in ${ms(took)}.`, 'ok');
+ await History.save({ key: body.result?.receipt?.digest || String(Date.now()), saved: Date.now(), where: eng.where, title: firstLine(job.qasm), job: body });
+ showHistory();
+ } catch (e) {
+ state.textContent = e.message === 'cancelled' ? 'cancelled' : 'refused'; state.className = 'qs-badge bad';
+ setStatus(e.message === 'cancelled' ? 'Cancelled.' : e.message, 'bad');
+ } finally { running = null; busy(false); }
+ }
+ function cancel() { running?.cancel(); }
+ const firstLine = (q) => (q.split('\n').find((l) => /\b(h|x|cx|rx|ry|rz|ccx|measure)\b/.test(l)) || q.split('\n')[0] || '').trim().slice(0, 60);
+
+ // ---- a result ----
+ async function show({ job, where: ran, took }) {
+ const r = job.result || {};
+ state.textContent = job.status || 'done'; state.className = 'qs-badge ok';
+ const acc = r.accuracy || {};
+ const extra = [];
+ if (acc.fidelity_estimate !== undefined) extra.push(`fidelity estimate ${Number(acc.fidelity_estimate).toFixed(4)}`);
+ if (acc.discarded_weight !== undefined) extra.push(`discarded weight ${Number(acc.discarded_weight).toExponential(2)}`);
+ if (acc.histories !== undefined) extra.push(`${acc.histories} error histories`);
+ if (acc.device) extra.push(`on ${acc.device}`);
+ if (acc.max_cut !== undefined) extra.push(`the widest cut ${acc.max_cut} bits`);
+ if (r.global_phase) extra.push(`global phase ${r.global_phase}`);
+ const dy = r.dynamic;
+ if (dy) {
+ const parts = [`${dy.measurements_and_resets} measurements and resets`];
+ if (dy.branches_run !== undefined) parts.push(`the shots split into ${dy.branches_run} branches`);
+ if (dy.branches_weighed !== undefined) parts.push(`the expectation values weigh ${dy.branches_weighed} branches exactly`);
+ extra.push(parts.join('; '));
+ }
+ const blocks = [
+ el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Method' }),
+ el('div', {}, el('b', { class: 'qs-method', text: METHOD_NAMES[job.method] || job.method || '' }), el('span', { class: 'qs-why', text: ' ' + (job.why || '') }))),
+ el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Accuracy' }),
+ el('div', {}, el('span', { class: 'qs-badge ok', text: acc.class || '?' }), el('span', { class: 'qs-why', text: ' ' + (ACCURACY[acc.class] || '') + (extra.length ? ' ' + extra.join(', ') + '.' : '') })),
+ r.warning ? el('p', { class: 'qs-warn', text: r.warning }) : null),
+ ];
+ if (r.counts) {
+ const entries = Object.entries(r.counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
+ const total = entries.reduce((s, [, n]) => s + n, 0) + (r.omitted_shots || 0);
+ const max = entries.length ? entries[0][1] : 1;
+ blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: `Counts · ${total} shots · highest bit first` }),
+ el('div', { class: 'qs-bars' }, ...entries.slice(0, 32).map(([k, n]) => el('div', { class: 'qs-bar' },
+ el('span', { class: 'qs-key', title: k, text: compact(k) }),
+ el('span', { class: 'qs-track' }, el('span', { class: 'qs-fill', style: `width:${(100 * n) / max}%` })),
+ el('span', { class: 'qs-n', text: `${n} · ${((100 * n) / total).toFixed(1)}%` })))),
+ entries.length > 32 ? el('p', { class: 'qs-note', text: `${entries.length - 32} more outcomes.` }) : null));
+ }
+ if (r.expectations) {
+ blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Expectation values' }),
+ el('table', { class: 'qs-table' }, el('tbody', {}, ...r.expectations.map((e) => el('tr', {},
+ el('th', { scope: 'row', text: `⟨${e.observable}⟩` }), el('td', { class: 'num', text: Number(e.value).toFixed(6) }),
+ el('td', { class: 'qs-note', text: e.stderr !== undefined ? `± ${Number(e.stderr).toExponential(1)}` : e.bound !== undefined ? `error ≤ ${Number(e.bound).toExponential(1)}` : '' })))))));
+ }
+ if (r.amplitudes) {
+ blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Amplitudes' }),
+ el('table', { class: 'qs-table' }, el('thead', {}, el('tr', {}, el('th', { text: 'state' }), el('th', { text: 'amplitude' }), el('th', { text: 'probability' }), el('th', { text: 'phase' }))),
+ el('tbody', {}, ...r.amplitudes.map((a) => {
+ const p = a.re * a.re + a.im * a.im;
+ const ph = Math.atan2(a.im, a.re) / Math.PI;
+ return el('tr', {}, el('th', { scope: 'row', text: `|${compact(a.bits)}⟩` }),
+ el('td', { class: 'num', text: `${Number(a.re).toFixed(6)} ${a.im < 0 ? '−' : '+'} ${Math.abs(a.im).toFixed(6)}i` }),
+ el('td', { class: 'num', text: p.toFixed(6) }), el('td', { class: 'num', text: p < 1e-24 ? '—' : `${ph.toFixed(4)}π` }));
+ })))));
+ }
+ // The receipt, checked here.
+ const rc = r.receipt || {};
+ const check = el('span', { class: 'qs-badge', id: 'qs-verified', text: 'checking…' });
+ const en = r.energy || {};
+ const energy = en.joules_micro ? `${en.joules_micro < 1e3 ? en.joules_micro + ' µJ' : en.joules_micro < 1e6 ? (en.joules_micro / 1e3).toFixed(1) + ' mJ' : (en.joules_micro / 1e6).toFixed(2) + ' J'}` : en.unmetered ? `unmetered: ${en.unmetered}` : '—';
+ const dl = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Download job', onclick: () => saveJob(job) });
+ blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Receipt' }),
+ el('table', { class: 'qs-table' }, el('tbody', {},
+ el('tr', {}, el('th', { scope: 'row', text: 'ran on' }), el('td', { text: ran })),
+ el('tr', {}, el('th', { scope: 'row', text: 'signed by' }), el('td', { class: 'mono', id: 'qs-signed-by', text: (rc.key || '').slice(0, 16) + '…' })),
+ el('tr', {}, el('th', { scope: 'row', text: 'check' }), el('td', {}, check)),
+ el('tr', {}, el('th', { scope: 'row', text: 'ran in' }), el('td', { text: r.elapsed_ms != null ? ms(r.elapsed_ms) : `${ms(took)}, timed by this page` })),
+ el('tr', {}, el('th', { scope: 'row', text: 'energy' }), el('td', { text: energy })))),
+ el('p', { class: 'qs-note', text: 'A receipt shows that this key signed this request and this result; anyone can run the request again to compare. It does not say who holds the key.' }),
+ el('div', { class: 'qs-actions' }, dl)));
+ resultBody.replaceChildren(...blocks);
+ try {
+ const v = await local.verify({ request: job.request, result: job.result });
+ const whose = v.key === serviceKey ? 'the service’s key' : v.signer && v.signer === mySigner ? (desktop ? 'this computer’s key' : 'this browser’s key') : 'the key it names';
+ document.getElementById('qs-signed-by').textContent = `${whose} ${v.signer ? v.signer.slice(0, 28) + '…' : ''}`;
+ check.textContent = v.valid ? `✓ verifies, checked on ${desktop ? 'this computer' : 'this device'}` : '✗ does not verify';
+ check.className = 'qs-badge ' + (v.valid ? 'ok' : 'bad');
+ } catch (e) { check.textContent = `not checked: ${e.message}`; check.className = 'qs-badge bad'; }
+ }
+
+ // ---- files ----
+ function download(name, text, type) {
+ const a = el('a', { href: URL.createObjectURL(new Blob([text], { type })), download: name });
+ document.body.append(a); a.click(); a.remove();
+ setTimeout(() => URL.revokeObjectURL(a.href), 0);
+ }
+ async function saveJob(job) {
+ const name = `wai-quantum-job-${job.id || (job.result?.receipt?.digest || 'here').slice(0, 12)}.json`;
+ if (desktop) { const r = await local.saveJob(name, job); setStatus(`Saved ${r.path}.`, 'ok'); }
+ else { download(name, JSON.stringify(job, null, 2) + '\n', 'application/json'); setStatus(`Saved ${name}. wai-quantum verify checks it.`, 'ok'); }
+ }
+ saveBtn.addEventListener('click', async () => {
+ const name = 'program.qasm';
+ if (desktop) { const r = await local.saveProgram(name, text()); setStatus(`Saved ${r.path}.`, 'ok'); }
+ else download(name, text(), 'text/plain');
+ });
+ openBtn.addEventListener('click', () => fileIn.click());
+ fileIn.addEventListener('change', async () => {
+ const f = fileIn.files?.[0]; if (!f) return;
+ setText(await f.text()); shows.textContent = `Opened ${f.name}.`; fileIn.value = '';
+ });
+ shareBtn.addEventListener('click', () => {
+ const u = new URL(location.href); u.hash = 'qasm=' + encodeURIComponent(text()); u.search = '';
+ navigator.clipboard?.writeText(u.toString()).then(() => setStatus('Copied a link with the program in its fragment, which no server sees.', 'ok'), () => setStatus(u.toString()));
+ });
+
+ // ---- history ----
+ async function showHistory() {
+ const all = await History.list();
+ historyList.replaceChildren(...(all.length ? all.slice(0, 50).map((h) => el('li', {},
+ el('button', { type: 'button', class: 'qs-hitem', onclick: () => { fill(h.job.request || {}); current = { job: h.job, where: h.where, took: 0 }; show(current); setStatus(`From the history: run on ${h.where}, ${new Date(h.saved).toLocaleString()}.`); } },
+ el('span', { class: 'qs-htitle', text: h.title || '(a program)' }),
+ el('span', { class: 'qs-note', text: `${METHOD_NAMES[h.job.method] || h.job.method} · ${h.where} · ${new Date(h.saved).toLocaleTimeString()}` })))) : [el('li', { class: 'qs-note', text: 'Jobs you run appear here, kept on this device.' })]));
+ }
+
+ // ---- where ----
+ for (const b of [whereLocal, whereService]) b.addEventListener('click', () => {
+ where = b.dataset.where;
+ for (const x of [whereLocal, whereService]) { x.classList.toggle('on', x === b); x.setAttribute('aria-pressed', String(x === b)); }
+ loadMethods(); plan();
+ });
+ async function loadMethods() {
+ const keep = method.value;
+ try {
+ const m = (await (where === 'service' ? service : local).methods()).methods || [];
+ method.replaceChildren(el('option', { value: 'auto', text: 'auto', title: 'The strongest method the program admits' }),
+ ...m.filter((x) => x.available !== false).map((x) => el('option', { value: x.method, text: `${METHOD_NAMES[x.method] || x.method} (to ${x.max_qubits} qubits)` })));
+ method.value = [...method.options].some((o) => o.value === keep) ? keep : 'auto';
+ } catch (e) {}
+ }
+ examples.addEventListener('change', () => {
+ const e = EXAMPLES.find((x) => x.id === examples.value); if (!e) return;
+ fill(e.job); shows.textContent = e.shows; examples.value = '';
+ });
+ [shots, seed, method, obs, amps, ...noiseInputs.map((n) => n[0])].forEach((i) => i.addEventListener('input', () => { clearTimeout(drawTimer); drawTimer = setTimeout(plan, 250); }));
+ runBtn.addEventListener('click', run);
+ cancelBtn.addEventListener('click', cancel);
+
+ // ---- start ----
+ const handed = new URLSearchParams(location.hash.slice(1)).get('qasm');
+ let draft = null;
+ try { draft = localStorage.getItem(DRAFT); } catch (e) {}
+ if (handed) { fill({ qasm: handed }); shows.textContent = 'The program from the link.'; }
+ else if (draft) setText(draft);
+ else { fill(EXAMPLES[0].job); shows.textContent = EXAMPLES[0].shows; }
+ loadMethods();
+ showHistory();
+ local.signer().then((d) => { mySigner = d; document.getElementById('qs-signer').textContent = `Signs as ${d.slice(0, 32)}… (${desktop ? 'this computer’s key' : 'a key made for this browser'})`; }).catch(() => {});
+ service?.describe().then((d) => { serviceKey = d.receipt_key || null; }).catch(() => { whereService.disabled = true; whereService.title = 'The service did not answer'; });
+ edited();
+ forceLinting(view);
+ return { view, run, fill, request };
+}
+
+const root = document.getElementById('studio');
+if (root) window.quantumStudio = mount(root);
diff --git a/wai/quantum-studio/src/qasm-lang.js b/wai/quantum-studio/src/qasm-lang.js
new file mode 100644
index 000000000..05a436455
--- /dev/null
+++ b/wai/quantum-studio/src/qasm-lang.js
@@ -0,0 +1,83 @@
+// OpenQASM for the editor: highlighting, the gates and keywords to complete,
+// and the registers a program declares. The engine's reader decides what a
+// program means; this only colours and suggests.
+import { StreamLanguage } from '@codemirror/language';
+
+export const KEYWORDS = [
+ 'OPENQASM', 'include', 'qubit', 'bit', 'qreg', 'creg', 'gate', 'measure', 'reset', 'barrier',
+ 'if', 'else', 'for', 'while', 'in', 'let', 'const', 'input', 'output', 'def', 'return',
+ 'ctrl', 'negctrl', 'inv', 'pow', 'box', 'delay', 'opaque', 'U', 'CX',
+];
+const TYPES = new Set(['qubit', 'bit', 'qreg', 'creg', 'int', 'uint', 'float', 'angle', 'bool', 'complex']);
+export const CONSTANTS = ['pi', 'tau', 'euler', 'π', 'τ'];
+export const FUNCTIONS = ['sin', 'cos', 'tan', 'exp', 'ln', 'sqrt', 'arcsin', 'arccos', 'arctan'];
+// The standard gates, with their parameters, as the engine reads them.
+export const GATES = [
+ ['id', 0, 1], ['x', 0, 1], ['y', 0, 1], ['z', 0, 1], ['h', 0, 1], ['s', 0, 1], ['sdg', 0, 1], ['t', 0, 1], ['tdg', 0, 1],
+ ['sx', 0, 1], ['sxdg', 0, 1], ['rx', 1, 1], ['ry', 1, 1], ['rz', 1, 1], ['p', 1, 1], ['u', 3, 1], ['u1', 1, 1], ['u2', 2, 1], ['u3', 3, 1],
+ ['cx', 0, 2], ['cy', 0, 2], ['cz', 0, 2], ['ch', 0, 2], ['cp', 1, 2], ['crx', 1, 2], ['cry', 1, 2], ['crz', 1, 2], ['cu', 4, 2],
+ ['swap', 0, 2], ['iswap', 0, 2], ['rxx', 1, 2], ['ryy', 1, 2], ['rzz', 1, 2], ['ccx', 0, 3], ['ccz', 0, 3], ['cswap', 0, 3],
+];
+const GATE_NAMES = new Set(GATES.map((g) => g[0]).concat(['cnot', 'toffoli', 'fredkin', 'phase', 'cphase', 'cu1', 'cu3']));
+const KEYWORD_SET = new Set(KEYWORDS);
+
+export const openqasm = StreamLanguage.define({
+ name: 'openqasm',
+ startState: () => ({ inComment: false }),
+ token(stream, state) {
+ if (state.inComment) {
+ if (stream.match(/^.*?\*\//)) state.inComment = false;
+ else stream.skipToEnd();
+ return 'comment';
+ }
+ if (stream.eatSpace()) return null;
+ if (stream.match('//')) { stream.skipToEnd(); return 'comment'; }
+ if (stream.match('/*')) { state.inComment = true; return 'comment'; }
+ if (stream.match(/^"[^"]*"?/)) return 'string';
+ if (stream.match(/^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?(ns|us|µs|ms|s|dt|im)?/)) return 'number';
+ if (stream.match(/^[A-Za-z_$πτ][\w$]*/u)) {
+ const w = stream.current();
+ if (TYPES.has(w)) return 'typeName';
+ if (KEYWORD_SET.has(w)) return 'keyword';
+ if (CONSTANTS.includes(w)) return 'atom';
+ if (FUNCTIONS.includes(w)) return 'builtin';
+ if (GATE_NAMES.has(w)) return 'variableName.function';
+ return 'variableName';
+ }
+ if (stream.match(/^(->|==|!=|<=|>=|&&|\|\||<<|>>|\*\*|[+\-*/%^=<>!~&|@])/)) return 'operator';
+ stream.next();
+ return null;
+ },
+ languageData: { commentTokens: { line: '//', block: { open: '/*', close: '*/' } } },
+});
+
+// The registers a program declares, for completion.
+export function registers(text) {
+ const out = [];
+ const re = /\b(qubit|bit)\s*(\[\s*\d+\s*\])?\s+([A-Za-z_]\w*)|\b(qreg|creg)\s+([A-Za-z_]\w*)/g;
+ for (let m; (m = re.exec(text)); ) out.push({ name: m[3] || m[5], kind: m[1] === 'qubit' || m[4] === 'qreg' ? 'qubit' : 'bit' });
+ return out;
+}
+
+// Completions at the cursor: gates with their parameters, keywords, constants
+// and the program's registers.
+export function complete(context) {
+ const word = context.matchBefore(/[\w$]*/);
+ if (!word || (word.from === word.to && !context.explicit)) return null;
+ const regs = registers(context.state.doc.toString());
+ return {
+ from: word.from,
+ options: [
+ ...GATES.map(([name, params, qubits]) => ({
+ label: name,
+ type: 'function',
+ detail: `${params ? `${params} angle${params > 1 ? 's' : ''}, ` : ''}${qubits} qubit${qubits > 1 ? 's' : ''}`,
+ apply: params ? `${name}(${Array(params).fill('').join(', ')})` : name,
+ })),
+ ...KEYWORDS.map((k) => ({ label: k, type: 'keyword' })),
+ ...CONSTANTS.slice(0, 3).map((c) => ({ label: c, type: 'constant' })),
+ ...regs.map((r) => ({ label: r.name, type: 'variable', detail: r.kind === 'qubit' ? 'qubit register' : 'bit register' })),
+ ],
+ validFor: /^[\w$]*$/,
+ };
+}
diff --git a/wai/quantum-studio/src/studio.css b/wai/quantum-studio/src/studio.css
new file mode 100644
index 000000000..a746645e3
--- /dev/null
+++ b/wai/quantum-studio/src/studio.css
@@ -0,0 +1,101 @@
+/* Quantum Studio. Every colour and face comes from the host's design tokens
+ (tokens.css standing alone; the site's own inside the site), so it follows
+ the host's theme. Syntax colours are the Studio's, taken from the palette. */
+.qs {
+ --qs-kw: var(--accent-text, #d4a574);
+ --qs-type: var(--accent-text-hover, #e0bd95);
+ --qs-gate: var(--info-text, #7aa0ff);
+ --qs-num: var(--warn-text, #f0a020);
+ --qs-str: var(--std-text, #10b981);
+ --qs-gap: 12px;
+ --qs-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--qs-gap);
+ min-height: min(100vh, 1100px);
+ color: var(--text-primary);
+ background: var(--bg-base);
+ font-family: var(--font-sans, system-ui, sans-serif);
+ font-size: 14px;
+}
+.qs *, .qs *::before, .qs *::after { box-sizing: border-box; }
+.qs h1, .qs h2 { margin: 0; text-wrap: balance; }
+.qs-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--qs-radius); background: var(--bg-surface); }
+.qs-brand { display: flex; align-items: baseline; gap: 8px; margin-right: auto; }
+.qs-brand h1 { font-size: 15px; font-weight: 650; letter-spacing: 0.01em; }
+.qs-mark { font-family: var(--font-mono, ui-monospace, monospace); color: var(--accent-text); font-size: 14px; }
+.qs-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: var(--qs-gap); flex: 1; min-height: 0; }
+.qs-side { display: flex; flex-direction: column; gap: var(--qs-gap); min-width: 0; }
+.qs-pane { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--border); border-radius: var(--qs-radius); background: var(--bg-surface); overflow: hidden; }
+.qs-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
+.qs-head h2 { font-size: 11px; font-family: var(--font-mono, ui-monospace, monospace); font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-secondary); }
+.qs-grow { flex: 1; }
+.qs-program { min-height: 520px; }
+.qs-editor { flex: 1; min-height: 260px; overflow: auto; }
+.qs-editor .cm-editor { height: 100%; }
+.qs-editor .cm-editor.cm-focused { outline: 2px solid var(--focus-ring, var(--accent)); outline-offset: -2px; }
+.qs-shows { margin: 0; padding: 0 12px; font-size: 12px; color: var(--text-secondary); line-height: 1.45; }
+.qs-shows:not(:empty) { padding: 8px 12px; border-bottom: 1px solid var(--border); }
+.qs-job { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); }
+.qs-row { display: flex; flex-wrap: wrap; gap: 8px; }
+.qs-field { display: flex; flex-direction: column; gap: 3px; flex: 1 1 120px; min-width: 0; font-size: 10px; font-family: var(--font-mono, ui-monospace, monospace); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
+.qs-input, .qs-select { width: 100%; min-height: 32px; padding: 5px 8px; border: 1px solid var(--border-subtle); border-radius: 6px; background: var(--bg-base); color: var(--text-primary); font: 12px var(--font-mono, ui-monospace, monospace); letter-spacing: 0; text-transform: none; }
+.qs-select { width: auto; }
+.qs-noise summary { cursor: pointer; font: 11px var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); padding: 4px 0; }
+.qs-noise .qs-row { padding-top: 6px; }
+.qs-btn { min-height: 32px; padding: 5px 12px; border: 1px solid var(--border-subtle); border-radius: 6px; background: var(--bg-base); color: var(--text-primary); font: 12px var(--font-mono, ui-monospace, monospace); cursor: pointer; white-space: nowrap; }
+.qs-btn:hover:not(:disabled) { border-color: var(--accent); }
+.qs-btn:disabled { opacity: 0.5; cursor: default; }
+.qs-btn.qs-small { min-height: 26px; padding: 2px 9px; font-size: 11px; }
+.qs-primary { background: var(--accent-fill, var(--accent)); border-color: var(--accent-fill, var(--accent)); color: var(--on-accent, #0c0a09); font-weight: 650; }
+.qs-primary:hover:not(:disabled) { background: var(--accent-fill-hover, var(--accent)); }
+.qs-where { display: inline-flex; border: 1px solid var(--border-subtle); border-radius: 6px; overflow: hidden; }
+.qs-seg { min-height: 32px; padding: 5px 11px; border: 0; background: var(--bg-base); color: var(--text-secondary); font: 12px var(--font-mono, ui-monospace, monospace); cursor: pointer; }
+.qs-seg + .qs-seg { border-left: 1px solid var(--border-subtle); }
+.qs-seg.on { background: color-mix(in srgb, var(--accent) 18%, var(--bg-base)); color: var(--text-primary); }
+.qs-seg:disabled { opacity: 0.45; cursor: default; }
+.qs :focus-visible { outline: 2px solid var(--focus-ring, var(--accent)); outline-offset: 2px; }
+.qs-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border-subtle); font: 11px var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
+.qs-badge.ok { color: var(--ok-text); border-color: color-mix(in srgb, var(--ok-text) 40%, transparent); }
+.qs-badge.bad { color: var(--bad-text); border-color: color-mix(in srgb, var(--bad-text) 40%, transparent); }
+.qs-badge.run { color: var(--info-text); border-color: color-mix(in srgb, var(--info-text) 40%, transparent); }
+.qs-circuit { margin: 0; padding: 10px 12px; min-height: 120px; max-height: 320px; overflow: auto; font: 12px/1.35 var(--font-mono, ui-monospace, monospace); color: var(--text-primary); background: var(--bg-base); white-space: pre; }
+.qs-circuit.stale { color: var(--bad-text); white-space: pre-wrap; }
+.qs-note { font-size: 11px; color: var(--text-muted); font-family: var(--font-mono, ui-monospace, monospace); overflow-wrap: anywhere; }
+.qs-result { padding: 10px 12px; display: flex; flex-direction: column; gap: 12px; overflow: auto; }
+.qs-empty { margin: 0; color: var(--text-muted); font-size: 12px; line-height: 1.5; }
+.qs-block { display: flex; flex-direction: column; gap: 5px; }
+.qs-k { font: 10px var(--font-mono, ui-monospace, monospace); letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-muted); }
+.qs-method { font-weight: 650; }
+.qs-why { color: var(--text-secondary); font-size: 12px; line-height: 1.5; }
+.qs-warn { margin: 4px 0 0; color: var(--warn-text); font-size: 12px; }
+.qs-bars { display: flex; flex-direction: column; gap: 3px; }
+.qs-bar { display: grid; grid-template-columns: minmax(3ch, max-content) 1fr auto; gap: 8px; align-items: center; font: 12px var(--font-mono, ui-monospace, monospace); font-variant-numeric: tabular-nums; }
+.qs-track { height: 10px; border-radius: 3px; background: color-mix(in srgb, var(--text-muted) 15%, transparent); overflow: hidden; }
+.qs-fill { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
+.qs-n { color: var(--text-secondary); }
+.qs-table { width: 100%; border-collapse: collapse; font: 12px var(--font-mono, ui-monospace, monospace); font-variant-numeric: tabular-nums; }
+.qs-table th, .qs-table td { text-align: left; padding: 4px 6px; border-bottom: 1px solid var(--border); vertical-align: top; overflow-wrap: anywhere; }
+.qs-table th { color: var(--text-secondary); font-weight: 500; }
+.qs-table thead th { color: var(--text-muted); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; }
+.qs-table .num { text-align: right; }
+.qs-table .mono { color: var(--text-primary); }
+.qs-actions { display: flex; gap: 8px; flex-wrap: wrap; }
+.qs-hist { max-height: 260px; }
+.qs-history { list-style: none; margin: 0; padding: 4px; overflow: auto; }
+.qs-hitem { display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 6px 8px; border: 0; border-radius: 6px; background: transparent; color: var(--text-primary); text-align: left; cursor: pointer; font: inherit; }
+.qs-hitem:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
+.qs-htitle { font: 12px var(--font-mono, ui-monospace, monospace); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.qs-history > li.qs-note { padding: 8px; }
+.qs-foot { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 4px 2px; font: 11px var(--font-mono, ui-monospace, monospace); color: var(--text-muted); }
+.qs-status { margin-left: auto; color: var(--text-secondary); overflow-wrap: anywhere; }
+.qs-status.ok { color: var(--ok-text); }
+.qs-status.bad { color: var(--bad-text); }
+@media (max-width: 900px) {
+ .qs-grid { grid-template-columns: minmax(0, 1fr); }
+ .qs-program { min-height: 0; }
+ .qs-editor { min-height: 240px; max-height: 60vh; }
+ .qs-brand { margin-right: 0; flex-basis: 100%; }
+ .qs-status { margin-left: 0; }
+}
+@media (prefers-reduced-motion: reduce) { .qs * { transition: none !important; } }
diff --git a/wai/quantum-studio/src/tokens.css b/wai/quantum-studio/src/tokens.css
new file mode 100644
index 000000000..9c50d878f
--- /dev/null
+++ b/wai/quantum-studio/src/tokens.css
@@ -0,0 +1,178 @@
+/* The site's design tokens (wai-transaction-science-web src/styles/global.css), so the Studio
+ looks the same standing alone (the desktop app) as inside the site. Copied, not edited. */
+/* -- Design Tokens -- */
+:root {
+ /* Neutrals — warm-tinted blacks (shared with transaction.science) */
+ --stone-950: #0c0a09;
+ --stone-900: #1c1917;
+ --stone-800: #292524;
+ --stone-700: #44403c;
+ --stone-600: #57534e;
+ --stone-500: #78716c;
+ --stone-400: #a8a29e;
+ --stone-300: #d6d3d1;
+ --stone-200: #e7e5e4;
+ --stone-100: #f5f5f4;
+ --stone-50: #fafaf9;
+
+ /* Brand — WAI warm amber/tan.
+ --accent is decorative: borders, glows, the focus ring and selection.
+ Accent-coloured TEXT uses --accent-text; a button or tile uses --accent-fill with
+ --on-accent on it. The hues are the brand's; each theme picks the step that keeps
+ text at WCAG 2.2 AA (4.5:1) on the base and surface backgrounds. */
+ --accent: #d4a574;
+ --accent-light: #e0bd95;
+ --accent-dark: #a87f4f;
+ --accent-text: #d4a574;
+ --accent-text-hover: #e0bd95;
+ --accent-text-elevated: #d4a574;
+ --accent-fill: #d4a574;
+ --accent-fill-hover: #e0bd95;
+ --on-accent: #0c0a09;
+ /* The keyboard focus ring. It must reach 3:1 against every background it can sit on
+ (base, surface and the elevated theme toggle): in the light theme --accent is 2.88:1
+ on --bg-elevated, so the ring takes the darker step, which is 5.0:1 there. */
+ --focus-ring: #d4a574;
+
+ /* Status text on the demonstration pages: a verdict that holds, one that fails, a
+ warning, an informational value. Dark values are the pages' own; the light theme
+ takes a darker step of the same hue so the text stays at 4.5:1 or more. */
+ --ok-text: #00e5a0;
+ --std-text: #10b981;
+ --bad-text: #ff6b6b;
+ --error-text: #ef4444;
+ --warn-text: #f0a020;
+ --info-text: #7aa0ff;
+
+ /* Sibling platform colors */
+ --veritas-color: #0d9488;
+ --terra-color: #3b82f6;
+ --insights-color: #f59e0b;
+ --tradingos-color: #06b6d4;
+ --legalos-color: #8b5cf6;
+ --compliance-color: #f43f5e;
+ --settlement-color: #10b981;
+
+ /* Type — both faces are served from this site (above); a system face stands in only
+ until one loads, or where it cannot */
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
+
+ /* Semantic — dark by default */
+ --text-primary: var(--stone-100);
+ --text-secondary: var(--stone-400);
+ --text-muted: #948d88;
+ --bg-base: var(--stone-950);
+ --bg-surface: var(--stone-900);
+ --bg-elevated: var(--stone-800);
+ --border: var(--stone-800);
+ --border-subtle: var(--stone-700);
+ --nav-bg: rgba(12, 10, 9, 0.8);
+ --selection-color: white;
+ /* Headline gradient: light to mid stone on the dark page, dark to mid on the light
+ one, so both ends of a headline stay readable (7:1 or more at the faint end). */
+ --gradient-text-from: var(--stone-100);
+ --gradient-text-to: var(--stone-400);
+}
+
+/* -- Dark theme (explicit) -- */
+[data-theme="dark"] {
+ --text-primary: var(--stone-100);
+ --text-secondary: var(--stone-400);
+ --text-muted: #948d88;
+ --bg-base: var(--stone-950);
+ --bg-surface: var(--stone-900);
+ --bg-elevated: var(--stone-800);
+ --border: var(--stone-800);
+ --border-subtle: var(--stone-700);
+ --nav-bg: rgba(12, 10, 9, 0.8);
+ --selection-color: white;
+ --accent: #d4a574;
+ --accent-light: #e0bd95;
+ --accent-dark: #a87f4f;
+ --accent-text: #d4a574;
+ --accent-text-hover: #e0bd95;
+ --accent-text-elevated: #d4a574;
+ --accent-fill: #d4a574;
+ --accent-fill-hover: #e0bd95;
+ --on-accent: #0c0a09;
+ --focus-ring: #d4a574;
+ --ok-text: #00e5a0;
+ --std-text: #10b981;
+ --bad-text: #ff6b6b;
+ --error-text: #ef4444;
+ --warn-text: #f0a020;
+ --info-text: #7aa0ff;
+}
+
+/* -- Light theme -- */
+[data-theme="light"] {
+ --text-primary: var(--stone-900);
+ --text-secondary: var(--stone-600);
+ --text-muted: #6b645f;
+ --bg-base: var(--stone-50);
+ --bg-surface: var(--stone-100);
+ --bg-elevated: var(--stone-200);
+ --border: var(--stone-200);
+ --border-subtle: var(--stone-300);
+ --accent: #a87f4f;
+ /* In light mode the "light" step is the emphasis step used for code and hover
+ text, so it is the darker one: #d4a574 on the light surface is 2:1. */
+ --accent-light: #7a5a37;
+ --accent-dark: #8a6740;
+ --accent-text: #8a6740;
+ --accent-text-hover: #7a5a37;
+ /* accent text on --bg-elevated: #8a6740 is 4.08:1 there, one step darker is 5.2:1 */
+ --accent-text-elevated: #7a5a37;
+ --accent-fill: #8a6740;
+ --accent-fill-hover: #7a5a37;
+ --on-accent: #ffffff;
+ --focus-ring: #7a5a37;
+ --ok-text: #047857;
+ --std-text: #047857;
+ --bad-text: #b91c1c;
+ --error-text: #b91c1c;
+ --warn-text: #b45309;
+ --info-text: #1d4ed8;
+ --nav-bg: rgba(250, 250, 249, 0.8);
+ --selection-color: var(--stone-900);
+ --gradient-text-from: var(--stone-900);
+ --gradient-text-to: var(--stone-600);
+}
+
+/* -- Auto theme -- */
+[data-theme="auto"] {}
+
+@media (prefers-color-scheme: light) {
+ [data-theme="auto"] {
+ --text-primary: var(--stone-900);
+ --text-secondary: var(--stone-600);
+ --text-muted: #6b645f;
+ --bg-base: var(--stone-50);
+ --bg-surface: var(--stone-100);
+ --bg-elevated: var(--stone-200);
+ --border: var(--stone-200);
+ --border-subtle: var(--stone-300);
+ --accent: #a87f4f;
+ --accent-light: #7a5a37;
+ --accent-dark: #8a6740;
+ --accent-text: #8a6740;
+ --accent-text-hover: #7a5a37;
+ --accent-text-elevated: #7a5a37;
+ --accent-fill: #8a6740;
+ --accent-fill-hover: #7a5a37;
+ --on-accent: #ffffff;
+ --focus-ring: #7a5a37;
+ --ok-text: #047857;
+ --std-text: #047857;
+ --bad-text: #b91c1c;
+ --error-text: #b91c1c;
+ --warn-text: #b45309;
+ --info-text: #1d4ed8;
+ --nav-bg: rgba(250, 250, 249, 0.8);
+ --selection-color: var(--stone-900);
+ --gradient-text-from: var(--stone-900);
+ --gradient-text-to: var(--stone-600);
+ }
+}
+
diff --git a/wai/quantum-studio/src/worker.js b/wai/quantum-studio/src/worker.js
new file mode 100644
index 000000000..05ddc2262
--- /dev/null
+++ b/wai/quantum-studio/src/worker.js
@@ -0,0 +1,47 @@
+// The job service's engine, compiled for the browser, off the page's thread.
+// The page posts {id, op, pkg, seed, memoryMb, ...}; the first message loads
+// the engine package from `pkg`. Answers are {id, ok, value} or {id, ok: false,
+// error}. The page ends a long job by ending this worker.
+let loaded = null;
+let mod = null;
+let engine = null;
+let engineKey = null;
+
+async function load(pkg) {
+ loaded ??= import(pkg).then(async (m) => { await m.default(); mod = m; });
+ await loaded;
+}
+
+function engineFor(seed, memoryMb) {
+ const key = seed.join(',') + '/' + memoryMb;
+ if (!engine || engineKey !== key) {
+ engine?.free?.();
+ engine = new mod.QuantumEngine(new Uint8Array(seed), memoryMb);
+ engineKey = key;
+ }
+ return engine;
+}
+
+self.onmessage = async (ev) => {
+ const { id, op, pkg, seed, memoryMb } = ev.data;
+ try {
+ await load(pkg);
+ const e = () => engineFor(seed, memoryMb);
+ let text;
+ switch (op) {
+ case 'run': text = e().run(JSON.stringify(ev.data.request)); break;
+ case 'plan': text = e().plan(JSON.stringify(ev.data.request)); break;
+ case 'methods': text = e().methods(); break;
+ case 'signer': text = JSON.stringify({ did: e().signer_did() }); break;
+ case 'lint': text = mod.engine_lint(ev.data.qasm); break;
+ case 'draw': text = mod.engine_draw(ev.data.qasm, ev.data.width ?? 100); break;
+ case 'verify': text = mod.engine_verify(JSON.stringify(ev.data.job)); break;
+ default: throw new Error(`no operation ${op}`);
+ }
+ const value = JSON.parse(text);
+ if (value && value.error && op !== 'lint') self.postMessage({ id, ok: false, error: value.error });
+ else self.postMessage({ id, ok: true, value });
+ } catch (err) {
+ self.postMessage({ id, ok: false, error: String(err && err.message ? err.message : err) });
+ }
+};
diff --git a/wai/quantum-studio/test/studio.test.mjs b/wai/quantum-studio/test/studio.test.mjs
new file mode 100644
index 000000000..f3cbf7fa0
--- /dev/null
+++ b/wai/quantum-studio/test/studio.test.mjs
@@ -0,0 +1,148 @@
+// The Studio in the installed browser, as a reader uses it:
+//
+// node build.mjs --pkg PATH/TO/ENGINE/PKG && node test/studio.test.mjs
+//
+// Serves dist/ (with the engine package in dist/pkg/); the page's index.html names the
+// live job service, https://wai.transaction.science/quantum-jobs, which the service checks use.
+// - the first example loads; the program is linted, drawn and planned as it is typed;
+// - it runs in this browser: the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, the
+// receipt checked here and signed by this browser's key;
+// - an error in the program is marked on its line;
+// - a rotation runs on the state vector, ⟨Z0⟩ = cos 0.3;
+// - the same Bell job on the service gives the counts run here, shot for shot, and its
+// receipt checks here as the service's (left out with STUDIO_SKIP_SERVICE=1);
+// - Ctrl+Enter in the editor runs; the program and the history survive a reload;
+// - no console error and no failed request; at a phone's width nothing scrolls sideways.
+// Exit 0 iff every check passes.
+import { createServer } from 'node:http';
+import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
+import { dirname, extname, join, normalize, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
+const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
+const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots');
+const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm', '.json': 'application/json' };
+const server = createServer((req, res) => {
+ let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
+ if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; }
+ if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
+ if (!existsSync(file)) { res.writeHead(404).end('not found'); return; }
+ res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' });
+ res.end(readFileSync(file));
+});
+await new Promise((r) => server.listen(0, '127.0.0.1', r));
+const ORIGIN = `http://127.0.0.1:${server.address().port}`;
+let fails = 0;
+const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
+const LONG = 120_000;
+const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
+
+async function open(viewport = { width: 1400, height: 950 }) {
+ const ctx = await browser.newContext({ viewport, colorScheme: 'dark' });
+ const page = await ctx.newPage();
+ const seen = { errors: [], failed: [] };
+ page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); });
+ page.on('pageerror', (e) => seen.errors.push(String(e)));
+ page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`));
+ page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); });
+ await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' });
+ return { ctx, page, seen };
+}
+const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG });
+const verified = (page) => page.waitForFunction(() => /verifies|does not verify|not checked/.test(document.querySelector('#qs-verified')?.textContent ?? ''), null, { timeout: LONG });
+const counts = (page) => page.$$eval('.qs-bar', (bs) => bs.map((b) => [b.querySelector('.qs-key').textContent, b.querySelector('.qs-n').textContent]));
+
+const { ctx, page, seen } = await open();
+await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
+check((await page.textContent('#qs-lint')) === '2 qubits · 2 gates', `the first example is linted as the engine reads it (${await page.textContent('#qs-lint')})`);
+await page.waitForFunction(() => document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG });
+const drawing = await page.textContent('#qs-circuit');
+check(/q0: .*H.*●/.test(drawing) && /q1: .*⊕/.test(drawing), 'the circuit is drawn as the program is typed');
+await page.waitForFunction(() => /Stabilizer tableau/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
+check(true, `the plan is shown before running (${await page.textContent('#qs-plan')})`);
+
+await page.click('#qs-run');
+await done(page);
+await verified(page);
+check((await page.textContent('.qs-method')) === 'Stabilizer tableau', 'run in this browser: the stabilizer tableau');
+const here = await counts(page);
+check(here.length === 2 && here.every(([k]) => k === '00' || k === '11'), `only 00 and 11 (${JSON.stringify(here)})`);
+const zz = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
+check(zz.some((t) => t.startsWith('⟨Z0 Z1⟩') && t.includes('1.000000')), `⟨Z0 Z1⟩ = 1 (${zz.slice(0, 2).join(' | ')})`);
+check((await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the receipt verifies, checked here (${await page.textContent('#qs-verified')})`);
+check((await page.textContent('#qs-signed-by')).startsWith('this browser’s key did:key:z6Mk'), `signed by this browser's key (${await page.textContent('#qs-signed-by')})`);
+check((await page.$$('.qs-hitem')).length === 1, 'the run is in the history');
+// What it looks like, for a reader of the change: the dark scheme and the light.
+mkdirSync(SHOTS, { recursive: true });
+await page.screenshot({ path: join(SHOTS, 'studio-dark.png'), fullPage: true });
+await page.emulateMedia({ colorScheme: 'light' });
+await page.screenshot({ path: join(SHOTS, 'studio-light.png'), fullPage: true });
+await page.emulateMedia({ colorScheme: 'dark' });
+
+// An error is marked on its line.
+await page.click('.qs-editor .cm-content');
+await page.keyboard.press('ControlOrMeta+End');
+await page.keyboard.type('\nfoo q[0];');
+await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === 'problem', null, { timeout: LONG });
+await page.waitForSelector('.cm-lintRange-error', { timeout: LONG });
+check(true, 'an unknown gate is marked in the editor');
+for (let i = 0; i < 10; i++) await page.keyboard.press('Backspace');
+await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
+
+// A rotation: any angle, on the state vector.
+await page.selectOption('#qs-example', 'rotation');
+await page.waitForFunction(() => /State vector/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
+await page.click('#qs-run');
+await done(page);
+const z = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
+check((await page.textContent('.qs-method')) === 'State vector, double precision' && z.some((t) => t.startsWith('⟨Z0⟩') && t.includes(Math.cos(0.3).toFixed(6))), `rx(0.3) on the state vector, ⟨Z0⟩ = cos 0.3 (${z[0]})`);
+
+// The same Bell job on the service, and here: the same counts.
+// (STUDIO_SKIP_SERVICE=1 leaves it out, where a build may not reach the service.)
+if (process.env.STUDIO_SKIP_SERVICE) {
+ console.log('SKIP the service checks (STUDIO_SKIP_SERVICE)');
+} else {
+ await page.selectOption('#qs-example', 'bell');
+ await page.click('#qs-run'); await done(page); await verified(page);
+ const local = await counts(page);
+ await page.click('.qs-seg[data-where=service]');
+ await page.click('#qs-run'); await done(page); await verified(page);
+ const remote = await counts(page);
+ check(JSON.stringify(remote) === JSON.stringify(local), `the service gives the counts run here, shot for shot (${JSON.stringify(remote)})`);
+ check((await page.textContent('#qs-signed-by')).startsWith('the service’s key') && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the service's receipt checks here, as the service's (${await page.textContent('#qs-signed-by')})`);
+ await page.click('.qs-seg[data-where=local]');
+
+}
+
+// Ctrl+Enter runs; the program and the history survive a reload. (A job run again
+// unchanged gives the same signed result, which the history keeps once; so this one
+// asks for other shots.)
+const before = (await page.$$('.qs-hitem')).length;
+await page.fill('#qs-shots', '321');
+await page.click('.qs-editor .cm-content');
+await page.keyboard.press('ControlOrMeta+Enter');
+await done(page);
+check((await page.$$('.qs-hitem')).length === before + 1, 'Ctrl+Enter in the editor runs the program');
+const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
+await page.reload({ waitUntil: 'domcontentloaded' });
+await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
+check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === program, 'the program survives a reload');
+await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length >= n, before + 1, { timeout: LONG });
+check(true, 'the history survives a reload');
+check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
+check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`);
+await ctx.close();
+
+{
+ const { ctx, page } = await open({ width: 390, height: 844 });
+ await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
+ const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
+ check(over <= 0, `at 390 px nothing scrolls sideways (overflow ${over} px)`);
+ await ctx.close();
+}
+
+await browser.close();
+server.close();
+console.log(`\n${fails === 0 ? 'every check passed' : `${fails} check(s) failed`}`);
+process.exit(fails === 0 ? 0 : 1);
diff --git a/wai/tools/quantum-meter/Cargo.lock b/wai/tools/quantum-meter/Cargo.lock
index 985e84317..4300fb6a2 100644
--- a/wai/tools/quantum-meter/Cargo.lock
+++ b/wai/tools/quantum-meter/Cargo.lock
@@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/tools/quantum-wgsl-check/Cargo.lock b/wai/tools/quantum-wgsl-check/Cargo.lock
index f4b0bcb14..6140a68a4 100644
--- a/wai/tools/quantum-wgsl-check/Cargo.lock
+++ b/wai/tools/quantum-wgsl-check/Cargo.lock
@@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock
index 0572fe23e..c491d5a53 100644
--- a/wai/wai-pack/Cargo.lock
+++ b/wai/wai-pack/Cargo.lock
@@ -713,7 +713,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum-cli/Cargo.lock b/wai/wai-quantum-cli/Cargo.lock
new file mode 100644
index 000000000..deeb58e91
--- /dev/null
+++ b/wai/wai-quantum-cli/Cargo.lock
@@ -0,0 +1,1348 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "blake3"
+version = "1.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
+dependencies = [
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.1",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cookie"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "libc"
+version = "0.2.191"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "litrs"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "objc2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "foldhash",
+ "indexmap",
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
+dependencies = [
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
+dependencies = [
+ "base64",
+ "cookie_store",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wai-quantum"
+version = "0.6.11"
+dependencies = [
+ "blake3",
+ "ed25519-dalek",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "wai-quantum-cli"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "clap_complete",
+ "getrandom 0.4.3",
+ "serde_json",
+ "ureq",
+ "wai-quantum",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/wai/wai-quantum-cli/Cargo.toml b/wai/wai-quantum-cli/Cargo.toml
new file mode 100644
index 000000000..074a2b131
--- /dev/null
+++ b/wai/wai-quantum-cli/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "wai-quantum-cli"
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.97"
+license = "Apache-2.0"
+description = "The wai-quantum command line: OpenQASM programs run on this machine as the quantum job service runs them (the strongest method each admits, its accuracy named, the result signed with the user's key), sent to the service, linted, drawn and verified; an MCP server for agents over stdio; and the research tools (error correction, DMRG, neural quantum states, resource estimates)."
+repository = "https://git.transaction.science/Transaction-Science/open-standards"
+homepage = "https://wai.transaction.science/quantum-run"
+keywords = ["quantum", "openqasm", "simulation", "cli", "mcp"]
+categories = ["command-line-utilities", "science", "simulation"]
+readme = "README.md"
+
+[lib]
+name = "wai_quantum_cli"
+path = "src/lib.rs"
+
+[[bin]]
+name = "wai-quantum"
+path = "src/main.rs"
+
+[features]
+default = ["service"]
+# `submit`: send a job to the quantum job service over HTTPS.
+service = ["dep:ureq"]
+# The single-precision state vector on the GPU (`--method statevector_single`), on macOS.
+metal = ["wai-quantum/quantum_sv_metal"]
+
+[dependencies]
+clap = { version = "4.6", features = ["derive", "env", "wrap_help"] }
+clap_complete = "4.6"
+getrandom = "0.4"
+# float_roundtrip: a number read back reads as the double it was, so a
+# result's receipt still verifies after any JSON writer has carried it.
+serde_json = { version = "1", features = ["float_roundtrip"] }
+ureq = { version = "3", optional = true, features = ["json"] }
+wai-quantum = { version = "0.6.11", path = "../wai-quantum", default-features = false, features = ["full"] }
diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md
new file mode 100644
index 000000000..9a220b86f
--- /dev/null
+++ b/wai/wai-quantum-cli/README.md
@@ -0,0 +1,129 @@
+# wai-quantum
+
+The command line of the WAI quantum stack. It runs OpenQASM programs on this machine, planned and run by the same engine as the quantum job service ([`wai_quantum::engine`](../wai-quantum/src/engine/mod.rs)), so a job gives the same result here as there. It also sends programs to the service, checks and draws them, verifies receipts, and serves its tools to agents over MCP.
+
+```sh
+cargo install wai-quantum-cli # installs the `wai-quantum` binary
+```
+
+## Running a program
+
+```sh
+wai-quantum run bell.qasm --shots 1000
+wai-quantum run prog.qasm -o "Z0 Z1" -o "0.5 X2" -a 000 -a 111
+wai-quantum run prog.qasm --noise depolarizing_2q=0.01 --noise readout=0.02
+wai-quantum run prog.qasm --method mps --option max_bond=128
+cat prog.qasm | wai-quantum run --json > job.json
+```
+
+A program is OpenQASM 3, or the OpenQASM 2 forms in common use. The reader accepts:
+
+- registers, and a gate applied to a whole register;
+- the standard gates;
+- `ctrl` and `negctrl` modifiers;
+- gate definitions;
+- measurement and `reset` anywhere, and `if` on measured bits.
+
+It refuses loops, `inv` and `pow`, giving the line of each.
+
+`--method auto` (the default) picks the strongest method the program admits:
+
+| Method | Takes |
+|---|---|
+| exact | dyadic angles; the state vector, the same bits on every machine |
+| statevector | any angle |
+| stabilizer | Clifford circuits, to tens of thousands of qubits |
+| mps | matrix product states |
+| pauli | Pauli dynamics, for expectation values |
+| tensor_network | amplitudes |
+| cutrank | a Clifford circuit with a layer of rotations |
+| density_matrix, trajectories | noise |
+
+`wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16).
+
+Each result names:
+
+- its method, and why that method was chosen;
+- its accuracy class;
+- the counts, expectation values or amplitudes asked for;
+- its receipt.
+
+## Receipts
+
+Every result is signed over the request and the result in their canonical JSON. The service signs with its key; this tool signs with this user's key:
+
+- `--key FILE`, or `$WAI_QUANTUM_KEY`, names a file holding the key;
+- otherwise the key is `$XDG_CONFIG_HOME/wai/quantum-key`, or `~/.config/wai/quantum-key`. It is made from the system's random source on first use and is readable by its owner only.
+
+`wai-quantum key` prints the key's `did:key`. `--demo-key` signs with the published demo key. Its receipts show only that a result is unaltered, not who signed it.
+
+```sh
+wai-quantum run prog.qasm --json > job.json
+wai-quantum verify job.json # VALID, and the signer's did:key
+wai-quantum submit prog.qasm --json > svc.json
+wai-quantum verify svc.json # the same check on the service's receipt
+```
+
+A receipt shows that this key signed this request and this result, and anyone can rerun the request to compare. It does not say who holds the key.
+
+## Checking and drawing
+
+```sh
+wai-quantum lint prog.qasm # ok, with its shape, or the line of the first problem
+wai-quantum plan prog.qasm # the method and why, the memory and work, without running
+wai-quantum draw prog.qasm --width 80
+```
+
+```text
+q0: ─────H─────●──M0──────
+ │
+q1: ───────────⊕──●───M1──
+ │
+q2: ──Rz(π/4)─────⊕───M2──
+```
+
+## The service
+
+`wai-quantum submit` sends a job to `https://wai.transaction.science/quantum-jobs`, or to `--service URL` (also read from `$WAI_QUANTUM_SERVICE`). It waits for a long job to finish, then prints the result just as `run` does.
+
+## For agents
+
+`wai-quantum mcp` is a Model Context Protocol server on standard input and output. Its tools are:
+
+- `run_quantum_circuit`
+- `plan_quantum_circuit`
+- `lint_qasm`
+- `draw_circuit`
+- `verify_quantum_result`
+- `list_methods`
+
+Runs happen on this machine and are signed with this user's key; nothing is sent anywhere. In an MCP client's configuration:
+
+```json
+{ "mcpServers": { "wai-quantum": { "command": "wai-quantum", "args": ["mcp"] } } }
+```
+
+## The research tools
+
+`wai-quantum lab …` holds the research tools:
+
+- algorithms from the catalogue, OpenQASM and QIR export, and receipts over them;
+- error correction at circuit level;
+- qLDPC memory;
+- fault-tolerant resource estimates and architectures;
+- DMRG, TDVP and molecules;
+- neural quantum states;
+- Gaussian boson sampling;
+- optimisation;
+- the compute fabric;
+- information and transport.
+
+`wai-quantum lab help` lists them. `wai-quantum verify` also checks the receipts they sign.
+
+## Builds
+
+- **Default:** `submit`, over HTTPS.
+- **`--features metal`:** the single-precision state vector on the GPU (`--method statevector_single`), on macOS.
+- **`--no-default-features --target wasm32-wasip2`:** a WASI component without `submit`. It gives the same output under any WASI runtime.
+
+`wai-quantum completions zsh` (or bash, fish, elvish, PowerShell) prints shell completion.
diff --git a/wai/wai-quantum-cli/smoke.sh b/wai/wai-quantum-cli/smoke.sh
new file mode 100644
index 000000000..bd4430188
--- /dev/null
+++ b/wai/wai-quantum-cli/smoke.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+# smoke.sh: the command line as a reader uses it, every check exact.
+#
+# bash smoke.sh path/to/wai-quantum [SERVICE_URL]
+#
+# With SERVICE_URL, `submit` sends one job to the service there and its
+# receipt is checked here. Each command's output is written to a file before
+# it is read, so no check depends on a pipe closing early.
+set -euo pipefail
+W="$1"
+SVC="${2:-}"
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' EXIT
+export XDG_CONFIG_HOME="$tmp/config"
+unset WAI_QUANTUM_KEY
+fail() { echo "smoke: $*" >&2; exit 1; }
+has() { grep -q -- "$2" "$1" || { echo "--- $1:" >&2; cat "$1" >&2; fail "expected: $2"; }; }
+
+cat > "$tmp/ghz.qasm" <<'EOF'
+OPENQASM 3.0;
+include "stdgates.inc";
+qubit[3] q;
+bit[3] c;
+h q[0];
+cx q[0], q[1];
+cx q[1], q[2];
+c = measure q;
+EOF
+
+"$W" lint "$tmp/ghz.qasm" > "$tmp/lint"
+has "$tmp/lint" "ok — 3 qubits, 3 gates, 3 bits measured"
+printf 'OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n' > "$tmp/bad.qasm"
+if "$W" lint "$tmp/bad.qasm" > "$tmp/lint-bad"; then fail "lint passed a program with an unknown gate"; fi
+has "$tmp/lint-bad" "bad.qasm:3: unknown gate foo"
+echo "lint: ok"
+
+"$W" plan "$tmp/ghz.qasm" --json > "$tmp/plan.json"
+python3 -c 'import json,sys; p=json.load(open(sys.argv[1])); assert p["method"]=="stabilizer" and p["qubits"]==3, p' "$tmp/plan.json"
+echo "plan: ok, stabilizer"
+
+"$W" run "$tmp/ghz.qasm" --shots 1000 -o "Z0 Z2" --json > "$tmp/job.json" 2> "$tmp/run.err"
+has "$tmp/run.err" "made a signing key"
+python3 - "$tmp/job.json" <<'EOF'
+import json, sys
+j = json.load(open(sys.argv[1]))
+r = j["result"]
+assert j["status"] == "done" and j["method"] == "stabilizer", j
+assert set(r["counts"]) <= {"000", "111"} and sum(r["counts"].values()) == 1000, r["counts"]
+assert r["expectations"][0]["value"] == 1.0, r["expectations"]
+assert r["receipt"]["alg"] == "ed25519" and len(r["receipt"]["key"]) == 64, r["receipt"]
+assert j["request"] == {"qasm": j["request"]["qasm"], "shots": 1000, "observables": ["Z0 Z2"]}, j["request"]
+EOF
+"$W" verify "$tmp/job.json" > "$tmp/verify"
+has "$tmp/verify" "^signature VALID"
+python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); j["result"]["counts"]={"000":1000}; json.dump(j,open(sys.argv[2],"w"))' "$tmp/job.json" "$tmp/forged.json"
+if "$W" verify "$tmp/forged.json" > "$tmp/verify-forged"; then fail "a changed result verified"; fi
+has "$tmp/verify-forged" "^signature INVALID"
+# The same user, the same key; the signer is the key's did:key.
+"$W" key > "$tmp/key" 2> /dev/null
+has "$tmp/key" "^did:key:z6Mk"
+has "$tmp/verify" "^signer $(cat "$tmp/key")"
+echo "run, verify, key: ok, signed by $(cat "$tmp/key")"
+
+"$W" run "$tmp/ghz.qasm" > "$tmp/run-text"
+has "$tmp/run-text" "^method stabilizer"
+has "$tmp/run-text" "^receipt verifies"
+if "$W" run "$tmp/ghz.qasm" --shot 5 > /dev/null 2> "$tmp/typo"; then fail "an unknown flag was accepted"; fi
+has "$tmp/typo" "unexpected argument '--shot'"
+if "$W" run "$tmp/ghz.qasm" --noise depolarizing=0.1 > /dev/null 2> "$tmp/noise"; then fail "an unknown noise channel was accepted"; fi
+has "$tmp/noise" "no such channel"
+echo "refusals: ok"
+
+"$W" draw "$tmp/ghz.qasm" > "$tmp/draw"
+has "$tmp/draw" "^q0: .*H.*●"
+has "$tmp/draw" "^q2: .*⊕.*M2"
+echo "draw: ok"
+
+printf '%s\n' \
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
+ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_quantum_circuit","arguments":{"qasm":"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];","observables":["Z0 Z1"]}}}' \
+ > "$tmp/mcp.in"
+"$W" mcp < "$tmp/mcp.in" > "$tmp/mcp.out" 2> /dev/null
+python3 - "$tmp/mcp.out" <<'EOF'
+import json, sys
+lines = [json.loads(l) for l in open(sys.argv[1])]
+assert [m["id"] for m in lines] == [1, 2, 3], lines
+assert lines[0]["result"]["serverInfo"]["name"] == "wai-quantum"
+assert "run_quantum_circuit" in [t["name"] for t in lines[1]["result"]["tools"]]
+job = lines[2]["result"]["structuredContent"]
+assert job["result"]["expectations"][0]["value"] == 1.0, job
+EOF
+echo "mcp: ok"
+
+"$W" lab list > "$tmp/lab-list"
+has "$tmp/lab-list" "^ghz "
+"$W" lab receipt ghz -n 3 > "$tmp/lab-receipt" 2> /dev/null
+"$W" verify "$tmp/lab-receipt" > "$tmp/lab-verify"
+has "$tmp/lab-verify" "^signature VALID"
+has "$tmp/lab-verify" "^signer $(cat "$tmp/key")"
+echo "lab: ok"
+
+if [ -n "$SVC" ]; then
+ "$W" submit "$tmp/ghz.qasm" --shots 500 --service "$SVC" --json > "$tmp/svc.json"
+ "$W" verify "$tmp/svc.json" > "$tmp/svc-verify"
+ has "$tmp/svc-verify" "^signature VALID"
+ echo "submit: ok, the service's receipt verifies here"
+fi
+
+echo "smoke: every check held"
diff --git a/wai/wai-quantum-cli/src/keys.rs b/wai/wai-quantum-cli/src/keys.rs
new file mode 100644
index 000000000..e71ffbfad
--- /dev/null
+++ b/wai/wai-quantum-cli/src/keys.rs
@@ -0,0 +1,135 @@
+//! The key a receipt is signed with.
+//!
+//! - `--demo-key`: the published demo key, for receipts that repeat byte for
+//! byte everywhere. Anyone can sign with it, so it proves nothing about who
+//! signed.
+//! - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key.
+//! - Otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or
+//! `~/.config/wai/quantum-key`), made from the operating system's random
+//! source on first use and readable by its owner only.
+//!
+//! A key file holds the 32-byte Ed25519 seed as 64 hex digits.
+
+use std::path::{Path, PathBuf};
+use wai_quantum::ed25519_dalek::SigningKey;
+
+/// The published demo key's seed.
+pub const DEMO_SEED: [u8; 32] = [42; 32];
+
+/// The key to sign with, and where it came from.
+pub struct Key {
+ pub key: SigningKey,
+ /// The file it was read from or made in; `None` for the demo key.
+ pub path: Option,
+ /// Whether this call made the file.
+ pub made: bool,
+}
+
+/// This user's key file: `$XDG_CONFIG_HOME/wai/quantum-key`, or
+/// `~/.config/wai/quantum-key`.
+pub fn user_path() -> Option {
+ let base = std::env::var_os("XDG_CONFIG_HOME")
+ .map(PathBuf::from)
+ .or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))?;
+ Some(base.join("wai").join("quantum-key"))
+}
+
+fn parse(text: &str) -> Option<[u8; 32]> {
+ let t = text.trim();
+ if t.len() != 64 {
+ return None;
+ }
+ let mut out = [0u8; 32];
+ for (i, b) in out.iter_mut().enumerate() {
+ *b = u8::from_str_radix(t.get(2 * i..2 * i + 2)?, 16).ok()?;
+ }
+ Some(out)
+}
+
+/// The key `demo` and `given` (`--demo-key`, `--key FILE`) name: a named file
+/// must exist; this user's own is made when there is none.
+pub fn load(given: Option<&Path>, demo: bool) -> Result {
+ if demo {
+ return Ok(Key { key: SigningKey::from_bytes(&DEMO_SEED), path: None, made: false });
+ }
+ let named = given.map(Path::to_path_buf).or_else(|| std::env::var_os("WAI_QUANTUM_KEY").map(PathBuf::from));
+ let path = match &named {
+ Some(p) => p.clone(),
+ None => user_path().ok_or("no HOME to keep a key in: pass --key FILE")?,
+ };
+ match std::fs::read_to_string(&path) {
+ Ok(text) => {
+ let seed = parse(&text).ok_or_else(|| format!("{}: a key file holds 64 hex digits", path.display()))?;
+ Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: false })
+ }
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound && named.is_none() => {
+ let mut seed = [0u8; 32];
+ getrandom::fill(&mut seed).map_err(|e| format!("no random source for a new key: {e}"))?;
+ if let Some(dir) = path.parent() {
+ std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
+ }
+ let text: String = seed.iter().map(|b| format!("{b:02x}")).collect();
+ write_private(&path, &format!("{text}\n")).map_err(|e| format!("{}: {e}", path.display()))?;
+ Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: true })
+ }
+ Err(e) => Err(format!("{}: {e}", path.display())),
+ }
+}
+
+fn write_private(path: &Path, text: &str) -> std::io::Result<()> {
+ use std::io::Write as _;
+ let mut f = std::fs::OpenOptions::new();
+ f.write(true).create_new(true);
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::OpenOptionsExt;
+ f.mode(0o600);
+ }
+ f.open(path)?.write_all(text.as_bytes())
+}
+
+/// What `--demo-key` and a new key say on standard error.
+pub fn notes(k: &Key) {
+ match &k.path {
+ None => eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it"),
+ Some(p) if k.made => eprintln!("note: made a signing key at {}", p.display()),
+ Some(_) => {}
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn a_key_is_made_once_private_then_read_and_a_named_file_must_exist() {
+ let dir = std::env::temp_dir().join(format!("wai-quantum-cli-key-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ let path = dir.join("k");
+ // A named file that is not there is refused, never made.
+ assert!(load(Some(&path), false).is_err());
+ assert!(!path.exists());
+ // The demo key is the published one.
+ let demo = load(None, true).unwrap();
+ assert_eq!(demo.key.to_bytes(), DEMO_SEED);
+ assert!(demo.path.is_none());
+ // A written key reads back; a malformed one is refused.
+ std::fs::create_dir_all(&dir).unwrap();
+ let seed: String = (0..32).map(|i| format!("{:02x}", i * 7)).collect();
+ std::fs::write(&path, format!("{seed}\n")).unwrap();
+ let k = load(Some(&path), false).unwrap();
+ assert_eq!(k.key.to_bytes()[1], 7);
+ assert!(!k.made);
+ std::fs::write(&path, "not a key").unwrap();
+ assert!(load(Some(&path), false).is_err());
+ // A made key is the owner's alone.
+ let fresh = dir.join("fresh");
+ write_private(&fresh, "x").unwrap();
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ assert_eq!(std::fs::metadata(&fresh).unwrap().permissions().mode() & 0o777, 0o600);
+ }
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+}
diff --git a/wai/wai-rs/src/bin/wai_quantum.rs b/wai/wai-quantum-cli/src/lab.rs
similarity index 87%
rename from wai/wai-rs/src/bin/wai_quantum.rs
rename to wai/wai-quantum-cli/src/lab.rs
index c00f05f71..184ed5a68 100644
--- a/wai/wai-rs/src/bin/wai_quantum.rs
+++ b/wai/wai-quantum-cli/src/lab.rs
@@ -1,4 +1,5 @@
-//! `wai-quantum` — the deterministic quantum stack as a command-line tool.
+//! `wai-quantum lab`: the research tools of the command line, the deterministic
+//! quantum stack's every capability.
//!
//! Every quantum capability in this crate is pure Rust with no host bindings, so
//! this binary targets **native and cloud WASM from one source**:
@@ -11,12 +12,12 @@
//! and corpus digest.
//!
//! ```text
-//! wai-quantum list
-//! wai-quantum run ghz --qubits 5 [--top 8] [--json]
-//! wai-quantum run --qasm # read OpenQASM on stdin
-//! wai-quantum qasm bell # emit OpenQASM
-//! wai-quantum receipt ghz -n 3 [--shots 4096 --seed 7]
-//! wai-quantum verify # read a receipt JSON on stdin
+//! wai-quantum lab list
+//! wai-quantum lab run ghz --qubits 5 [--top 8] [--json]
+//! wai-quantum lab run --qasm # read OpenQASM on stdin
+//! wai-quantum lab qasm bell # emit OpenQASM
+//! wai-quantum lab receipt ghz -n 3 [--shots 4096 --seed 7]
+//! wai-quantum lab verify # read a receipt JSON on stdin
//! ```
//!
//! A circuit is built in `wai.quantum.circuit2`, whose phase table is correctly
@@ -29,20 +30,20 @@
//! what transport does to it:
//!
//! ```text
-//! wai-quantum entropy bell --keep 0 # half a Bell pair: 1 bit, and no state vector
-//! wai-quantum channel depolarizing --prob 0.5
-//! wai-quantum bb84 --eavesdrop # interception shows up in the error rate
-//! wai-quantum chsh # 2√2, exactly, not sampled
-//! wai-quantum qutrit # d = 3, not just qubits
+//! wai-quantum lab entropy bell --keep 0 # half a Bell pair: 1 bit, and no state vector
+//! wai-quantum lab channel depolarizing --prob 0.5
+//! wai-quantum lab bb84 --eavesdrop # interception shows up in the error rate
+//! wai-quantum lab chsh # 2√2, exactly, not sampled
+//! wai-quantum lab qutrit # d = 3, not just qubits
//! ```
//!
//! At utility scale, sparse Pauli dynamics computes observables of circuits far
//! past any state vector, with arbitrary rotation angles:
//!
//! ```text
-//! wai-quantum utility --theta 0.2 --steps 20 # the 127-qubit heavy-hex experiment
-//! wai-quantum utility --method bptn --chi 32 # the same, by tensor networks
-//! wai-quantum utility --receipt | wai-quantum verify
+//! wai-quantum lab utility --theta 0.2 --steps 20 # the 127-qubit heavy-hex experiment
+//! wai-quantum lab utility --method bptn --chi 32 # the same, by tensor networks
+//! wai-quantum lab utility --receipt | wai-quantum verify
//! ```
//!
//! And for fault-tolerant machines that do not exist yet, what a program would
@@ -50,10 +51,10 @@
//! signed estimate:
//!
//! ```text
-//! wai-quantum estimate factoring # 2048-bit factoring, all six qubits
-//! wai-quantum estimate chemistry --qubit ns-e4 --frontier
-//! wai-quantum estimate --qubits 100 --ccz 1e6 --budget 0.01
-//! wai-quantum estimate factoring --qubit ns-e4 --receipt | wai-quantum verify
+//! wai-quantum lab estimate factoring # 2048-bit factoring, all six qubits
+//! wai-quantum lab estimate chemistry --qubit ns-e4 --frontier
+//! wai-quantum lab estimate --qubits 100 --ccz 1e6 --budget 0.01
+//! wai-quantum lab estimate factoring --qubit ns-e4 --receipt | wai-quantum verify
//! ```
//!
//! Error correction itself, at circuit level: sample a noisy Clifford circuit,
@@ -61,9 +62,9 @@
//! model that estimates otherwise take on trust:
//!
//! ```text
-//! wai-quantum qec -d 5 --p 0.003 --shots 100000
-//! wai-quantum qec --file circuit.txt
-//! wai-quantum qec --fit # then: estimate --prefactor A --threshold P
+//! wai-quantum lab qec -d 5 --p 0.003 --shots 100000
+//! wai-quantum lab qec --file circuit.txt
+//! wai-quantum lab qec --fit # then: estimate --prefactor A --threshold P
//! ```
//!
//! The `channel` command prints the Holevo bound before and after, which can only
@@ -74,18 +75,19 @@
use wai_quantum::ed25519_dalek::SigningKey;
use std::io::Read;
-use wai::quantum::{BaseGate, Circuit, Gateset, Measure, ONE};
-use wai::quantum_channel::{tomography, Channel};
-use wai::quantum_comm::{bb84, clone_fidelity};
-use wai::quantum_info::{chsh, DensityMatrix};
-use wai::quantum_qudit::QuditState;
-use wai::quantum_energy::{EnergyClass, MaybeLabelled};
-use wai::quantum_receipt::QuantumReceipt;
-use wai::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
-use wai::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
-use wai::quantum_spd_receipt::{SpdJob, SpdReceipt};
-use wai::quantum_source::{holevo_bound, schumacher_limit, von_neumann_entropy};
-use wai::quantum_toolchain as qt;
+use wai_quantum::quantum::{BaseGate, Circuit, Gateset, Measure, ONE};
+use wai_quantum::quantum_channel::{tomography, Channel};
+use wai_quantum::quantum_comm::{bb84, clone_fidelity};
+use wai_quantum::quantum_info::{chsh, DensityMatrix};
+use wai_quantum::quantum_qudit::QuditState;
+use wai_quantum::quantum_energy::{EnergyClass, MaybeLabelled};
+use wai_quantum::quantum_receipt::QuantumReceipt;
+use wai_quantum::quantum_receipt::did_key;
+use wai_quantum::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
+use wai_quantum::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
+use wai_quantum::quantum_spd_receipt::{SpdJob, SpdReceipt};
+use wai_quantum::quantum_source::{holevo_bound, schumacher_limit, von_neumann_entropy};
+use wai_quantum::quantum_toolchain as qt;
/// The three single-qubit states the channel command sends: the ground state, the
/// excited one, and the superposition whose coherence noise actually destroys.
@@ -141,75 +143,75 @@ fn die(msg: &str) -> ! {
fn usage() -> ! {
eprintln!(
- "wai-quantum — deterministic quantum computation (native + cloud WASM)\n\n\
+ "wai-quantum lab — the research tools (run `wai-quantum --help` for run, plan, lint, draw, verify and the rest)\n\n\
USAGE:\n \
- wai-quantum list\n \
- wai-quantum run [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \
- wai-quantum run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \
- wai-quantum run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \
- wai-quantum qasm [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
- wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
- wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \
- wai-quantum verify (receipt JSON on stdin)\n \
+ wai-quantum lab list\n \
+ wai-quantum lab run [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \
+ wai-quantum lab run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \
+ wai-quantum lab run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \
+ wai-quantum lab qasm [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
+ wai-quantum lab qir [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
+ wai-quantum lab receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2] [--key FILE | --demo-key]\n \
+ wai-quantum lab verify (receipt JSON on stdin)\n \
--gateset v2 (the default) is wai.quantum.circuit2; v1 is the frozen wai.quantum.circuit.\n\n\
INFORMATION AND TRANSPORT:\n \
- wai-quantum entropy [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \
- wai-quantum channel [--prob P] [--state zero|one|plus] [--json]\n \
- wai-quantum bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \
- wai-quantum chsh [--json]\n \
- wai-quantum qutrit [-n QUTRITS] [--json]\n\n\
+ wai-quantum lab entropy [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \
+ wai-quantum lab channel [--prob P] [--state zero|one|plus] [--json]\n \
+ wai-quantum lab bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \
+ wai-quantum lab chsh [--json]\n \
+ wai-quantum lab qutrit [-n QUTRITS] [--json]\n\n\
UTILITY SCALE:\n \
- wai-quantum utility [--theta T] [--steps N] [--threshold D] [-q QUBIT] [--json] [--receipt]\n \
- wai-quantum utility --method bptn [--chi X] [--theta T] [--steps N] [-q QUBIT] [--json]\n\n\
+ wai-quantum lab utility [--theta T] [--steps N] [--threshold D] [-q QUBIT] [--json] [--receipt]\n \
+ wai-quantum lab utility --method bptn [--chi X] [--theta T] [--steps N] [-q QUBIT] [--json]\n\n\
FAULT-TOLERANT RESOURCES:\n \
- wai-quantum estimate [factoring|chemistry] [--qubit NAME|all] [--code surface|surface-meas|floquet]\n \
+ wai-quantum lab estimate [factoring|chemistry] [--qubit NAME|all] [--code surface|surface-meas|floquet]\n \
[--budget E] [--slowdown N] [--frontier] [--json] [--receipt]\n \
- wai-quantum estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
+ wai-quantum lab estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
Qubits: us-e3, us-e4, ns-e3, ns-e4, meas-e4, meas-e6. A measured code: --prefactor A --threshold P.\n\n\
ERROR CORRECTION AT CIRCUIT LEVEL:\n \
- wai-quantum qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
- wai-quantum qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
- wai-quantum qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
- wai-quantum qec --fit [--shots N] (measure a and p* of the code model)\n\n\
+ wai-quantum lab qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
+ wai-quantum lab qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
+ wai-quantum lab qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
+ wai-quantum lab qec --fit [--shots N] (measure a and p* of the code model)\n\n\
GAUSSIAN BOSON SAMPLING:\n \
- wai-quantum gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
- wai-quantum gbs … --sample K (exact click patterns)\n \
- wai-quantum gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
+ wai-quantum lab gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
+ wai-quantum lab gbs … --sample K (exact click patterns)\n \
+ wai-quantum lab gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
SPIN CHAINS (DMRG ground states, exact referees):\n \
- wai-quantum dmrg [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--bond D] [--sweeps S]\n \
+ wai-quantum lab dmrg [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--bond D] [--sweeps S]\n \
[--cutoff C] [--exact] [--json]\n \
- wai-quantum tdvp [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--start plus|neel|up]\n \
+ wai-quantum lab tdvp [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--start plus|neel|up]\n \
[--bond D] [--dt DT] [--t T] [--every K] [--cutoff C] [--json] (time evolution by TDVP)\n\n\
MOLECULES (DMRG from FCIDUMP integrals, full CI as referee):\n \
- wai-quantum chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
+ wai-quantum lab chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
NEURAL QUANTUM STATES (RBM, variational Monte Carlo, stochastic reconfiguration):\n \
- wai-quantum nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
+ wai-quantum lab nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
[--complex] [--project [--kpi] [--point-group a1|a2|b1|b2 | --reflections a1|a2|b1|b2]\n \
[--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \
[--init W] [--guide ε] [--exact] [--seed S] [--json]\n\n\
THE FABRIC (members, parity against the CPU reference, placement):\n \
- wai-quantum fabric [--circuits N] [--seed S] [--gateset v1|v2] [--json]\n\n\
+ wai-quantum lab fabric [--circuits N] [--seed S] [--gateset v1|v2] [--json]\n\n\
OPTIMISATION (simulated bifurcation, annealing, exact):\n \
- wai-quantum qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \
+ wai-quantum lab qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \
[--steps N] [--trials T] [--dt D] [--c1 C] [--gamma G] [--beta0 B --beta1 B] [--descend]\n \
[--target V] [--seed S] [--threads T] [--spins] [--json]\n\n\
QLDPC MEMORY (circuit level, BP-OSD):\n \
- wai-quantum ldpc [--code 72|90|108|144|288] [--p P] [--cycles N] [--shots S] [--basis z|x|both]\n \
+ wai-quantum lab ldpc [--code 72|90|108|144|288] [--p P] [--cycles N] [--shots S] [--basis z|x|both]\n \
[--max-iter I] [--osd-order K] [--threads T] [--seed S] [--json]\n \
- wai-quantum ldpc --circuit [--code …] [--p P] [--cycles N] [--basis z|x] (the circuit text)\n\n\
+ wai-quantum lab ldpc --circuit [--code …] [--p P] [--cycles N] [--basis z|x] (the circuit text)\n\n\
ARCHITECTURE:\n \
- wai-quantum arch factoring [--bits 1024|1536|2048|3072|4096|6144|8192] [--json]\n \
- wai-quantum arch storage [--target T] [--fits refit|uniform|si1000] [--json]\n \
- wai-quantum arch cultivate [--p 5e-4|1e-3|2e-3] [--target T] [--json]\n\n\
+ wai-quantum lab arch factoring [--bits 1024|1536|2048|3072|4096|6144|8192] [--json]\n \
+ wai-quantum lab arch storage [--target T] [--fits refit|uniform|si1000] [--json]\n \
+ wai-quantum lab arch cultivate [--p 5e-4|1e-3|2e-3] [--target T] [--json]\n\n\
NOISY CIRCUITS (trajectories, exact density matrix, cross-entropy):\n \
- wai-quantum noisy --file CIRCUIT.jsonl [-n N] [--depol1 P] [--depol2 P] [--damping G] [--dephasing P]\n \
+ wai-quantum lab noisy --file CIRCUIT.jsonl [-n N] [--depol1 P] [--depol2 P] [--damping G] [--dephasing P]\n \
[--readout P] [--trajectories T] [--seed S] [--expect \"Z0 Z1; X3\"] [--xeb] [--exact] [--json]\n\n\
STATE VECTOR (any gate, any angle):\n \
- wai-quantum sv --file CIRCUIT.jsonl [-n N] [--fuse W] [--top K | --sample S [--seed X] | --expect \"Z0 Z1; X3\"] [--json]\n\n\
+ wai-quantum lab sv --file CIRCUIT.jsonl [-n N] [--fuse W] [--top K | --sample S [--seed X] | --expect \"Z0 Z1; X3\"] [--json]\n\n\
TENSOR NETWORKS:\n \
- wai-quantum tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--whole-gates] [--plan-only] [--json]\n\n\
+ wai-quantum lab tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--whole-gates] [--plan-only] [--json]\n\n\
Channels: identity, bitflip, dephasing, depolarizing, damping.\n\
- Run `wai-quantum list` for the algorithm catalog."
+ Run `wai-quantum lab list` for the algorithm catalog."
);
std::process::exit(2);
}
@@ -222,6 +224,15 @@ fn read_stdin() -> String {
s
}
+/// The key a receipt is signed with ([`crate::keys`]), from `--key FILE` or
+/// `--demo-key`.
+fn signing_key(args: &[String]) -> SigningKey {
+ let given = opt(args, "--key", "").map(std::path::PathBuf::from);
+ let k = wai_quantum_cli::keys::load(given.as_deref(), flag(args, "--demo-key")).unwrap_or_else(|e| die(&e));
+ wai_quantum_cli::keys::notes(&k);
+ k.key
+}
+
/// Pull `--flag VALUE` out of the argument list.
fn opt(args: &[String], long: &str, short: &str) -> Option {
args.iter()
@@ -259,9 +270,9 @@ fn gateset_from(args: &[String]) -> Gateset {
fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit {
let gs = gateset_from(args);
let mut c = if flag(args, "--qasm") {
- qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}")))
+ wai_quantum::qasm::dyadic_circuit(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}")))
} else if flag(args, "--qir") {
- wai::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}")))
+ wai_quantum::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}")))
} else {
let id = algo.unwrap_or_else(|| usage());
let n: u8 = num(args, "--qubits", "-n", 3);
@@ -324,8 +335,8 @@ fn human_time(ns: f64) -> String {
/// A circuit file: JSON lines, one gate per line, `{"q":[a,b],"m":[re,im, …]}`
/// (rows are outputs, the first qubit most significant). Returns the gates
/// and the register size they need.
-fn read_gate_file(path: &str) -> (Vec, u32) {
- use wai::gates::{complex, Gate};
+fn read_gate_file(path: &str) -> (Vec, u32) {
+ use wai_quantum::gates::{complex, Gate};
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let mut gates = Vec::new();
let mut n: u32 = 0;
@@ -435,8 +446,76 @@ fn estimate_json(e: &qr::Estimate) -> String {
)
}
-fn main() {
- let argv: Vec = std::env::args().collect();
+/// Check a receipt of any kind this tool or the library signs: `src` is its
+/// JSON. Prints what it binds, and exits 1 when it does not verify.
+pub fn verify_receipt(src: &str) {
+ // Either form: a receipt as sealed, or one whose joule figure
+ // carries its acquisition class inside the signature
+ // (`quantum_energy::Labelled`, as `wai_quantum_meter` emits).
+ if let Some(any) = MaybeLabelled::::from_json(src) {
+ let ok = any.verify();
+ let r = any.receipt();
+ let e = &r.estimate;
+ println!("signature {}", if ok { "VALID" } else { "INVALID" });
+ println!("kind fault-tolerant resource estimate");
+ println!("job-hash {}", hx(&r.job_hash));
+ println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
+ println!("physical {} qubits", e.physical_qubits);
+ println!("runtime {}", human_time(e.runtime_ns));
+ println!("factories {}", e.factories);
+ println!("joules {} uJ", r.joules_micro);
+ println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
+ println!("signer {}", r.signer_id);
+ if !ok {
+ std::process::exit(1);
+ }
+ return;
+ }
+ if let Some(any) = MaybeLabelled::::from_json(src) {
+ let ok = any.verify();
+ let r = any.receipt();
+ println!("signature {}", if ok { "VALID" } else { "INVALID" });
+ println!("kind sparse Pauli dynamics");
+ println!("circuit-hash {}", hx(&r.circuit_hash));
+ println!("query-hash {}", hx(&r.query_hash));
+ println!("value {}", r.result.value);
+ println!("bound {}", r.result.truncation_bound);
+ println!("work {} term-updates (peak {} terms)", r.result.term_updates, r.result.peak_terms);
+ println!("joules {} uJ", r.joules_micro);
+ println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
+ println!("signer {}", r.signer_id);
+ if !ok {
+ std::process::exit(1);
+ }
+ return;
+ }
+ match MaybeLabelled::::from_json(src) {
+ None => {
+ println!("INVALID (could not parse receipt JSON, or its energy class is malformed)");
+ std::process::exit(1);
+ }
+ Some(any) => {
+ let ok = any.verify();
+ let r = any.receipt();
+ println!("signature {}", if ok { "VALID" } else { "INVALID" });
+ println!("circuit-hash {}", hx(&r.circuit_hash));
+ println!("state-hash {}", hx(&r.statevector_hash));
+ println!("qubits {}", r.n_qubits);
+ println!("gates {}", r.n_ops);
+ println!("work {} amplitude-updates", r.work_amp_updates);
+ println!("joules {} uJ", r.joules_micro);
+ println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
+ println!("signer {}", r.signer_id);
+ if !ok {
+ std::process::exit(1);
+ }
+ }
+ }
+}
+
+/// The research tools, with `args` the words after `wai-quantum lab`.
+pub fn main(args: Vec) {
+ let argv: Vec = std::iter::once("wai-quantum lab".to_string()).chain(args).collect();
if argv.len() < 2 {
usage();
}
@@ -446,7 +525,7 @@ fn main() {
match cmd {
"list" => {
- println!("{:<14} {:<26} {}", "ID", "NAME", "DESCRIPTION");
+ println!("{:<14} {:<26} DESCRIPTION", "ID", "NAME");
for (id, name, desc) in qt::catalog() {
println!("{id:<14} {name:<26} {desc}");
}
@@ -459,7 +538,7 @@ fn main() {
"qir" => {
let c = circuit_from(args, algo);
- print!("{}", wai::quantum_qir::to_qir(&c, !flag(args, "--no-measure")));
+ print!("{}", wai_quantum::quantum_qir::to_qir(&c, !flag(args, "--no-measure")));
}
"run" => {
@@ -509,79 +588,13 @@ fn main() {
let shots: u64 = num(args, "--shots", "", 0);
let seed: u64 = num(args, "--seed", "", 0);
let measure = (shots > 0).then_some(Measure { seed, shots });
- // Deterministic demo key: the receipt is reproducible byte-for-byte, so
- // native and cloud-WASM runs sign identically. Supply real keys in prod.
- let key = SigningKey::from_bytes(&[42u8; 32]);
- let r = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None)
+ let key = signing_key(args);
+ let r = QuantumReceipt::seal(&key, did_key(&key.verifying_key()), &c, measure, 0, None)
.unwrap_or_else(|e| die(&format!("seal: {e:?}")));
println!("{}", r.to_json());
}
- "verify" => {
- // Either form: a receipt as sealed, or one whose joule figure
- // carries its acquisition class inside the signature
- // (`quantum_energy::Labelled`, as `wai_quantum_meter` emits).
- let src = read_stdin();
- if let Some(any) = MaybeLabelled::::from_json(&src) {
- let ok = any.verify();
- let r = any.receipt();
- let e = &r.estimate;
- println!("signature {}", if ok { "VALID" } else { "INVALID" });
- println!("kind fault-tolerant resource estimate");
- println!("job-hash {}", hx(&r.job_hash));
- println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
- println!("physical {} qubits", e.physical_qubits);
- println!("runtime {}", human_time(e.runtime_ns));
- println!("factories {}", e.factories);
- println!("joules {} uJ", r.joules_micro);
- println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
- println!("signer {}", r.signer_id);
- if !ok {
- std::process::exit(1);
- }
- return;
- }
- if let Some(any) = MaybeLabelled::::from_json(&src) {
- let ok = any.verify();
- let r = any.receipt();
- println!("signature {}", if ok { "VALID" } else { "INVALID" });
- println!("kind sparse Pauli dynamics");
- println!("circuit-hash {}", hx(&r.circuit_hash));
- println!("query-hash {}", hx(&r.query_hash));
- println!("value {}", r.result.value);
- println!("bound {}", r.result.truncation_bound);
- println!("work {} term-updates (peak {} terms)", r.result.term_updates, r.result.peak_terms);
- println!("joules {} uJ", r.joules_micro);
- println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
- println!("signer {}", r.signer_id);
- if !ok {
- std::process::exit(1);
- }
- return;
- }
- match MaybeLabelled::::from_json(&src) {
- None => {
- println!("INVALID (could not parse receipt JSON, or its energy class is malformed)");
- std::process::exit(1);
- }
- Some(any) => {
- let ok = any.verify();
- let r = any.receipt();
- println!("signature {}", if ok { "VALID" } else { "INVALID" });
- println!("circuit-hash {}", hx(&r.circuit_hash));
- println!("state-hash {}", hx(&r.statevector_hash));
- println!("qubits {}", r.n_qubits);
- println!("gates {}", r.n_ops);
- println!("work {} amplitude-updates", r.work_amp_updates);
- println!("joules {} uJ", r.joules_micro);
- println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
- println!("signer {}", r.signer_id);
- if !ok {
- std::process::exit(1);
- }
- }
- }
- }
+ "verify" => verify_receipt(&read_stdin()),
// How much quantum information a state actually carries. A state vector
// cannot answer this for a subsystem — half a Bell pair has no state
@@ -592,7 +605,7 @@ fn main() {
// qubit and RZZ(−π/2) on every edge, and the bound on how far
// truncation can have moved it. `--receipt` seals the run instead.
"utility" => {
- use wai::quantum_spd::{heavy_hex_127, kicked_ising, Pauli, SpdConfig};
+ use wai_quantum::quantum_spd::{heavy_hex_127, kicked_ising, Pauli, SpdConfig};
let theta: f64 = num(args, "--theta", "", std::f64::consts::FRAC_PI_4);
let steps: u32 = num(args, "--steps", "", 20);
let threshold: f64 = num(args, "--threshold", "", 1e-3);
@@ -606,7 +619,7 @@ fn main() {
}
let chi: usize = num(args, "--chi", "", 16);
let edges = heavy_hex_127();
- let mut tns = wai::quantum_bptn::Tns::new(127, &edges, chi).unwrap_or_else(|e| die(&e.to_string()));
+ let mut tns = wai_quantum::quantum_bptn::Tns::new(127, &edges, chi).unwrap_or_else(|e| die(&e.to_string()));
tns.run(&kicked_ising(127, &edges, theta, steps)).unwrap_or_else(|e| die(&e.to_string()));
let value = tns.expect1(qubit, Pauli::Z).unwrap_or_else(|e| die(&e.to_string()));
if flag(args, "--json") {
@@ -633,10 +646,8 @@ fn main() {
config: SpdConfig { threshold, max_weight: None },
};
if flag(args, "--receipt") {
- // The same deterministic demo key as `receipt`: the run is
- // bit-identical everywhere, so is its receipt.
- let key = SigningKey::from_bytes(&[42u8; 32]);
- let r = SpdReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
+ let key = signing_key(args);
+ let r = SpdReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
run();
0
})
@@ -698,10 +709,8 @@ fn main() {
die("--receipt seals one estimate: pick a --qubit");
}
let job = ResourceJob { program: Program::Logical(counts), qubit: qubits[0].clone(), code, config: cfg };
- // The same deterministic demo key as `receipt`: the estimate is
- // bit-identical everywhere, so is its receipt.
- let key = SigningKey::from_bytes(&[42u8; 32]);
- let r = ResourceReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
+ let key = signing_key(args);
+ let r = ResourceReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
run();
0
})
@@ -731,7 +740,7 @@ fn main() {
println!("[{}]", rows.join(","));
return;
}
- println!("{:>8} {:>3} {:>9} {:>14} {}", "slowdown", "d", "factories", "physical", "runtime");
+ println!("{:>8} {:>3} {:>9} {:>14} runtime", "slowdown", "d", "factories", "physical");
for e in &points {
println!(
"{:>8} {:>3} {:>9} {:>14} {}",
@@ -759,8 +768,8 @@ fn main() {
let isa = qr::layout(&counts, budget);
println!("program {} tiles, {} logical steps, {} T states (budget {budget})", isa.tiles, isa.min_steps, isa.t_states);
println!(
- "{:<12} {:<22} {:>3} {:>9} {:>8} {:>14} {}",
- "qubit", "code", "d", "factories", "share", "physical", "runtime"
+ "{:<12} {:<22} {:>3} {:>9} {:>8} {:>14} runtime",
+ "qubit", "code", "d", "factories", "share", "physical"
);
for (q, e) in &estimates {
match e {
@@ -782,8 +791,8 @@ fn main() {
// Error correction at circuit level: sample a noisy Clifford circuit,
// derive its detector error model, decode it exactly.
"qec" => {
- use wai::quantum_frame::{error_model, parse, sample, surface_code_memory};
- use wai::quantum_match::{fit_code_model, memory_experiment, MatchingGraph};
+ use wai_quantum::quantum_frame::{error_model, parse, sample, surface_code_memory};
+ use wai_quantum::quantum_match::{fit_code_model, memory_experiment, MatchingGraph};
let shots: usize = num(args, "--shots", "", 100_000);
let seed: u64 = num(args, "--seed", "", 1);
let json = flag(args, "--json");
@@ -840,7 +849,7 @@ fn main() {
let decoder = opt(args, "--decoder", "").unwrap_or_else(|| "exact".into());
let failures = match decoder.as_str() {
"exact" => g.failures(&s),
- "uf" => wai::quantum_uf::UnionFind::new(&g).failures(&s),
+ "uf" => wai_quantum::quantum_uf::UnionFind::new(&g).failures(&s),
_ => die("--decoder: exact or uf"),
}
.unwrap_or_else(|e| die(&format!("decode: {e:?}")));
@@ -858,7 +867,7 @@ fn main() {
println!("model {} error mechanisms", model.mechanisms.len());
println!("failures {failures} of {shots} shots ({rate:.3e} per shot, {decoder} decoder)");
if opt(args, "--file", "").is_none() {
- let per_round = wai::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
+ let per_round = wai_quantum::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
println!("per round {per_round:.3e} (d = {d}, {rounds} rounds, p = {p})");
}
}
@@ -870,7 +879,7 @@ fn main() {
// {"q":[0,1],"m":[re,im, …]} (rows are outputs, first qubit most
// significant).
"tn" => {
- use wai::quantum_tn::{contract_plan, find_path, Families, Network, PathOptions};
+ use wai_quantum::quantum_tn::{contract_plan, find_path, Families, Network, PathOptions};
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);
@@ -950,7 +959,7 @@ fn main() {
// Gaussian boson sampling, exactly: squeezed light through an
// interferometer with loss, and the click statistics it must show.
"gbs" => {
- use wai::quantum_gbs::{random_unitary, GaussianState, Mat, C};
+ use wai_quantum::quantum_gbs::{random_unitary, GaussianState, Mat, C};
let r: Vec = opt(args, "--squeezing", "")
.unwrap_or_else(|| die("--squeezing r1,r2,… is required (one value per mode)"))
.split(',')
@@ -1039,7 +1048,7 @@ fn main() {
// where one is known (the Ising chain at any length, any chain by
// exact diagonalisation up to 22 sites).
"dmrg" => {
- use wai::quantum_dmrg::*;
+ use wai_quantum::quantum_dmrg::*;
let n: usize = num(args, "--sites", "-n", 40);
if n < 2 {
die("-n: at least 2 sites");
@@ -1088,8 +1097,8 @@ fn main() {
// Ising chain started with every σˣ = +1 is held at each report to
// its free-fermion solution.
"tdvp" => {
- use wai::quantum_dmrg::{Chain, Mps, Op, Scalar, C};
- use wai::quantum_tdvp::*;
+ use wai_quantum::quantum_dmrg::{Chain, Mps, Op, Scalar, C};
+ use wai_quantum::quantum_tdvp::*;
let n: usize = num(args, "--sites", "-n", 40);
if n < 2 {
die("-n: at least 2 sites");
@@ -1171,7 +1180,7 @@ fn main() {
// A molecule's ground state by DMRG from its integrals, against
// Hartree–Fock and full configuration interaction where it fits.
"chem" => {
- use wai::quantum_chem::*;
+ use wai_quantum::quantum_chem::*;
let path = opt(args, "--fcidump", "").unwrap_or_else(|| die("--fcidump FILE is required"));
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let fd = read_fcidump(&text).unwrap_or_else(|e| die(&format!("{path}: {e}")));
@@ -1210,7 +1219,7 @@ fn main() {
// A neural quantum state for a spin model on a chain or an open
// square lattice, against exact diagonalisation where it fits.
"nqs" => {
- use wai::quantum_nqs::*;
+ use wai_quantum::quantum_nqs::*;
let lx: usize = num(args, "--lx", "", 10);
let ly: usize = num(args, "--ly", "", 1);
let n = lx * ly;
@@ -1350,7 +1359,7 @@ fn main() {
// host, so they differ between platforms; the parity and the corpus
// digest do not.
"fabric" => {
- use wai::quantum_fabric::*;
+ use wai_quantum::quantum_fabric::*;
let circuits: usize = num(args, "--circuits", "", 200);
let seed: u64 = num(args, "--seed", "", 7);
let gs = gateset_from(args);
@@ -1405,7 +1414,7 @@ fn main() {
// glass, by simulated bifurcation, annealing, or exact enumeration.
// Max-Cut reports cuts; a spin glass reports energies.
"qio" => {
- use wai::quantum_qio::*;
+ use wai_quantum::quantum_qio::*;
let (problem, graph) = if let Some(path) = opt(args, "--gset", "") {
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let g = read_gset(&text).unwrap_or_else(|e| die(&e.to_string()));
@@ -1479,8 +1488,8 @@ fn main() {
// qLDPC memory: a bivariate-bicycle code's depth-8 syndrome cycle,
// sampled with circuit-level noise and decoded by BP-OSD.
"ldpc" => {
- use wai::quantum_frame::Basis;
- use wai::quantum_ldpc::{memory_circuit, memory_experiment, BbCode, BpOsdConfig};
+ use wai_quantum::quantum_frame::Basis;
+ use wai_quantum::quantum_ldpc::{memory_circuit, memory_experiment, BbCode, BpOsdConfig};
let code = match opt(args, "--code", "").as_deref().unwrap_or("72") {
"72" => BbCode::bb72(),
"90" => BbCode::bb90(),
@@ -1540,7 +1549,7 @@ fn main() {
// states, CCZ factories and the clock, priced; and the published
// factoring machine re-run region by region.
"arch" => {
- use wai::quantum_arch::*;
+ use wai_quantum::quantum_arch::*;
let json = flag(args, "--json");
match algo.map(String::as_str) {
Some("factoring") | None => {
@@ -1618,7 +1627,7 @@ fn main() {
// exact density matrix (up to 12 qubits) and cross-entropy fidelity
// against the ideal circuit and the digital error model.
"noisy" => {
- use wai::quantum_noisy::*;
+ use wai_quantum::quantum_noisy::*;
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);
@@ -1661,7 +1670,7 @@ fn main() {
None
};
let xeb = if flag(args, "--xeb") {
- let mut ideal = wai::quantum_sv::StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?}")));
+ let mut ideal = wai_quantum::quantum_sv::StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?}")));
ideal.run(&gates, 2).unwrap_or_else(|e| die(&format!("{e:?}")));
let sampled = linear_xeb(&ideal, &run.samples);
let exact_x = exact.as_ref().map(|rho| exact_xeb(&ideal, rho, noise.readout));
@@ -1710,7 +1719,7 @@ fn main() {
// (fused to --fuse qubits), then show the likeliest outcomes, draw
// samples, or evaluate Pauli observables.
"sv" => {
- use wai::quantum_sv::StateVector;
+ use wai_quantum::quantum_sv::StateVector;
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);
diff --git a/wai/wai-quantum-cli/src/lib.rs b/wai/wai-quantum-cli/src/lib.rs
new file mode 100644
index 000000000..78aadd4b4
--- /dev/null
+++ b/wai/wai-quantum-cli/src/lib.rs
@@ -0,0 +1,4 @@
+//! The command line's shared pieces, for other hosts of the engine on a
+//! user's machine (the desktop app): the key a receipt is signed with.
+
+pub mod keys;
diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs
new file mode 100644
index 000000000..a440c10c0
--- /dev/null
+++ b/wai/wai-quantum-cli/src/main.rs
@@ -0,0 +1,519 @@
+//! `wai-quantum`: OpenQASM programs run on this machine as the quantum job
+//! service runs them, or sent to the service; every result names its method
+//! and its accuracy and carries a signed receipt.
+//!
+//! ```text
+//! wai-quantum run bell.qasm --shots 1000 # here, planned as the service plans it
+//! wai-quantum run prog.qasm -o "Z0 Z1" -a 00 --json
+//! wai-quantum plan prog.qasm # the method and why, without running
+//! wai-quantum lint prog.qasm # each problem with its line
+//! wai-quantum draw prog.qasm # the circuit as text
+//! wai-quantum submit prog.qasm # on the job service
+//! wai-quantum verify result.json # a result's receipt, or a lab receipt
+//! wai-quantum key # the key this user signs with
+//! wai-quantum methods # each method and its reach here
+//! wai-quantum mcp # these tools for an agent, over stdio
+//! wai-quantum lab … # the research tools
+//! wai-quantum completions zsh # shell completion
+//! ```
+//!
+//! `run`, `plan` and `mcp` use the job service's own engine
+//! ([`wai_quantum::engine`]), so a job gives here the result it gives there.
+//! `run --json` prints the job as the service returns it: the request, the
+//! plan, and the result with its receipt, which `verify` checks.
+
+mod lab;
+mod mcp;
+
+use clap::{Args, CommandFactory, Parser, Subcommand};
+use serde_json::{Value, json};
+use std::path::{Path, PathBuf};
+use wai_quantum::engine::{catalog, draw, job};
+use wai_quantum_cli::keys;
+
+#[derive(Parser)]
+#[command(
+ name = "wai-quantum",
+ version,
+ about = "OpenQASM programs run here as the quantum job service runs them, or sent to it; every result names its method and accuracy and is signed",
+ propagate_version = true
+)]
+struct Cli {
+ #[command(subcommand)]
+ command: Command,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ /// Run a program here, planned as the job service plans it, and sign the result
+ Run {
+ #[command(flatten)]
+ job: Job,
+ #[command(flatten)]
+ out: Output,
+ #[command(flatten)]
+ key: KeyArgs,
+ },
+ /// Show the method a program would run on and why, without running it
+ Plan {
+ #[command(flatten)]
+ job: Job,
+ /// Print the plan as JSON
+ #[arg(long)]
+ json: bool,
+ },
+ /// Check a program and print each problem with its line
+ Lint {
+ /// The program; `-` or nothing reads standard input
+ file: Option,
+ /// Print the outcome as JSON
+ #[arg(long)]
+ json: bool,
+ },
+ /// Draw a program's circuit as text
+ Draw {
+ /// The program; `-` or nothing reads standard input
+ file: Option,
+ /// Wrap at this many columns
+ #[arg(long, default_value_t = 100)]
+ width: usize,
+ },
+ /// Send a program to the job service and print its result
+ #[cfg(feature = "service")]
+ Submit {
+ #[command(flatten)]
+ job: Job,
+ #[command(flatten)]
+ out: Output,
+ /// The service's address
+ #[arg(long, env = "WAI_QUANTUM_SERVICE", default_value = "https://wai.transaction.science/quantum-jobs")]
+ service: String,
+ },
+ /// Check a receipt: a result from `run --json`, `submit --json` or the service, or a `lab` receipt
+ Verify {
+ /// The JSON; `-` or nothing reads standard input
+ file: Option,
+ },
+ /// Show the key this user signs receipts with
+ Key {
+ #[command(flatten)]
+ key: KeyArgs,
+ },
+ /// Each method, what it takes, and how far it reaches in a job's memory here
+ Methods {
+ /// Print the table as JSON
+ #[arg(long)]
+ json: bool,
+ /// The memory a job may take, GiB
+ #[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
+ memory_gb: u64,
+ },
+ /// Serve run, plan, lint, draw and verify to an agent over standard input and output (MCP)
+ Mcp {
+ /// The memory a job may take, GiB
+ #[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
+ memory_gb: u64,
+ #[command(flatten)]
+ key: KeyArgs,
+ },
+ /// The research tools: error correction, DMRG, neural states, resource estimates and more (`wai-quantum lab help`)
+ Lab {
+ #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
+ args: Vec,
+ },
+ /// Print shell completion for bash, zsh, fish, elvish or PowerShell
+ Completions { shell: clap_complete::Shell },
+}
+
+/// The memory a job may take by default, GiB.
+const DEFAULT_MEMORY_GB: u64 = 16;
+
+/// A job: a program and what is asked of it, as the service reads it.
+#[derive(Args, Clone)]
+struct Job {
+ /// The OpenQASM 3 (or 2) program; `-` or nothing reads standard input
+ file: Option,
+ /// Shots to sample (1024 when nothing else is asked)
+ #[arg(long)]
+ shots: Option,
+ /// The seed shots are sampled with (1 if not given)
+ #[arg(long)]
+ seed: Option,
+ /// A method by name, or auto (the default) for the strongest the program admits; `wai-quantum methods` lists them
+ #[arg(long)]
+ method: Option,
+ /// A Pauli observable to take the expectation of, as "Z0 Z1" or "0.5 X2" (repeatable)
+ #[arg(short = 'o', long = "observable")]
+ observables: Vec,
+ /// A basis state's amplitude, as a bit string with the highest qubit first (repeatable)
+ #[arg(short = 'a', long = "amplitude")]
+ amplitudes: Vec,
+ /// A noise channel and its probability, as depolarizing_1q=0.01 (repeatable)
+ #[arg(long, value_parser = noise_arg)]
+ noise: Vec<(String, f64)>,
+ /// A method option, as max_bond=128 (repeatable)
+ #[arg(long = "option", value_parser = option_arg)]
+ options: Vec<(String, Value)>,
+ /// The memory a job may take here, GiB
+ #[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
+ memory_gb: u64,
+}
+
+#[derive(Args, Clone)]
+struct Output {
+ /// Print the job as the service returns it: request, plan, result and receipt
+ #[arg(long)]
+ json: bool,
+ /// The most outcomes to list
+ #[arg(long, default_value_t = 16)]
+ top: usize,
+}
+
+#[derive(Args, Clone)]
+struct KeyArgs {
+ /// Sign with the key in this file (64 hex digits)
+ #[arg(long, value_name = "FILE")]
+ key: Option,
+ /// Sign with the published demo key: the receipt then shows a result is unaltered, not who signed it
+ #[arg(long, conflicts_with = "key")]
+ demo_key: bool,
+}
+
+fn noise_arg(s: &str) -> Result<(String, f64), String> {
+ let (k, v) = s.split_once('=').ok_or("give a channel and its probability, as depolarizing_1q=0.01")?;
+ if !catalog::NOISE_CHANNELS.contains(&k) {
+ return Err(format!("{k}: no such channel; the channels are {}", catalog::NOISE_CHANNELS.join(", ")));
+ }
+ let p: f64 = v.parse().map_err(|_| format!("{v}: a probability, 0 to 1"))?;
+ Ok((k.to_string(), p))
+}
+
+fn option_arg(s: &str) -> Result<(String, Value), String> {
+ let (k, v) = s.split_once('=').ok_or("give an option and its value, as max_bond=128")?;
+ // A number or true/false reads as JSON; anything else is a string.
+ let value = serde_json::from_str::(v).ok().filter(|x| x.is_number() || x.is_boolean()).unwrap_or_else(|| json!(v));
+ Ok((k.to_string(), value))
+}
+
+fn die(msg: &str) -> ! {
+ eprintln!("wai-quantum: {msg}");
+ std::process::exit(1)
+}
+
+/// The text of `file`, or standard input for `-` or none.
+fn read_input(file: Option<&Path>) -> Result {
+ match file {
+ Some(p) if p != Path::new("-") => std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display())),
+ _ => {
+ use std::io::Read as _;
+ let mut s = String::new();
+ std::io::stdin().read_to_string(&mut s).map_err(|e| format!("standard input: {e}"))?;
+ Ok(s)
+ }
+ }
+}
+
+/// The job's request, with only the fields given, as the service receives it.
+fn request(job: &Job, qasm: String) -> Value {
+ let mut r = json!({ "qasm": qasm });
+ if let Some(s) = job.shots {
+ r["shots"] = json!(s);
+ }
+ if let Some(s) = job.seed {
+ r["seed"] = json!(s);
+ }
+ if let Some(m) = &job.method {
+ r["method"] = json!(m);
+ }
+ if !job.observables.is_empty() {
+ r["observables"] = json!(job.observables);
+ }
+ if !job.amplitudes.is_empty() {
+ r["amplitudes"] = json!(job.amplitudes);
+ }
+ if !job.noise.is_empty() {
+ r["noise"] = Value::Object(job.noise.iter().map(|(k, v)| (k.clone(), json!(v))).collect());
+ }
+ if !job.options.is_empty() {
+ r["options"] = Value::Object(job.options.iter().cloned().collect());
+ }
+ r
+}
+
+/// What a result says of its energy: the command line does not meter a run.
+const UNMETERED: &str = "the command line does not meter a run's energy";
+
+/// The job body `run` gives, as the service gives it: the request, the plan,
+/// and the result sealed with `key` ([`job::run`]).
+pub fn run_here(req: &Value, memory_gb: u64, key: &wai_quantum::ed25519_dalek::SigningKey) -> Result {
+ job::run(req, memory_gb << 30, key, json!({ "unmetered": UNMETERED }))
+}
+
+/// A job body as a reader takes it in.
+fn print_job(body: &Value, top: usize) {
+ let r = &body["result"];
+ let s = |v: &Value| v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string());
+ println!("method {} — {}", s(&body["method"]), s(&body["why"]));
+ if let Some(acc) = r.get("accuracy") {
+ let mut extra: Vec = acc.as_object().map(|o| o.iter().filter(|(k, _)| *k != "class" && *k != "note").map(|(k, v)| format!("{k} {}", s(v))).collect()).unwrap_or_default();
+ if let Some(n) = acc.get("note").and_then(Value::as_str) {
+ extra.push(n.to_string());
+ }
+ println!("accuracy {}{}", s(&acc["class"]), if extra.is_empty() { String::new() } else { format!(" ({})", extra.join("; ")) });
+ }
+ println!("circuit {} qubits, {} gates", r["qubits"], r["gates"]);
+ if let Some(w) = r.get("warning").and_then(Value::as_str) {
+ println!("warning {w}");
+ }
+ if let Some(p) = r.get("global_phase").and_then(Value::as_str) {
+ println!("phase {p}");
+ }
+ if let Some(counts) = r.get("counts").and_then(Value::as_object) {
+ let total: u64 = counts.values().filter_map(Value::as_u64).sum::() + r["omitted_shots"].as_u64().unwrap_or(0);
+ println!("counts {total} shots, {} outcomes", r["distinct_outcomes"].as_u64().unwrap_or(counts.len() as u64));
+ let mut v: Vec<(&String, u64)> = counts.iter().map(|(k, c)| (k, c.as_u64().unwrap_or(0))).collect();
+ v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
+ for (k, c) in v.iter().take(top) {
+ println!(" {k} {c:>8} {:.4}", *c as f64 / total.max(1) as f64);
+ }
+ if v.len() > top {
+ println!(" … {} more (--top N)", v.len() - top);
+ }
+ }
+ for e in r.get("expectations").and_then(Value::as_array).into_iter().flatten() {
+ let bound = e.get("bound").map(|b| format!(" ± {}", s(b))).unwrap_or_default();
+ println!("⟨{}⟩ {}{bound}", s(&e["observable"]), s(&e["value"]));
+ }
+ for a in r.get("amplitudes").and_then(Value::as_array).into_iter().flatten() {
+ let (re, im) = (a["re"].as_f64().unwrap_or(f64::NAN), a["im"].as_f64().unwrap_or(f64::NAN));
+ println!("|{}⟩ {re:+.12} {}{:.12}i", s(&a["bits"]), if im < 0.0 { '-' } else { '+' }, im.abs());
+ }
+ if let Some(d) = r.get("dynamic") {
+ println!("dynamic {d}");
+ }
+ if let Some(ms) = r.get("elapsed_ms") {
+ println!("time {ms} ms");
+ }
+ if let Some(key) = r.pointer("/receipt/key").and_then(Value::as_str) {
+ let ok = job::verify(body).map(|(ok, _)| ok).unwrap_or(false);
+ println!("receipt {}, signed by {}", if ok { "verifies" } else { "DOES NOT VERIFY" }, job::did_of(key).unwrap_or_else(|| key.to_string()));
+ }
+}
+
+fn load_key(k: &KeyArgs) -> keys::Key {
+ let key = keys::load(k.key.as_deref(), k.demo_key).unwrap_or_else(|e| die(&e));
+ keys::notes(&key);
+ key
+}
+
+#[cfg(feature = "service")]
+fn submit(req: &Value, service: &str) -> Result {
+ use std::time::Duration;
+ let base = service.trim_end_matches('/');
+ let agent: ureq::Agent = ureq::Agent::config_builder().http_status_as_error(false).timeout_global(Some(Duration::from_secs(120))).build().into();
+ let ua = concat!("wai-quantum/", env!("CARGO_PKG_VERSION"));
+ let read = |mut resp: ureq::http::Response| -> Result<(u16, Value), String> {
+ let code = resp.status().as_u16();
+ let v: Value = resp.body_mut().read_json().map_err(|e| format!("the service's reply did not read: {e}"))?;
+ Ok((code, v))
+ };
+ let (mut code, mut body) = read(agent.post(format!("{base}/v1/run")).header("user-agent", ua).send_json(req).map_err(|e| format!("{base}: {e}"))?)?;
+ // A job still running after the service's wait is fetched until it ends.
+ while code == 202 {
+ let id = body["id"].as_str().ok_or("the service answered 202 with no job id")?.to_string();
+ eprintln!("note: job {id} is {}; waiting", body["status"].as_str().unwrap_or("running"));
+ std::thread::sleep(Duration::from_secs(3));
+ (code, body) = read(agent.get(format!("{base}/v1/jobs/{id}")).header("user-agent", ua).call().map_err(|e| format!("{base}: {e}"))?)?;
+ if body["status"] != "queued" && body["status"] != "running" {
+ break;
+ }
+ code = 202;
+ }
+ if code >= 400 {
+ return Err(body["error"].as_str().map(str::to_string).unwrap_or_else(|| format!("HTTP {code}: {body}")));
+ }
+ if body["status"] != "done" {
+ return Err(format!("the job {}: {}", body["status"].as_str().unwrap_or("failed"), body["error"].as_str().unwrap_or("")));
+ }
+ Ok(body)
+}
+
+
+fn main() {
+ let cli = Cli::parse();
+ match cli.command {
+ Command::Run { job, out, key } => {
+ let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
+ let req = request(&job, qasm);
+ let k = load_key(&key);
+ let body = run_here(&req, job.memory_gb, &k.key).unwrap_or_else(|e| die(&e));
+ if out.json {
+ println!("{body}");
+ } else {
+ print_job(&body, out.top);
+ }
+ }
+ Command::Plan { job, json } => {
+ let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
+ let p = catalog::plan(&request(&job, qasm), job.memory_gb << 30).unwrap_or_else(|e| die(&e));
+ if json {
+ println!("{}", serde_json::to_value(&p).unwrap_or(Value::Null));
+ } else {
+ println!("method {}", p.method);
+ println!("why {}", p.why);
+ println!("qubits {}", p.qubits);
+ println!("memory {:.1} MiB at most", p.memory_bytes as f64 / (1u64 << 20) as f64);
+ println!("work {}", p.work);
+ }
+ }
+ Command::Lint { file, json } => {
+ let name = file.as_deref().filter(|p| *p != Path::new("-")).map(|p| p.display().to_string()).unwrap_or_else(|| "".into());
+ let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
+ let v = job::lint(&src);
+ if json {
+ println!("{v}");
+ } else if v["ok"] == true {
+ let dynamic = v["dynamic_steps"].as_u64().map(|s| format!(", dynamic: {s} steps")).unwrap_or_default();
+ println!("{name}: ok — {} qubits, {} gates, {} bits measured{dynamic}", v["qubits"], v["gates"], v["measured"]);
+ } else {
+ println!("{name}:{}", v["error"].as_str().unwrap_or("").trim_start_matches("line "));
+ }
+ if v["ok"] != true {
+ std::process::exit(1);
+ }
+ }
+ Command::Draw { file, width } => {
+ let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
+ let p = wai_quantum::qasm::parse(&src).unwrap_or_else(|e| die(&e));
+ print!("{}", draw::draw(&p, width));
+ }
+ #[cfg(feature = "service")]
+ Command::Submit { job, out, service } => {
+ let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
+ let req = request(&job, qasm);
+ let body = submit(&req, &service).unwrap_or_else(|e| die(&e));
+ if out.json {
+ println!("{body}");
+ } else {
+ print_job(&body, out.top);
+ }
+ }
+ Command::Verify { file } => {
+ let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
+ let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}")));
+ if v.get("request").is_some() && v.get("result").is_some() {
+ let (ok, key) = job::verify(&v).unwrap_or_else(|e| die(&e));
+ println!("signature {}", if ok { "VALID" } else { "INVALID" });
+ println!("binds the request and the result as given");
+ if let Some(k) = key.as_str() {
+ println!("signer {}", job::did_of(k).unwrap_or_else(|| k.to_string()));
+ }
+ if !ok {
+ std::process::exit(1);
+ }
+ } else {
+ lab::verify_receipt(&src);
+ }
+ }
+ Command::Key { key } => {
+ let k = load_key(&key);
+ println!("{}", wai_quantum::quantum_receipt::did_key(&k.key.verifying_key()));
+ match &k.path {
+ Some(p) => eprintln!("key file {}", p.display()),
+ None => eprintln!("the published demo key"),
+ }
+ }
+ Command::Methods { json, memory_gb } => {
+ let m = catalog::describe(memory_gb << 30);
+ if json {
+ println!("{m}");
+ } else {
+ for x in m.as_array().into_iter().flatten() {
+ let reach = if x.get("available") == Some(&Value::Bool(false)) { "not in this build".to_string() } else { format!("up to {} qubits", x["max_qubits"]) };
+ println!("{:<20} {reach:<20} {}", x["method"].as_str().unwrap_or(""), x["accuracy"].as_str().unwrap_or(""));
+ println!("{:<20} {}", "", x["circuits"].as_str().unwrap_or(""));
+ }
+ }
+ }
+ Command::Mcp { memory_gb, key } => {
+ let k = load_key(&key);
+ mcp::serve(memory_gb, &k.key);
+ }
+ Command::Lab { args } => lab::main(args),
+ Command::Completions { shell } => {
+ clap_complete::generate(shell, &mut Cli::command(), "wai-quantum", &mut std::io::stdout());
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn job(qasm: &str) -> (Job, String) {
+ (
+ Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1 },
+ qasm.to_string(),
+ )
+ }
+
+ #[test]
+ fn a_run_here_is_the_services_job_and_its_receipt_verifies() {
+ let (mut j, q) = job("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];");
+ j.shots = Some(1000);
+ j.observables = vec!["Z0 Z1".into()];
+ let req = request(&j, q);
+ // Only what was given is in the request.
+ assert_eq!(req.as_object().unwrap().keys().collect::>(), ["qasm", "shots", "observables"]);
+ let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[3; 32]);
+ let body = run_here(&req, 1, &key).unwrap();
+ assert_eq!(body["method"], "stabilizer");
+ assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
+ let counts = body["result"]["counts"].as_object().unwrap();
+ assert!(counts.keys().all(|k| k == "00" || k == "11"), "{counts:?}");
+ assert!(job::verify(&body).unwrap().0);
+ // The result changed, or the request, and the receipt no longer holds.
+ let mut forged = body.clone();
+ forged["result"]["expectations"][0]["value"] = json!(0.5);
+ assert!(!job::verify(&forged).unwrap().0);
+ let mut forged = body.clone();
+ forged["request"]["shots"] = json!(999);
+ assert!(!job::verify(&forged).unwrap().0);
+ // A JSON writer's round trip leaves it verifying.
+ let again: Value = serde_json::from_str(&body.to_string()).unwrap();
+ assert!(job::verify(&again).unwrap().0);
+ }
+
+ #[test]
+ fn noise_and_options_are_read_and_a_misspelt_channel_is_refused() {
+ assert_eq!(noise_arg("depolarizing_1q=0.01").unwrap(), ("depolarizing_1q".into(), 0.01));
+ assert!(noise_arg("depolarizing=0.01").unwrap_err().contains("no such channel"));
+ assert!(noise_arg("readout").is_err());
+ assert_eq!(option_arg("max_bond=128").unwrap().1, json!(128));
+ assert_eq!(option_arg("x=true").unwrap().1, json!(true));
+ assert_eq!(option_arg("kind=fast").unwrap().1, json!("fast"));
+ }
+
+
+ #[test]
+ fn the_command_line_parses_as_documented() {
+ Cli::command().debug_assert();
+ let c = Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--shots", "10", "-o", "Z0", "-o", "X1 X2", "--noise", "readout=0.02", "--option", "max_bond=8", "--json"]).unwrap();
+ match c.command {
+ Command::Run { job, out, .. } => {
+ assert_eq!(job.shots, Some(10));
+ assert_eq!(job.observables, ["Z0", "X1 X2"]);
+ assert_eq!(job.noise, [("readout".to_string(), 0.02)]);
+ assert!(out.json);
+ }
+ _ => panic!("not run"),
+ }
+ // An unknown flag is an error, not ignored.
+ assert!(Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--shot", "10"]).is_err());
+ // The research tools take their own words.
+ match Cli::try_parse_from(["wai-quantum", "lab", "qec", "-d", "5", "--p", "0.003"]).unwrap().command {
+ Command::Lab { args } => assert_eq!(args, ["qec", "-d", "5", "--p", "0.003"]),
+ _ => panic!("not lab"),
+ }
+ }
+}
diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs
new file mode 100644
index 000000000..9d8f140c1
--- /dev/null
+++ b/wai/wai-quantum-cli/src/mcp.rs
@@ -0,0 +1,196 @@
+//! `wai-quantum mcp`: the command line's tools for an agent, as a Model Context
+//! Protocol server on standard input and output (one JSON-RPC message per
+//! line). Runs happen here, planned as the job service plans them, and are
+//! signed with this user's key; nothing leaves the machine.
+
+use serde_json::{Value, json};
+use std::io::{BufRead, Write};
+use wai_quantum::ed25519_dalek::SigningKey;
+use wai_quantum::engine::{catalog, draw, job};
+
+const PROTOCOL: &str = "2025-06-18";
+
+fn job_schema() -> Value {
+ json!({
+ "type": "object",
+ "required": ["qasm"],
+ "properties": {
+ "qasm": { "type": "string", "description": "The program, OpenQASM 3 or 2: registers and broadcast over them, the standard gates, ctrl/negctrl modifiers, gate definitions, measurement and reset anywhere, and if on measured bits. Loops, inv and pow are refused with their line." },
+ "shots": { "type": "integer", "minimum": 0, "description": "Shots to sample; 1024 when nothing else is asked." },
+ "seed": { "type": "integer", "minimum": 0, "description": "The sampling seed (default 1)." },
+ "method": { "type": "string", "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." },
+ "observables": { "type": "array", "items": { "type": "string" }, "description": "Pauli observables, as \"Z0 Z1\" or \"0.5 X2\"." },
+ "amplitudes": { "type": "array", "items": { "type": "string" }, "description": "Basis states whose amplitudes to return, as bit strings with the highest qubit first." },
+ "noise": { "type": "object", "description": "Channel probabilities", "properties": catalog::NOISE_CHANNELS.iter().map(|k| (k.to_string(), json!({ "type": "number", "minimum": 0, "maximum": 1 }))).collect::>(), "additionalProperties": false },
+ "options": { "type": "object", "description": "Method options: max_bond, fusion, trajectories, threshold, memory_gb." }
+ }
+ })
+}
+
+fn qasm_schema(extra: Value) -> Value {
+ let mut s = json!({ "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string", "description": "The program, OpenQASM 3 or 2." } } });
+ if let (Some(p), Some(e)) = (s["properties"].as_object_mut(), extra.as_object()) {
+ p.extend(e.clone());
+ }
+ s
+}
+
+fn tools() -> Value {
+ json!([
+ {
+ "name": "run_quantum_circuit",
+ "title": "Run a quantum program here",
+ "description": "Run an OpenQASM program on this machine, planned as the quantum job service plans it: measurement counts, Pauli expectation values and/or amplitudes, the method chosen and why, its accuracy class, and a receipt signed with this user's key. Every number in the answer was computed; quote them, do not estimate.",
+ "inputSchema": job_schema(),
+ },
+ {
+ "name": "plan_quantum_circuit",
+ "title": "Plan a quantum program",
+ "description": "The method a program would run on and why, the memory and work it would take, without running it.",
+ "inputSchema": job_schema(),
+ },
+ {
+ "name": "lint_qasm",
+ "title": "Check an OpenQASM program",
+ "description": "Parse an OpenQASM program as the engine reads it: its qubits, gates and measured bits, or the first problem with its line.",
+ "inputSchema": qasm_schema(json!({})),
+ },
+ {
+ "name": "draw_circuit",
+ "title": "Draw a circuit",
+ "description": "A program's circuit as text: one wire per qubit, controls as ●, targets as ⊕, measurements as M with their bit.",
+ "inputSchema": qasm_schema(json!({ "width": { "type": "integer", "minimum": 20, "description": "Wrap at this many columns (default 100)." } })),
+ },
+ {
+ "name": "verify_quantum_result",
+ "title": "Check a result's receipt",
+ "description": "Whether a job's receipt seals its request and its result, as run_quantum_circuit or the job service returns them, and which key signed it.",
+ "inputSchema": { "type": "object", "required": ["job"], "properties": { "job": { "type": "object", "description": "The job: {\"request\": ..., \"result\": ...}, the result carrying its receipt." } } },
+ },
+ {
+ "name": "list_methods",
+ "title": "List the methods",
+ "description": "Each simulation method: its accuracy class, the circuits it takes, and the most qubits it holds in a job's memory here.",
+ "inputSchema": { "type": "object", "properties": {} },
+ }
+ ])
+}
+
+/// A tool's answer: its JSON, or an error the agent can read and act on.
+fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey) -> Result {
+ let qasm = || args.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program");
+ match name {
+ "run_quantum_circuit" => {
+ qasm()?;
+ crate::run_here(args, memory_gb, key)
+ }
+ "plan_quantum_circuit" => {
+ qasm()?;
+ catalog::plan(args, memory_gb << 30).map(|p| serde_json::to_value(p).unwrap_or(Value::Null))
+ }
+ "lint_qasm" => Ok(job::lint(qasm()?)),
+ "draw_circuit" => {
+ let p = wai_quantum::qasm::parse(qasm()?)?;
+ let width = args.get("width").and_then(Value::as_u64).unwrap_or(100).max(20) as usize;
+ Ok(json!({ "text": draw::draw(&p, width) }))
+ }
+ "verify_quantum_result" => {
+ let given = args.get("job").ok_or("give \"job\": {\"request\": ..., \"result\": ...}")?;
+ let (valid, key) = job::verify(given)?;
+ Ok(json!({ "valid": valid, "key": key }))
+ }
+ "list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })),
+ _ => Err(format!("no tool {name}")),
+ }
+}
+
+/// The reply to one message, or `None` for a notification.
+fn handle(msg: &Value, memory_gb: u64, key: &SigningKey) -> Option {
+ let id = msg.get("id")?.clone();
+ let method = msg.get("method").and_then(Value::as_str).unwrap_or("");
+ let params = msg.get("params").cloned().unwrap_or(json!({}));
+ let ok = |result: Value| json!({ "jsonrpc": "2.0", "id": id, "result": result });
+ let err = |code: i64, message: String| json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } });
+ Some(match method {
+ "initialize" => ok(json!({
+ "protocolVersion": params.get("protocolVersion").and_then(Value::as_str).unwrap_or(PROTOCOL),
+ "capabilities": { "tools": {} },
+ "serverInfo": { "name": "wai-quantum", "title": "wai-quantum", "version": env!("CARGO_PKG_VERSION") },
+ "instructions": "Quantum programs in OpenQASM, run on this machine as the quantum job service runs them. Plan or lint before a large run. Every result names its method and accuracy; quote its numbers rather than estimating them."
+ })),
+ "ping" => ok(json!({})),
+ "tools/list" => ok(json!({ "tools": tools() })),
+ "tools/call" => {
+ let name = params.get("name").and_then(Value::as_str).unwrap_or("");
+ let args = params.get("arguments").cloned().unwrap_or(json!({}));
+ match call(name, &args, memory_gb, key) {
+ Ok(v) => ok(json!({ "content": [{ "type": "text", "text": v.to_string() }], "structuredContent": v, "isError": false })),
+ Err(e) => ok(json!({ "content": [{ "type": "text", "text": e }], "isError": true })),
+ }
+ }
+ _ => err(-32601, format!("no method {method}")),
+ })
+}
+
+/// Serve until standard input closes.
+pub fn serve(memory_gb: u64, key: &SigningKey) {
+ let stdin = std::io::stdin();
+ let mut out = std::io::stdout().lock();
+ for line in stdin.lock().lines() {
+ let Ok(line) = line else { break };
+ if line.trim().is_empty() {
+ continue;
+ }
+ let reply = match serde_json::from_str::(&line) {
+ Ok(msg) => handle(&msg, memory_gb, key),
+ Err(e) => Some(json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })),
+ };
+ if let Some(r) = reply
+ && writeln!(out, "{r}").and_then(|_| out.flush()).is_err()
+ {
+ break;
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn key() -> SigningKey {
+ SigningKey::from_bytes(&[5; 32])
+ }
+
+ #[test]
+ fn an_agent_initializes_lists_the_tools_and_runs_a_circuit() {
+ let init = handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": PROTOCOL } }), 1, &key()).unwrap();
+ assert_eq!(init["result"]["serverInfo"]["name"], "wai-quantum");
+ assert!(handle(&json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }), 1, &key()).is_none());
+ let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key()).unwrap();
+ let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
+ assert_eq!(names, ["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "draw_circuit", "verify_quantum_result", "list_methods"]);
+ let run = handle(
+ &json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "run_quantum_circuit", "arguments": { "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "observables": ["Z0 Z1"] } } }),
+ 1,
+ &key(),
+ )
+ .unwrap();
+ let job = &run["result"]["structuredContent"];
+ assert_eq!(run["result"]["isError"], false, "{run}");
+ assert_eq!(job["result"]["expectations"][0]["value"], 1.0);
+ // The job it returns checks with its own tool.
+ let check = handle(&json!({ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "verify_quantum_result", "arguments": { "job": job } } }), 1, &key()).unwrap();
+ assert_eq!(check["result"]["structuredContent"]["valid"], true, "{check}");
+ }
+
+ #[test]
+ fn a_bad_program_is_an_error_the_agent_can_read() {
+ let r = handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "run_quantum_circuit", "arguments": { "qasm": "qubit[1] q; foo q[0];" } } }), 1, &key()).unwrap();
+ assert_eq!(r["result"]["isError"], true);
+ assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("foo"), "{r}");
+ let lint = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "lint_qasm", "arguments": { "qasm": "qubit[1] q;\nfoo q[0];" } } }), 1, &key()).unwrap();
+ assert_eq!(lint["result"]["structuredContent"]["line"], 2, "{lint}");
+ let none = handle(&json!({ "jsonrpc": "2.0", "id": 3, "method": "nonsense" }), 1, &key()).unwrap();
+ assert_eq!(none["error"]["code"], -32601);
+ }
+}
diff --git a/wai/wai-quantum-desktop/.gitignore b/wai/wai-quantum-desktop/.gitignore
new file mode 100644
index 000000000..e8e450bed
--- /dev/null
+++ b/wai/wai-quantum-desktop/.gitignore
@@ -0,0 +1 @@
+gen/
diff --git a/wai/wai-quantum-desktop/Cargo.lock b/wai/wai-quantum-desktop/Cargo.lock
new file mode 100644
index 000000000..00e582c57
--- /dev/null
+++ b/wai/wai-quantum-desktop/Cargo.lock
@@ -0,0 +1,4691 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
+dependencies = [
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.1",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "brotli"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.13.2",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9"
+dependencies = [
+ "semver",
+ "serde",
+ "toml 1.1.8+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
+dependencies = [
+ "fnv",
+ "uuid",
+ "web-time",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "4.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cookie"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
+dependencies = [
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "ctor"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d"
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "defmt"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "embed-resource"
+version = "3.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 1.1.8+spec-1.1.0",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.9.1",
+ "zlib-rs",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.13.2",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png 0.17.16",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
+dependencies = [
+ "defmt",
+ "log",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
+dependencies = [
+ "bitflags 2.13.2",
+ "serde",
+]
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.191"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "markup5ever"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.21",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.2",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.13.2",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.2",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-core-text",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "plist"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65dca778f41c79ebbe8b2b2139fee86f01b766b67130d108c7c2b8ffbd949f77"
+dependencies = [
+ "base64 0.23.1",
+ "indexmap 2.14.2",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.17+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
+dependencies = [
+ "libredox",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
+dependencies = [
+ "bitflags 2.13.2",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "foldhash",
+ "indexmap 2.14.2",
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f"
+dependencies = [
+ "base64 0.23.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.2",
+ "jiff",
+ "schemars 0.9.0",
+ "schemars 1.2.3",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "siphasher"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_vcruntime"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.37.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "static_vcruntime",
+ "strsim",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
+dependencies = [
+ "base64 0.23.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png 0.18.1",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.119",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dom_query",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.21",
+ "toml 1.1.8+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 1.1.8+spec-1.1.0",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
+dependencies = [
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
+dependencies = [
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+
+[[package]]
+name = "tokio"
+version = "1.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.8+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
+dependencies = [
+ "indexmap 2.14.2",
+ "serde_core",
+ "serde_spanned 1.1.2",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.2",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.2",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.17+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
+dependencies = [
+ "indexmap 2.14.2",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.21",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b"
+dependencies = [
+ "icu_properties",
+ "regex",
+ "serde",
+ "url",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "wai-quantum"
+version = "0.6.11"
+dependencies = [
+ "blake3",
+ "ed25519-dalek",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "wai-quantum-cli"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "clap_complete",
+ "getrandom 0.4.3",
+ "serde_json",
+ "wai-quantum",
+]
+
+[[package]]
+name = "wai-quantum-desktop"
+version = "0.1.0"
+dependencies = [
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "wai-quantum",
+ "wai-quantum-cli",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
+dependencies = [
+ "js-sys",
+ "tokio",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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 = "web_atoms"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
+dependencies = [
+ "thiserror 2.0.21",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "windows-sys 0.61.2",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "wry"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.21",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/wai/wai-quantum-desktop/Cargo.toml b/wai/wai-quantum-desktop/Cargo.toml
new file mode 100644
index 000000000..42264825d
--- /dev/null
+++ b/wai/wai-quantum-desktop/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "wai-quantum-desktop"
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.97"
+license = "Apache-2.0"
+description = "Quantum Studio on the desktop: the Studio's interface (wai/quantum-studio) around the quantum job service's engine built natively, each job in a process of its own, signed with the user's key, with the service one switch away."
+repository = "https://git.transaction.science/Transaction-Science/open-standards"
+publish = false
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = [] }
+serde_json = { version = "1", features = ["float_roundtrip"] }
+wai-quantum = { version = "0.6.11", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt"] }
+wai-quantum-cli = { version = "0.1.0", path = "../wai-quantum-cli", default-features = false }
+
+[features]
+default = ["custom-protocol"]
+# The interface served from the bundled build (../quantum-studio/dist), as a release does.
+custom-protocol = ["tauri/custom-protocol"]
+# The single-precision state vector on the GPU, on macOS.
+metal = ["wai-quantum/quantum_sv_metal"]
+
+[profile.release]
+codegen-units = 1
+lto = true
diff --git a/wai/wai-quantum-desktop/README.md b/wai/wai-quantum-desktop/README.md
new file mode 100644
index 000000000..297021c88
--- /dev/null
+++ b/wai/wai-quantum-desktop/README.md
@@ -0,0 +1,22 @@
+# Quantum Studio for the desktop
+
+[Quantum Studio](../quantum-studio/) as an app. The interface is the browser's. The engine underneath is the quantum job service's engine ([`wai_quantum::engine`](../wai-quantum/src/engine/mod.rs)), built natively: it uses every core, holds as much memory as the machine has, and works offline.
+
+- **Each job runs in a process of its own,** this binary run as `worker`, so a large job cannot take the app's memory with it, and Cancel ends it.
+- **The worker plans and runs; the app seals.** It signs the answer with this user's key, the command line's (`~/.config/wai/quantum-key`), which never leaves the app.
+- **Saved jobs and programs** go to `~/Documents/Quantum Studio/`. `wai-quantum verify` checks a saved job.
+- **The job service is one switch away,** as in the browser.
+- **Memory:** a job may take `$WAI_QUANTUM_MEMORY_GB` (16 by default). The plan says when a job needs more.
+
+## Building
+
+The app embeds the Studio's build, so build the Studio first:
+
+```sh
+(cd ../quantum-studio && npm ci && node build.mjs)
+cargo build --release # add --features metal for the GPU state vector on macOS
+./target/release/wai-quantum-desktop # the app
+./target/release/wai-quantum-desktop check # a job through the worker, sealed and verified, with no window
+```
+
+It draws with the operating system's own web view, so the app carries no browser of its own. Installers for macOS, Windows and Linux come from `cargo tauri build`.
diff --git a/wai/wai-quantum-desktop/build.rs b/wai/wai-quantum-desktop/build.rs
new file mode 100644
index 000000000..d860e1e6a
--- /dev/null
+++ b/wai/wai-quantum-desktop/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/wai/wai-quantum-desktop/capabilities/default.json b/wai/wai-quantum-desktop/capabilities/default.json
new file mode 100644
index 000000000..d2afe49d5
--- /dev/null
+++ b/wai/wai-quantum-desktop/capabilities/default.json
@@ -0,0 +1,6 @@
+{
+ "identifier": "default",
+ "description": "The Studio's window: the core bridge, by which it calls the app's own commands (run, plan, lint, draw, verify, save).",
+ "windows": ["main"],
+ "permissions": ["core:default"]
+}
diff --git a/wai/wai-quantum-desktop/icons/128x128.png b/wai/wai-quantum-desktop/icons/128x128.png
new file mode 100644
index 000000000..565aba416
Binary files /dev/null and b/wai/wai-quantum-desktop/icons/128x128.png differ
diff --git a/wai/wai-quantum-desktop/icons/128x128@2x.png b/wai/wai-quantum-desktop/icons/128x128@2x.png
new file mode 100644
index 000000000..5a9b89c60
Binary files /dev/null and b/wai/wai-quantum-desktop/icons/128x128@2x.png differ
diff --git a/wai/wai-quantum-desktop/icons/32x32.png b/wai/wai-quantum-desktop/icons/32x32.png
new file mode 100644
index 000000000..4ce7da2d8
Binary files /dev/null and b/wai/wai-quantum-desktop/icons/32x32.png differ
diff --git a/wai/wai-quantum-desktop/icons/icon.png b/wai/wai-quantum-desktop/icons/icon.png
new file mode 100644
index 000000000..c3330bf4a
Binary files /dev/null and b/wai/wai-quantum-desktop/icons/icon.png differ
diff --git a/wai/wai-quantum-desktop/src/main.rs b/wai/wai-quantum-desktop/src/main.rs
new file mode 100644
index 000000000..d5f0312e7
--- /dev/null
+++ b/wai/wai-quantum-desktop/src/main.rs
@@ -0,0 +1,267 @@
+//! Quantum Studio on the desktop: the Studio's interface (wai/quantum-studio)
+//! around the quantum job service's engine, built natively.
+//!
+//! - A job runs in a process of its own (this binary run as `worker`), so a
+//! large one cannot take the app's memory with it, and Cancel ends it. The
+//! worker plans and runs; the app seals the answer with this user's key
+//! (the command line's, `~/.config/wai/quantum-key`), which never leaves it.
+//! - Lint, draw, plan, the methods and a receipt's check answer at once, here.
+//! - A job and a program are saved under `~/Documents/Quantum Studio/`.
+//! - The service is one switch away in the interface, as in the browser.
+//!
+//! ```text
+//! wai-quantum-desktop the app
+//! wai-quantum-desktop worker one job: {"request", "job_bytes"} on stdin, {"plan", "result"} out
+//! wai-quantum-desktop check a Bell pair through the worker, sealed and checked; no window
+//! ```
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use serde_json::{Value, json};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+use std::process::{Child, Command, Stdio};
+use std::sync::{Arc, Mutex};
+use wai_quantum::ed25519_dalek::SigningKey;
+use wai_quantum::engine::{catalog, draw, job};
+
+/// What a result says of its energy here.
+const UNMETERED: &str = "the desktop app does not meter a run's energy";
+
+/// The memory a job may take: `$WAI_QUANTUM_MEMORY_GB`, or 16 GiB.
+fn job_bytes() -> u64 {
+ std::env::var("WAI_QUANTUM_MEMORY_GB").ok().and_then(|v| v.parse::().ok()).unwrap_or(16).max(1) << 30
+}
+
+fn error_json(e: impl std::fmt::Display) -> String {
+ json!({ "error": e.to_string() }).to_string()
+}
+
+/// The app's state: the key it seals with, the memory a job may take, and
+/// the job running now.
+struct Studio {
+ key: SigningKey,
+ job_bytes: u64,
+ running: Arc>>,
+ cancelled: Arc>,
+}
+
+/// One job in its own process: plan and run, no key.
+fn worker() {
+ let mut input = String::new();
+ let out = match std::io::stdin().read_to_string(&mut input).map_err(|e| e.to_string()).and_then(|_| serde_json::from_str::(&input).map_err(|e| e.to_string())) {
+ Ok(v) => match job::plan_and_run(&v["request"], v["job_bytes"].as_u64().unwrap_or(1 << 30)) {
+ Ok((plan, result)) => json!({ "plan": plan, "result": result }).to_string(),
+ Err(e) => error_json(e),
+ },
+ Err(e) => error_json(format!("the job did not arrive: {e}")),
+ };
+ println!("{out}");
+}
+
+/// Run `request` in a worker process (`exe worker`), keeping its handle in
+/// `running` so it can be ended, and seal the answer with `key`.
+fn run_in_worker(exe: &Path, request: &Value, job_bytes: u64, key: &SigningKey, running: &Mutex>, cancelled: &Mutex) -> Result {
+ *cancelled.lock().unwrap() = false;
+ let mut child = Command::new(exe).arg("worker").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().map_err(|e| format!("the job's process did not start: {e}"))?;
+ let input = json!({ "request": request, "job_bytes": job_bytes }).to_string();
+ child.stdin.take().expect("piped").write_all(input.as_bytes()).map_err(|e| format!("the job did not reach its process: {e}"))?;
+ let mut stdout = child.stdout.take().expect("piped");
+ *running.lock().unwrap() = Some(child);
+ let mut out = String::new();
+ let read = stdout.read_to_string(&mut out);
+ let status = running.lock().unwrap().take().map(|mut c| c.wait());
+ if *cancelled.lock().unwrap() {
+ return Err("cancelled".into());
+ }
+ read.map_err(|e| format!("the job's answer did not arrive: {e}"))?;
+ let v: Value = serde_json::from_str(out.trim()).map_err(|_| match status {
+ Some(Ok(s)) => format!("the job's process ended without an answer ({s}); a job too large for this computer's memory ends this way"),
+ _ => "the job's process ended without an answer".to_string(),
+ })?;
+ if let Some(e) = v.get("error").and_then(Value::as_str) {
+ return Err(e.to_string());
+ }
+ let plan: catalog::Plan = serde_json::from_value(v["plan"].clone()).map_err(|e| format!("the job's plan did not read: {e}"))?;
+ Ok(job::seal(request, &plan, v["result"].clone(), key, json!({ "unmetered": UNMETERED })))
+}
+
+fn read_request(request: &str) -> Result {
+ serde_json::from_str(request).map_err(|e| format!("the job is not JSON: {e}"))
+}
+
+#[tauri::command]
+async fn run_job(studio: tauri::State<'_, Studio>, request: String) -> Result {
+ let req = match read_request(&request) {
+ Ok(r) => r,
+ Err(e) => return Ok(error_json(e)),
+ };
+ let exe = std::env::current_exe().map_err(|e| e.to_string())?;
+ let (key, bytes, running, cancelled) = (studio.key.clone(), studio.job_bytes, studio.running.clone(), studio.cancelled.clone());
+ // The wait for the worker goes to a thread of its own, off the interface's.
+ let out = tauri::async_runtime::spawn_blocking(move || run_in_worker(&exe, &req, bytes, &key, &running, &cancelled)).await.map_err(|e| e.to_string())?;
+ Ok(match out {
+ Ok(body) => body.to_string(),
+ Err(e) => error_json(e),
+ })
+}
+
+#[tauri::command]
+fn cancel(studio: tauri::State<'_, Studio>) -> String {
+ *studio.cancelled.lock().unwrap() = true;
+ if let Some(c) = studio.running.lock().unwrap().as_mut() {
+ let _ = c.kill();
+ }
+ json!({ "ok": true }).to_string()
+}
+
+#[tauri::command]
+fn plan_job(studio: tauri::State<'_, Studio>, request: String) -> String {
+ match read_request(&request).and_then(|r| catalog::plan(&r, studio.job_bytes)) {
+ Ok(p) => serde_json::to_value(p).map(|v| v.to_string()).unwrap_or_else(error_json),
+ Err(e) => error_json(e),
+ }
+}
+
+#[tauri::command]
+fn methods(studio: tauri::State<'_, Studio>) -> String {
+ json!({ "methods": catalog::describe(studio.job_bytes) }).to_string()
+}
+
+#[tauri::command]
+fn lint(qasm: String) -> String {
+ job::lint(&qasm).to_string()
+}
+
+#[tauri::command]
+fn draw(qasm: String, width: u32) -> String {
+ match wai_quantum::qasm::parse(&qasm) {
+ Ok(p) => json!({ "text": draw::draw(&p, width.max(20) as usize) }).to_string(),
+ Err(e) => error_json(e),
+ }
+}
+
+#[tauri::command]
+fn verify(job: String) -> String {
+ match serde_json::from_str::(&job).map_err(|e| format!("not JSON: {e}")).and_then(|v| job::verify(&v)) {
+ Ok((valid, key)) => json!({ "valid": valid, "signer": key.as_str().and_then(job::did_of), "key": key }).to_string(),
+ Err(e) => error_json(e),
+ }
+}
+
+#[tauri::command]
+fn signer(studio: tauri::State<'_, Studio>) -> String {
+ json!({ "did": wai_quantum::quantum_receipt::did_key(&studio.key.verifying_key()) }).to_string()
+}
+
+/// Where the Studio keeps what it saves: `~/Documents/Quantum Studio/`.
+fn save_dir() -> Result {
+ let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).ok_or("no home folder to save in")?;
+ let dir = Path::new(&home).join("Documents").join("Quantum Studio");
+ std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
+ Ok(dir)
+}
+
+/// Write `text` as `name` in `dir`, beside anything there: `name`, then
+/// `stem-2.ext`, `stem-3.ext`, ….
+fn save_as(dir: &Path, name: &str, text: &str) -> Result {
+ let name = Path::new(name).file_name().and_then(|n| n.to_str()).filter(|n| !n.is_empty()).unwrap_or("untitled");
+ let (stem, ext) = name.rsplit_once('.').map(|(s, e)| (s.to_string(), format!(".{e}"))).unwrap_or((name.to_string(), String::new()));
+ let mut path = dir.join(name);
+ let mut n = 2;
+ while path.exists() {
+ path = dir.join(format!("{stem}-{n}{ext}"));
+ n += 1;
+ }
+ std::fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display()))?;
+ Ok(path)
+}
+
+#[tauri::command]
+fn save_job(name: String, job: String) -> String {
+ match save_dir().and_then(|d| save_as(&d, &name, &job)) {
+ Ok(p) => json!({ "path": p.display().to_string() }).to_string(),
+ Err(e) => error_json(e),
+ }
+}
+
+#[tauri::command]
+fn save_program(name: String, text: String) -> String {
+ match save_dir().and_then(|d| save_as(&d, &name, &text)) {
+ Ok(p) => json!({ "path": p.display().to_string() }).to_string(),
+ Err(e) => error_json(e),
+ }
+}
+
+/// A Bell pair through the worker, sealed and checked, with no window: what
+/// a build is held to where there is no screen.
+fn check() -> Result {
+ let exe = std::env::current_exe().map_err(|e| e.to_string())?;
+ let key = SigningKey::from_bytes(&[11; 32]);
+ let req = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 1000, "observables": ["Z0 Z1"] });
+ let body = run_in_worker(&exe, &req, 1 << 30, &key, &Mutex::new(None), &Mutex::new(false))?;
+ let (ok, _) = job::verify(&body)?;
+ let counts = body["result"]["counts"].as_object().ok_or("no counts")?;
+ if !ok || body["method"] != "stabilizer" || body["result"]["expectations"][0]["value"] != 1.0 || !counts.keys().all(|k| k == "00" || k == "11") {
+ return Err(format!("the Bell pair came back wrong: {body}"));
+ }
+ Ok(format!("check: a Bell pair ran in the worker on the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, and its receipt verifies ({} ms)", body["result"]["elapsed_ms"]))
+}
+
+fn main() {
+ match std::env::args().nth(1).as_deref() {
+ Some("worker") => return worker(),
+ Some("check") => {
+ match check() {
+ Ok(s) => println!("{s}"),
+ Err(e) => {
+ eprintln!("check failed: {e}");
+ std::process::exit(1);
+ }
+ }
+ return;
+ }
+ _ => {}
+ }
+ let key = match wai_quantum_cli::keys::load(None, false) {
+ Ok(k) => k.key,
+ Err(e) => {
+ eprintln!("wai-quantum-desktop: no signing key: {e}");
+ std::process::exit(1);
+ }
+ };
+ tauri::Builder::default()
+ .manage(Studio { key, job_bytes: job_bytes(), running: Arc::new(Mutex::new(None)), cancelled: Arc::new(Mutex::new(false)) })
+ .invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, draw, verify, signer, save_job, save_program])
+ .run(tauri::generate_context!())
+ .expect("the window opens");
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn saving_never_overwrites() {
+ let dir = std::env::temp_dir().join(format!("wai-quantum-desktop-save-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ let a = save_as(&dir, "job.json", "1").unwrap();
+ let b = save_as(&dir, "job.json", "2").unwrap();
+ let c = save_as(&dir, "../escape.json", "3").unwrap();
+ assert_eq!(a.file_name().unwrap(), "job.json");
+ assert_eq!(b.file_name().unwrap(), "job-2.json");
+ assert_eq!(c.parent().unwrap(), dir, "a name cannot leave the folder");
+ assert_eq!(std::fs::read_to_string(&a).unwrap(), "1");
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn lint_draw_and_verify_answer_as_json() {
+ let l: Value = serde_json::from_str(&lint("qubit[1] q;\nfoo q[0];".into())).unwrap();
+ assert_eq!(l["line"], 2);
+ let d: Value = serde_json::from_str(&draw("qubit[2] q; h q[0]; cx q[0], q[1];".into(), 80)).unwrap();
+ assert!(d["text"].as_str().unwrap().contains('⊕'));
+ let v: Value = serde_json::from_str(&verify("{}".into())).unwrap();
+ assert!(v["error"].is_string());
+ }
+}
diff --git a/wai/wai-quantum-desktop/tauri.conf.json b/wai/wai-quantum-desktop/tauri.conf.json
new file mode 100644
index 000000000..e29577b81
--- /dev/null
+++ b/wai/wai-quantum-desktop/tauri.conf.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "Quantum Studio",
+ "version": "0.1.0",
+ "identifier": "science.transaction.quantum-studio",
+ "build": {
+ "frontendDist": "../quantum-studio/dist"
+ },
+ "app": {
+ "withGlobalTauri": true,
+ "windows": [
+ {
+ "label": "main",
+ "title": "Quantum Studio",
+ "width": 1440,
+ "height": 960,
+ "minWidth": 720,
+ "minHeight": 560
+ }
+ ],
+ "security": {
+ "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://wai.transaction.science; worker-src 'self' blob:"
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"],
+ "category": "DeveloperTool",
+ "shortDescription": "OpenQASM programs run on this computer or the quantum job service, every result named and signed",
+ "copyright": "Transaction Science"
+ }
+}
diff --git a/wai/wai-quantum-qpu/Cargo.lock b/wai/wai-quantum-qpu/Cargo.lock
index 0b9cf7c2a..ccc628b7d 100644
--- a/wai/wai-quantum-qpu/Cargo.lock
+++ b/wai/wai-quantum-qpu/Cargo.lock
@@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock
index f9a3beec5..71dcfd7fa 100644
--- a/wai/wai-quantum-serve/Cargo.lock
+++ b/wai/wai-quantum-serve/Cargo.lock
@@ -599,7 +599,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
diff --git a/wai/wai-quantum-service/Cargo.lock b/wai/wai-quantum-service/Cargo.lock
index 50058c247..2d240a0a6 100644
--- a/wai/wai-quantum-service/Cargo.lock
+++ b/wai/wai-quantum-service/Cargo.lock
@@ -857,13 +857,14 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
"objc2",
"objc2-foundation",
"objc2-metal",
+ "serde",
"serde_json",
]
@@ -872,7 +873,6 @@ name = "wai-quantum-service"
version = "0.1.0"
dependencies = [
"axum",
- "blake3",
"getrandom 0.4.3",
"libc",
"serde",
diff --git a/wai/wai-quantum-service/Cargo.toml b/wai/wai-quantum-service/Cargo.toml
index 18b304ea2..d07deb7c2 100644
--- a/wai/wai-quantum-service/Cargo.toml
+++ b/wai/wai-quantum-service/Cargo.toml
@@ -15,9 +15,8 @@ serde = { version = "1", features = ["derive"] }
# float_roundtrip: a number a client sends back reads as the double it was
# (the default reader rounds twice), so its receipt still verifies.
serde_json = { version = "1", features = ["float_roundtrip"] }
-blake3 = "1"
getrandom = "0.4"
-wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] }
+wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_sv_metal"] }
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs).
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/wai/wai-quantum-service/src/discovery.rs b/wai/wai-quantum-service/src/discovery.rs
index bb40633c3..4639ae4db 100644
--- a/wai/wai-quantum-service/src/discovery.rs
+++ b/wai/wai-quantum-service/src/discovery.rs
@@ -199,6 +199,32 @@ pub fn index(base: &str, console: &str) -> Value {
#[cfg(test)]
mod tests {
use super::*;
+ use wai_quantum::engine::{catalog::plan, run::run};
+
+ fn go(job: Value) -> Value {
+ let p = plan(&job, 8 << 30).unwrap();
+ run(&job, &p).unwrap()
+ }
+
+ /// The two worked examples that measure in the middle do what their
+ /// `shows` says.
+ #[test]
+ fn the_measuring_examples_show_what_they_say() {
+ let ex = examples();
+ let job = |name: &str| ex.as_array().unwrap().iter().find(|e| e["name"] == name).unwrap()["job"].clone();
+ let t = go(job("teleport"));
+ assert_eq!(t["method"], "statevector", "{t}");
+ assert!((t["expectations"][0]["value"].as_f64().unwrap() - 0.7f64.cos()).abs() < 1e-12, "{t}");
+ assert!((t["expectations"][1]["value"].as_f64().unwrap() - 0.7f64.sin()).abs() < 1e-12, "{t}");
+ assert_eq!(t["dynamic"]["branches_weighed"], 4, "{t}");
+ let b = go(job("bit-flip-code"));
+ assert_eq!(b["method"], "stabilizer", "{b}");
+ assert_eq!(b["expectations"][0]["value"], 1.0, "{b}");
+ let counts = b["counts"].as_object().unwrap();
+ assert!(counts.keys().all(|k| k.ends_with("11")), "the syndrome names the middle qubit on every shot: {b}");
+ assert_eq!(counts.values().map(|v| v.as_u64().unwrap()).sum::(), 1000);
+ }
+
/// Every example plans, with the method its description promises.
#[test]
diff --git a/wai/wai-quantum-service/src/main.rs b/wai/wai-quantum-service/src/main.rs
index 42608b028..7012d4eab 100644
--- a/wai/wai-quantum-service/src/main.rs
+++ b/wai/wai-quantum-service/src/main.rs
@@ -13,16 +13,13 @@
//! wai-quantum-service run FILE one job from a JSON file, here, printed
//! ```
-mod catalog;
mod discovery;
-mod dynamic;
mod energy;
mod http;
mod jobs;
mod mcp;
-mod qasm;
+use wai_quantum::engine::{catalog, run};
mod receipt;
-mod run;
use std::io::Read;
use std::net::{IpAddr, SocketAddr};
diff --git a/wai/wai-quantum-service/src/receipt.rs b/wai/wai-quantum-service/src/receipt.rs
index c6a36518c..e782d4f1a 100644
--- a/wai/wai-quantum-service/src/receipt.rs
+++ b/wai/wai-quantum-service/src/receipt.rs
@@ -1,269 +1,18 @@
-//! Signed receipts: every finished job's result is sealed with the service's
-//! Ed25519 key, binding the request as submitted and the result as returned,
-//! so a client can show anyone what was run and what came back.
-//!
-//! What is signed is the JSON Canonicalization Scheme (RFC 8785) text of each:
-//! keys sorted, no whitespace, and every number written as ECMAScript writes
-//! the double it reads. A result therefore verifies after any JSON reader and
-//! writer has carried it: a browser's `JSON.parse` and `JSON.stringify` turn
-//! `1.0` into `1`, and other readers reorder keys.
+//! The service's signing key. The receipt form and its check are the
+//! engine's ([`wai_quantum::engine::receipt`]): every finished job's result is
+//! sealed with this key.
-use serde_json::{Value, json};
+pub use wai_quantum::engine::receipt::{inexact_number, verify};
+use serde_json::Value;
use std::path::{Path, PathBuf};
-use wai_quantum::ed25519_dalek::{Signer as _, SigningKey, Verifier as _, VerifyingKey};
-
-/// The first receipts signed serde_json's own text, which only a reader that
-/// keeps each number's spelling and each object's key order can reproduce.
-/// They still verify; new ones are canonical.
-const DOMAIN_V1: &[u8] = b"wai:quantum-service-receipt\x01";
-const DOMAIN: &[u8] = b"wai:quantum-service-receipt\x02";
-/// The receipt's name for its canonical form.
-const CANON: &str = "rfc8785";
+use wai_quantum::ed25519_dalek::SigningKey;
+use wai_quantum::engine::receipt;
/// The service's signing key.
pub struct Signer {
key: SigningKey,
}
-fn hex(b: &[u8]) -> String {
- b.iter().map(|x| format!("{x:02x}")).collect()
-}
-
-fn unhex(s: &str) -> Option> {
- s.len().is_multiple_of(2).then_some(())?;
- (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
-}
-
-/// A length-prefixed byte string, so that no field reads as part of another.
-fn put(o: &mut Vec, bytes: &[u8]) {
- o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
- o.extend_from_slice(bytes);
-}
-
-/// The digest a receipt signs: BLAKE3(domain ‖ put(request) ‖ put(result)),
-/// each in its RFC 8785 canonical text.
-pub fn digest(request: &Value, result: &Value) -> [u8; 32] {
- let mut o = DOMAIN.to_vec();
- put(&mut o, &canonical(request));
- put(&mut o, &canonical(result));
- *blake3::hash(&o).as_bytes()
-}
-
-/// The digest the first receipts signed (see [`DOMAIN_V1`]).
-fn digest_v1(request: &Value, result: &Value) -> [u8; 32] {
- let mut o = DOMAIN_V1.to_vec();
- put(&mut o, request.to_string().as_bytes());
- put(&mut o, result.to_string().as_bytes());
- *blake3::hash(&o).as_bytes()
-}
-
-/// The RFC 8785 canonical text of `v`.
-pub fn canonical(v: &Value) -> Vec {
- let mut o = Vec::new();
- write(v, &mut o);
- o
-}
-
-fn write(v: &Value, o: &mut Vec) {
- match v {
- Value::Null => o.extend_from_slice(b"null"),
- Value::Bool(b) => o.extend_from_slice(if *b { b"true" } else { b"false" }),
- // Every number as the double a JSON reader in ECMAScript holds.
- Value::Number(n) => o.extend_from_slice(es_number(n.as_f64().unwrap_or(f64::NAN)).as_bytes()),
- Value::String(s) => write_str(s, o),
- Value::Array(a) => {
- o.push(b'[');
- for (i, x) in a.iter().enumerate() {
- if i > 0 {
- o.push(b',');
- }
- write(x, o);
- }
- o.push(b']');
- }
- Value::Object(m) => {
- // Keys in the order of their UTF-16 code units.
- let mut keys: Vec<&String> = m.keys().collect();
- keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
- o.push(b'{');
- for (i, k) in keys.into_iter().enumerate() {
- if i > 0 {
- o.push(b',');
- }
- write_str(k, o);
- o.push(b':');
- write(&m[k], o);
- }
- o.push(b'}');
- }
- }
-}
-
-/// A string as RFC 8785 writes it: `"` and `\\` escaped, the five short
-/// control escapes, every other control character as `\u00xx`, and the rest
-/// as it is.
-fn write_str(s: &str, o: &mut Vec) {
- o.push(b'"');
- for c in s.chars() {
- match c {
- '"' => o.extend_from_slice(b"\\\""),
- '\\' => o.extend_from_slice(b"\\\\"),
- '\u{8}' => o.extend_from_slice(b"\\b"),
- '\t' => o.extend_from_slice(b"\\t"),
- '\n' => o.extend_from_slice(b"\\n"),
- '\u{c}' => o.extend_from_slice(b"\\f"),
- '\r' => o.extend_from_slice(b"\\r"),
- c if (c as u32) < 0x20 => o.extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes()),
- c => {
- let mut b = [0u8; 4];
- o.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
- }
- }
- }
- o.push(b'"');
-}
-
-/// A double as ECMAScript's Number::toString writes it (ECMA-262, 6.1.6.1.20):
-/// the shortest digits that read back as `x`, laid out by where the decimal
-/// point falls. JSON holds no infinity or NaN; they write as `null`.
-pub fn es_number(x: f64) -> String {
- if !x.is_finite() {
- return "null".into();
- }
- if x == 0.0 {
- return "0".into();
- }
- let (s, n) = shortest(x.abs());
- let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
- let k = s.len() as i32;
- let mut out = String::new();
- if x < 0.0 {
- out.push('-');
- }
- if k <= n && n <= 21 {
- out.push_str(&digits);
- out.extend(std::iter::repeat_n('0', (n - k) as usize));
- } else if 0 < n && n <= 21 {
- out.push_str(&digits[..n as usize]);
- out.push('.');
- out.push_str(&digits[n as usize..]);
- } else if -6 < n && n <= 0 {
- out.push_str("0.");
- out.extend(std::iter::repeat_n('0', (-n) as usize));
- out.push_str(&digits);
- } else {
- out.push_str(&digits[..1]);
- if k > 1 {
- out.push('.');
- out.push_str(&digits[1..]);
- }
- out.push('e');
- out.push(if n - 1 < 0 { '-' } else { '+' });
- out.push_str(&(n - 1).abs().to_string());
- }
- out
-}
-
-/// The digits s and the exponent n, x = 0.s × 10^n, that ECMAScript chooses:
-/// the fewest that read back as `x` (> 0), and of those the nearest to `x`,
-/// a tie going to the even.
-fn shortest(x: f64) -> (Vec, i32) {
- let e = format!("{x:e}");
- let (mant, exp) = e.split_once('e').unwrap_or((&e, "0"));
- let mut s: Vec = mant.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
- while s.len() > 1 && s.last() == Some(&0) {
- s.pop();
- }
- let n = exp.parse::().unwrap_or(0) + 1;
- // Rust's digits are the fewest, but a tie between two equally near it
- // rounds up. Only when another of the same length also reads back as x
- // (near 17 digits) is there a choice: then take x's exact expansion (a
- // double's has at most 767 significant digits) rounded to that length.
- if ![false, true].into_iter().any(|up| step(&s, up).is_some_and(|t| reads_as(&t, n, x))) {
- return (s, n);
- }
- let k = s.len();
- let exact = format!("{x:.800e}");
- let (em, ee) = exact.split_once('e').unwrap_or((&exact, "0"));
- let all: Vec = em.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
- let mut cn = ee.parse::().unwrap_or(0) + 1;
- let mut c = all[..k].to_vec();
- let rest = &all[k..];
- let up = match rest.first().copied().unwrap_or(0) {
- d if d > 5 => true,
- 5 => rest[1..].iter().any(|&d| d != 0) || c[k - 1] % 2 == 1,
- _ => false,
- };
- if up {
- match step(&c, true) {
- Some(t) => c = t,
- None => {
- // 99…9 rounds to 10…0.
- c = std::iter::once(1).chain(std::iter::repeat_n(0, k - 1)).collect();
- cn += 1;
- }
- }
- }
- if reads_as(&c, cn, x) { (c, cn) } else { (s, n) }
-}
-
-/// The k-digit decimal one unit above or below `s` in its last digit, if it
-/// still has k digits.
-fn step(s: &[u8], up: bool) -> Option> {
- let mut t = s.to_vec();
- for i in (0..t.len()).rev() {
- match (up, t[i]) {
- (true, 9) => t[i] = 0,
- (false, 0) => t[i] = 9,
- (true, d) | (false, d) => {
- t[i] = if up { d + 1 } else { d - 1 };
- return (t[0] != 0).then_some(t);
- }
- }
- }
- None
-}
-
-/// Whether 0.s × 10^n reads as `x` (std's reader is correctly rounded).
-fn reads_as(s: &[u8], n: i32, x: f64) -> bool {
- let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
- format!("0.{digits}e{n}").parse::().is_ok_and(|y| y == x)
-}
-
-/// The first number in `v` that a double cannot hold exactly (an integer past
-/// ±2^53), by its path. Such a number would be read differently by readers that
-/// hold every number as a double, so the receipt could not bind it.
-pub fn inexact_number(v: &Value) -> Option {
- const SAFE: u64 = (1 << 53) - 1;
- fn walk(v: &Value, path: &mut String) -> bool {
- match v {
- Value::Number(n) => n.as_u64().is_some_and(|u| u > SAFE) || n.as_i64().is_some_and(|i| i.unsigned_abs() > SAFE),
- Value::Array(a) => a.iter().enumerate().any(|(i, x)| {
- let at = path.len();
- path.push_str(&format!("[{i}]"));
- let hit = walk(x, path);
- if !hit {
- path.truncate(at);
- }
- hit
- }),
- Value::Object(m) => m.iter().any(|(k, x)| {
- let at = path.len();
- path.push('.');
- path.push_str(k);
- let hit = walk(x, path);
- if !hit {
- path.truncate(at);
- }
- hit
- }),
- _ => false,
- }
- }
- let mut path = String::new();
- walk(v, &mut path).then(|| path.trim_start_matches('.').to_string())
-}
-
impl Signer {
/// The key at `path`, created (32 random bytes, readable by the owner
/// only) when there is none.
@@ -281,40 +30,17 @@ impl Signer {
Ok(Signer { key: SigningKey::from_bytes(&seed) })
}
- /// A key for tests.
- #[cfg(test)]
- pub fn from_seed(seed: [u8; 32]) -> Signer {
- Signer { key: SigningKey::from_bytes(&seed) }
- }
-
/// The public key, in hex.
pub fn public_key(&self) -> String {
- hex(self.key.verifying_key().as_bytes())
+ self.key.verifying_key().as_bytes().iter().map(|b| format!("{b:02x}")).collect()
}
/// The receipt for `result` of `request`.
pub fn seal(&self, request: &Value, result: &Value) -> Value {
- let d = digest(request, result);
- let sig = self.key.sign(&d);
- json!({ "alg": "ed25519", "canon": CANON, "key": self.public_key(), "digest": hex(&d), "signature": hex(&sig.to_bytes()) })
+ receipt::seal(&self.key, request, result)
}
}
-/// Whether `receipt` is a valid seal of `result` of `request` under the key
-/// it names.
-pub fn verify(request: &Value, result: &Value, receipt: &Value) -> bool {
- let field = |k: &str| receipt.get(k).and_then(Value::as_str).and_then(unhex);
- let (Some(key), Some(sig)) = (field("key"), field("signature")) else { return false };
- let (Ok(key), Ok(sig)) = (<[u8; 32]>::try_from(key.as_slice()), <[u8; 64]>::try_from(sig.as_slice())) else { return false };
- let Ok(vk) = VerifyingKey::from_bytes(&key) else { return false };
- let d = match receipt.get("canon").and_then(Value::as_str) {
- Some(CANON) => digest(request, result),
- None => digest_v1(request, result),
- Some(_) => return false,
- };
- field("digest").is_some_and(|g| g == d) && vk.verify(&d, &wai_quantum::ed25519_dalek::Signature::from_bytes(&sig)).is_ok()
-}
-
#[cfg(unix)]
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
@@ -341,100 +67,6 @@ pub fn default_path() -> PathBuf {
mod tests {
use super::*;
- #[test]
- fn a_receipt_verifies_and_any_change_breaks_it() {
- let s = Signer::from_seed([7; 32]);
- let req = json!({"qasm": "OPENQASM 3.0;", "shots": 10});
- let res = json!({"method": "statevector", "counts": {"0": 10}});
- let r = s.seal(&req, &res);
- assert!(verify(&req, &res, &r));
- assert!(!verify(&req, &json!({"method": "statevector", "counts": {"0": 9}}), &r));
- assert!(!verify(&json!({"qasm": "OPENQASM 3.0;", "shots": 11}), &res, &r));
- let mut forged = r.clone();
- forged["key"] = json!(Signer::from_seed([8; 32]).public_key());
- assert!(!verify(&req, &res, &forged));
- }
-
- #[test]
- fn numbers_are_written_as_ecmascript_writes_them() {
- // Bit patterns and ECMAScript's Number::toString of each, written by a
- // JavaScript engine: RFC 8785's appendix B, the edges of each layout,
- // and patterns spread over every exponent.
- let mut n = 0;
- for line in include_str!("../testdata/es-number-tostring.txt").lines() {
- let (bits, want) = line.split_once(' ').unwrap();
- let x = f64::from_bits(u64::from_str_radix(bits, 16).unwrap());
- assert_eq!(es_number(x), want, "{bits}");
- // And the text reads back as the same double, as it must for a
- // result to verify after a round trip (serde_json's
- // float_roundtrip: its default reader rounds twice).
- if x != 0.0 {
- assert_eq!(serde_json::from_str::(want).unwrap().to_bits(), x.to_bits(), "{bits} {want}");
- }
- n += 1;
- }
- assert!(n > 3000);
- }
-
- /// The same against a larger set, in the same format, written by a
- /// JavaScript engine: `WAI_ES_VECTORS=path cargo test -- --ignored`.
- #[test]
- #[ignore]
- fn numbers_match_a_javascript_engine_on_a_large_set() {
- let path = std::env::var("WAI_ES_VECTORS").expect("WAI_ES_VECTORS: a file of `hexbits text` lines");
- let text = std::fs::read_to_string(path).unwrap();
- let mut n = 0u64;
- for line in text.lines() {
- let (bits, want) = line.split_once(' ').unwrap();
- assert_eq!(es_number(f64::from_bits(u64::from_str_radix(bits, 16).unwrap())), want, "{bits}");
- n += 1;
- }
- eprintln!("{n} numbers agree");
- }
-
- #[test]
- fn the_canonical_text_sorts_keys_and_writes_numbers_and_strings_one_way() {
- let v = json!({ "b": 1.0, "a": [true, null, "x\n\u{1}\"\\é"], "\u{e000}": 0.000001, "\u{1f600}": -0.0, "c": 1e21 });
- // U+1F600 is a surrogate pair, so it sorts before U+E000.
- assert_eq!(String::from_utf8(canonical(&v)).unwrap(), "{\"a\":[true,null,\"x\\n\\u0001\\\"\\\\é\"],\"b\":1,\"c\":1e+21,\"\u{1f600}\":0,\"\u{e000}\":0.000001}");
- }
-
- #[test]
- fn a_receipt_verifies_after_a_round_trip_through_another_json_writer() {
- let s = Signer::from_seed([7; 32]);
- let req = json!({ "qasm": "qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
- let res = json!({ "method": "stabilizer", "expectations": [{ "observable": "Z0 Z1", "coeff": 1.0, "value": 1.0 }], "threshold": 0.00001, "elapsed_ms": 0 });
- let r = s.seal(&req, &res);
- // What a browser sends back: integral doubles without a fraction, keys
- // in another order, the threshold in plain decimals.
- let req2: Value = serde_json::from_str(r#"{"observables":["Z0 Z1"],"shots":1000,"qasm":"qubit[2] q; h q[0]; cx q[0], q[1];"}"#).unwrap();
- let res2: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":1,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
- assert!(verify(&req2, &res2, &r));
- let res3: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":0.999,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
- assert!(!verify(&req2, &res3, &r));
- }
-
- #[test]
- fn the_first_receipts_still_verify_and_an_unknown_form_does_not() {
- let s = Signer::from_seed([7; 32]);
- let req = json!({ "qasm": "OPENQASM 3.0;", "shots": 10 });
- let res = json!({ "method": "statevector", "counts": { "0": 10 } });
- let d = digest_v1(&req, &res);
- let old = json!({ "alg": "ed25519", "key": s.public_key(), "digest": hex(&d), "signature": hex(&s.key.sign(&d).to_bytes()) });
- assert!(verify(&req, &res, &old));
- let mut other = s.seal(&req, &res);
- other["canon"] = json!("something-else");
- assert!(!verify(&req, &res, &other));
- }
-
- #[test]
- fn integers_a_double_cannot_hold_are_found_by_path() {
- assert_eq!(inexact_number(&json!({ "seed": 9007199254740991u64, "shots": 10 })), None);
- assert_eq!(inexact_number(&json!({ "seed": 9007199254740992u64 })).as_deref(), Some("seed"));
- assert_eq!(inexact_number(&json!({ "options": { "x": [1, -9007199254740993i64] } })).as_deref(), Some("options.x[1]"));
- assert_eq!(inexact_number(&json!({ "noise": { "readout": 1e300 } })), None);
- }
-
#[test]
fn a_key_is_created_once_private_and_then_reused() {
let dir = std::env::temp_dir().join(format!("wai-qsvc-key-{}", std::process::id()));
diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock
index 47a7b5bcb..81954092f 100644
--- a/wai/wai-quantum/Cargo.lock
+++ b/wai/wai-quantum/Cargo.lock
@@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"block2",
@@ -1090,6 +1090,7 @@ dependencies = [
"objc2-foundation",
"objc2-metal",
"opencl3",
+ "serde",
"serde_json",
"wgpu",
]
diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml
index a86503a1b..0652599d6 100644
--- a/wai/wai-quantum/Cargo.toml
+++ b/wai/wai-quantum/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
@@ -26,6 +26,8 @@ all-features = true
blake3 = "1"
# Canonical receipt JSON (sorted keys, byte-stable round-trip).
serde_json = { version = "1", features = ["preserve_order"] }
+# The engine's request and plan types (feature quantum_engine).
+serde = { version = "1", features = ["derive"], optional = true }
# Receipt signatures. Optional — only the sealing features need it.
ed25519-dalek = { version = "2", optional = true }
@@ -80,7 +82,15 @@ quantum_tableau = []
# Exact amplitudes, probabilities and samples of a Clifford circuit measured in a
# product basis, at a cost set by its cut-rank (src/quantum_cutrank.rs).
quantum_cutrank = ["quantum_tableau", "quantum_sv"]
+# OpenQASM 3 (and the OpenQASM 2 forms in common use) read to one program and
+# lowered to each method's input (src/qasm.rs).
+quantum_qasm = ["quantum", "quantum_sv", "quantum_spd", "quantum_tableau"]
quantum_mpsim = ["quantum_sv", "quantum_mps"]
+# The job service's engine: a request planned onto a method, run, and answered
+# as the service's JSON, for every host that runs jobs (src/engine/). Numbers
+# are read correctly rounded, so a result a client sends back reads as the
+# double it was.
+quantum_engine = ["quantum_qasm", "quantum_tn", "quantum_cutrank", "quantum_mpsim", "quantum_noisy", "dep:serde", "serde_json/float_roundtrip"]
quantum_pauli = ["quantum"]
# Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up
# to 1024 qubits, reproducible bit for bit, with a rigorous truncation bound.
@@ -206,7 +216,7 @@ quantum_phasor_meter = ["_signing"]
# Everything.
full = [
- "quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
+ "quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_engine", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_fabric",
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",
diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md
index dfba3c2ed..1d7a1da4f 100644
--- a/wai/wai-quantum/README.md
+++ b/wai/wai-quantum/README.md
@@ -67,6 +67,8 @@ never creates information.
| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` |
| `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
+| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps; loops, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason |
+| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
| `quantum_mps` | tensor-network (matrix-product-state) backend |
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |
diff --git a/wai/wai-quantum-service/src/catalog.rs b/wai/wai-quantum/src/engine/catalog.rs
similarity index 88%
rename from wai/wai-quantum-service/src/catalog.rs
rename to wai/wai-quantum/src/engine/catalog.rs
index 5a0a10e2e..8a4c10152 100644
--- a/wai/wai-quantum-service/src/catalog.rs
+++ b/wai/wai-quantum/src/engine/catalog.rs
@@ -36,14 +36,14 @@ pub const MAX_AMPLITUDES: usize = 64;
pub const MAX_OUTCOMES: usize = 65_536;
pub const EXACT_MAX_QUBITS: u32 = 26;
-pub const STABILIZER_MAX_QUBITS: u32 = wai_quantum::quantum_tableau::MAX_QUBITS;
+pub const STABILIZER_MAX_QUBITS: u32 = crate::quantum_tableau::MAX_QUBITS;
pub const PAULI_MAX_QUBITS: u32 = 1024;
-pub const DENSITY_MAX_QUBITS: u32 = wai_quantum::quantum_noisy::MAX_EVOLVE_QUBITS;
+pub const DENSITY_MAX_QUBITS: u32 = crate::quantum_noisy::MAX_EVOLVE_QUBITS;
/// The most a density-matrix job may do: its gates times the `4ⁿ` entries
/// of `ρ`, a few minutes of passes.
pub const DENSITY_MAX_WORK: u64 = 1_000_000_000_000;
pub const TRAJECTORY_MAX_QUBITS: u32 = 32;
-pub const MPS_MAX_QUBITS: u32 = wai_quantum::quantum_mpsim::MAX_QUBITS;
+pub const MPS_MAX_QUBITS: u32 = crate::quantum_mpsim::MAX_QUBITS;
/// The bond dimension an `mps` job gets unless it asks, and the most it may.
pub const MPS_DEFAULT_BOND: usize = 64;
pub const MPS_MAX_BOND: usize = 256;
@@ -131,11 +131,25 @@ pub fn density_max_qubits(job_bytes: u64) -> u32 {
}
/// The most qubits the GPU's largest buffer holds, asked of the device once.
+/// A build without the GPU state vector (feature `quantum_sv_metal`) has none.
pub fn gpu_max_qubits() -> Result {
- static MAX: std::sync::OnceLock> = std::sync::OnceLock::new();
- MAX.get_or_init(|| wai_quantum::quantum_sv_metal::GpuState::max_qubits().map_err(|e| e.to_string())).clone()
+ #[cfg(feature = "quantum_sv_metal")]
+ {
+ static MAX: std::sync::OnceLock> = std::sync::OnceLock::new();
+ MAX.get_or_init(|| crate::quantum_sv_metal::GpuState::max_qubits().map_err(|e| e.to_string())).clone()
+ }
+ #[cfg(not(feature = "quantum_sv_metal"))]
+ {
+ Err("this build has no GPU state vector".into())
+ }
}
+/// The widest gate the GPU state vector applies.
+#[cfg(feature = "quantum_sv_metal")]
+const SINGLE_MAX_WIDTH: usize = crate::quantum_sv_metal::MAX_WIDTH;
+#[cfg(not(feature = "quantum_sv_metal"))]
+const SINGLE_MAX_WIDTH: usize = 4;
+
/// The most qubits the single-precision state vector holds in `job_bytes`.
pub fn single_max_qubits(job_bytes: u64) -> u32 {
(3..=40u32).take_while(|&n| (8u64 << n) + 256 * MIB <= job_bytes).last().unwrap_or(0)
@@ -143,7 +157,7 @@ pub fn single_max_qubits(job_bytes: u64) -> u32 {
/// The most qubits the dense state vector holds in `job_bytes`.
pub fn statevector_max_qubits(job_bytes: u64) -> u32 {
- (1..=wai_quantum::quantum_sv::MAX_QUBITS).rev().find(|&n| statevector_bytes(n) <= job_bytes).unwrap_or(0)
+ (1..=crate::quantum_sv::MAX_QUBITS).rev().find(|&n| statevector_bytes(n) <= job_bytes).unwrap_or(0)
}
/// An observable as asked: its text, its coefficient, and its Pauli factors
@@ -158,7 +172,7 @@ pub struct Request {
pub seed: u64,
pub observables: Vec,
pub amplitudes: Vec,
- pub noise: Option,
+ pub noise: Option,
pub options: Value,
}
@@ -192,12 +206,20 @@ fn observable(v: &Value, n: u32) -> Result {
Ok((text, coeff, string))
}
-fn noise(v: &Value) -> Result {
+/// The noise channels a job may name.
+pub const NOISE_CHANNELS: [&str; 5] = ["depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"];
+
+fn noise(v: &Value) -> Result {
+ // A channel the model does not know is refused: read as no noise, a
+ // misspelt one would run the circuit noiseless.
+ if let Some(k) = v.as_object().and_then(|o| o.keys().find(|k| !NOISE_CHANNELS.contains(&k.as_str()))) {
+ return Err(format!("noise {k}: no such channel; the channels are {}", NOISE_CHANNELS.join(", ")));
+ }
let f = |k: &str| -> Result {
let x = v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
if (0.0..=1.0).contains(&x) { Ok(x) } else { Err(format!("noise {k} is a probability, 0 to 1")) }
};
- Ok(wai_quantum::quantum_noisy::NoiseModel {
+ Ok(crate::quantum_noisy::NoiseModel {
depol1: f("depolarizing_1q")?,
depol2: f("depolarizing_2q")?,
damping: f("amplitude_damping")?,
@@ -284,9 +306,9 @@ pub fn mps_work(r: &Request, chi: u64) -> u64 {
/// memory and work; the run (`thorough`) makes several longer ones and keeps
/// the cheapest order, which is never costlier than the planner's. Both are
/// deterministic.
-pub fn product_measured(r: &Request, thorough: bool) -> Result {
- use wai_quantum::quantum_cutrank::ProductMeasured;
- use wai_quantum::quantum_tableau::Tableau;
+pub fn product_measured(r: &Request, thorough: bool) -> Result {
+ use crate::quantum_cutrank::ProductMeasured;
+ use crate::quantum_tableau::Tableau;
let n = r.program.n_qubits;
if n > CUTRANK_MAX_QUBITS {
return Err(format!("cutrank holds at most {CUTRANK_MAX_QUBITS} qubits"));
@@ -312,15 +334,21 @@ pub fn product_measured(r: &Request, thorough: bool) -> Result> = std::thread::scope(|sc| {
- let handles: Vec<_> = (1..=CUTRANK_RUN_SEEDS)
- .map(|seed| {
- let pm = ±
- sc.spawn(move || pm.anneal_order(m, seed))
- })
- .collect();
- handles.into_iter().map(|h| h.join().expect("a search")).collect()
- });
+ // One thread per seed, where there are threads; a browser tab has one, so
+ // there the seeds run in turn and find the same orders.
+ let found: Vec> = if cfg!(target_arch = "wasm32") {
+ (1..=CUTRANK_RUN_SEEDS).map(|seed| pm.anneal_order(m, seed)).collect()
+ } else {
+ std::thread::scope(|sc| {
+ let handles: Vec<_> = (1..=CUTRANK_RUN_SEEDS)
+ .map(|seed| {
+ let pm = ±
+ sc.spawn(move || pm.anneal_order(m, seed))
+ })
+ .collect();
+ handles.into_iter().map(|h| h.join().expect("a search")).collect()
+ })
+ };
let mut best = quick;
for o in &found {
let cand = pm.clone().with_order(o).map_err(|e| e.to_string())?;
@@ -344,9 +372,9 @@ pub const CUTRANK_PHASE_OPS: u64 = 50_000_000_000;
/// identity in place of the closing gates. The lowering of the gates to
/// Clifford ones drops a phase `e^{iΘ}` ([`qasm::Program::product_measured`]).
/// The factor is `e^{iΘ} a / b`, of modulus 1.
-pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeasured, cut: usize, precision: wai_quantum::quantum_cutrank::Precision) -> Result, String> {
- use wai_quantum::gates::complex;
- use wai_quantum::quantum_tableau::{Clifford, PhasedTableau};
+pub fn cutrank_phase(r: &Request, pm: &crate::quantum_cutrank::ProductMeasured, cut: usize, precision: crate::quantum_cutrank::Precision) -> Result , String> {
+ use crate::gates::complex;
+ use crate::quantum_tableau::{Clifford, PhasedTableau};
let n = r.program.n_qubits as u64;
let (bulk, _, theta) = r.program.product_measured()?;
let hadamards = bulk.iter().map(|g| match g {
@@ -367,7 +395,7 @@ pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeas
let ratio = complex((are * b.re + aim * b.im) / bb, (aim * b.re - are * b.im) / bb);
let k = complex(theta.re * ratio.re - theta.im * ratio.im, theta.re * ratio.im + theta.im * ratio.re);
let off = (k.re.hypot(k.im) - 1.0).abs();
- let tolerance = if precision == wai_quantum::quantum_cutrank::Precision::Double { 1e-9 } else { 1e-5 };
+ let tolerance = if precision == crate::quantum_cutrank::Precision::Double { 1e-9 } else { 1e-5 };
if off > tolerance {
return Err(format!("internal: the tracked and summed amplitudes differ in modulus by {off:e}"));
}
@@ -376,7 +404,7 @@ pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeas
/// The memory a `cutrank` job takes: its largest table with a margin, the
/// graph form, and room to run.
-pub fn cutrank_bytes(pm: &wai_quantum::quantum_cutrank::ProductMeasured, precision: wai_quantum::quantum_cutrank::Precision) -> u64 {
+pub fn cutrank_bytes(pm: &crate::quantum_cutrank::ProductMeasured, precision: crate::quantum_cutrank::Precision) -> u64 {
let n = pm.qubits() as u64;
let table = pm.table_bytes(precision);
table + table / 16 + n * n.div_ceil(64) * 8 * 4 + 256 * MIB
@@ -384,13 +412,13 @@ pub fn cutrank_bytes(pm: &wai_quantum::quantum_cutrank::ProductMeasured, precisi
/// Whether `method` can take the job, and the plan if so; otherwise why not.
/// A program that measures in the middle, resets after a qubit's first gate,
-/// or acts on measured bits ([`crate::dynamic`]): the dense state vector and
+/// or acts on measured bits ([`crate::engine::dynamic`]): the dense state vector and
/// the stabilizer tableau run it, splitting its shots at each measurement and
/// keeping a copy of the state for each branch that runs on.
-fn try_dynamic(method: &str, r: &Request, clifford: &Result, String>, cap: u64, steps: &[crate::qasm::Instr]) -> Result {
+fn try_dynamic(method: &str, r: &Request, clifford: &Result, String>, cap: u64, steps: &[crate::qasm::Instr]) -> Result {
let n = r.program.n_qubits;
let gates = r.program.ops.len() as u64;
- let measured = crate::dynamic::measurements(steps);
+ let measured = crate::engine::dynamic::measurements(steps);
if r.noise.is_some() {
return Err("noise in a program that measures in the middle, resets or acts on measured bits is not simulated yet".into());
}
@@ -400,12 +428,16 @@ fn try_dynamic(method: &str, r: &Request, clifford: &Result {
- let one = statevector_bytes(n);
- if n > wai_quantum::quantum_sv::MAX_QUBITS || 2 * one > cap {
- return Err(format!("statevector runs a program that measures in the middle on at most {} qubits: a branch holds a second state", statevector_max_qubits(cap / 2)));
+ // At least two states (a branch's copy beside the state it left),
+ // at most sixteen, and the job's margin once, as for trajectories.
+ let fits = |m: u32| 2 * amplitude_bytes(m) + 256 * MIB <= cap;
+ if n > crate::quantum_sv::MAX_QUBITS || !fits(n) {
+ let most = (1..=crate::quantum_sv::MAX_QUBITS).rev().find(|&m| fits(m)).unwrap_or(0);
+ return Err(format!("statevector runs a program that measures in the middle on at most {most} qubits: a branch holds a second state"));
}
- let states = (cap / one).clamp(2, 16);
- Ok(plan(states * one + r.shots * 24, (gates << n).saturating_mul(r.shots.clamp(1, 64)), format!("dense double-precision state vector of {n} qubits, its shots split at each of {measured} mid-circuit measurements and resets")))
+ let one = amplitude_bytes(n);
+ let states = (cap.saturating_sub(256 * MIB) / one).clamp(2, 16);
+ Ok(plan(states * one + 256 * MIB + r.shots * 24, (gates << n).saturating_mul(r.shots.clamp(1, 64)), format!("dense double-precision state vector of {n} qubits, its shots split at each of {measured} mid-circuit measurements and resets")))
}
"stabilizer" => {
if let Err(e) = clifford {
@@ -430,7 +462,7 @@ fn try_dynamic(method: &str, r: &Request, clifford: &Result, clifford: &Result, String>, cap: u64) -> Result {
+fn try_method(method: &str, r: &Request, dyadic: &Result, clifford: &Result, String>, cap: u64) -> Result {
let n = r.program.n_qubits;
let gates = r.program.ops.len() as u64;
let plan = |memory_bytes: u64, work: u64, why: String| Plan { method: method.into(), why, memory_bytes, work, qubits: n };
@@ -456,7 +488,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result {
let bytes = statevector_bytes(n);
- if bytes > cap || n > wai_quantum::quantum_sv::MAX_QUBITS {
+ if bytes > cap || n > crate::quantum_sv::MAX_QUBITS {
return Err(format!("statevector holds at most {} qubits in a job's memory", statevector_max_qubits(cap)));
}
Ok(plan(bytes + r.shots * 24, gates << n, format!("dense double-precision state vector of {n} qubits")))
@@ -467,7 +499,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result max || bytes > cap {
return Err(format!("statevector_single holds at most {} qubits on this GPU in a job's memory", max.min(single_max_qubits(cap))));
}
- if r.program.ops.iter().any(|o| o.qubits.len() > wai_quantum::quantum_sv_metal::MAX_WIDTH) {
+ if r.program.ops.iter().any(|o| o.qubits.len() > SINGLE_MAX_WIDTH) {
return Err("statevector_single takes gates on at most four qubits".into());
}
Ok(plan(bytes + r.shots * 24, gates << n, format!("dense single-precision state vector of {n} qubits on the GPU")))
@@ -535,7 +567,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result {
- use wai_quantum::quantum_cutrank::Precision;
+ use crate::quantum_cutrank::Precision;
if wants_expectations {
return Err("cutrank answers counts and amplitudes".into());
}
@@ -706,6 +738,18 @@ mod tests {
assert!(plan(&job(rot, json!({ "method": "nonsense" })), CAP).unwrap_err().contains("unknown method"));
}
+ /// A noise channel the model does not know is refused, never read as no
+ /// noise.
+ #[test]
+ fn an_unknown_noise_channel_is_refused() {
+ let bell = "qubit[2] q; h q[0]; cx q[0], q[1];";
+ let e = plan(&job(bell, json!({ "noise": { "depolarizing": 0.1 } })), CAP).unwrap_err();
+ assert!(e.contains("noise depolarizing: no such channel") && e.contains("depolarizing_1q"), "{e}");
+ for k in NOISE_CHANNELS {
+ assert!(plan(&job(bell, json!({ "noise": { k: 0.01 } })), CAP).is_ok(), "{k}");
+ }
+ }
+
/// A program that measures in the middle goes to the tableau when it is
/// Clifford and to the state vector otherwise; every other method, noise
/// and amplitudes are refused with what would run it.
@@ -718,7 +762,7 @@ mod tests {
assert!(p.why.contains("split at each of 2 mid-circuit measurements"), "{}", p.why);
let p = plan(&job(rotated, json!({ "observables": ["Z1"] })), CAP).unwrap();
assert_eq!(p.method, "statevector");
- assert!(p.memory_bytes >= 2 * statevector_bytes(2), "room for a branch's copy");
+ assert!(p.memory_bytes >= 2 * amplitude_bytes(2) + 256 * MIB, "room for a branch's copy");
for (extra, needle) in [
(json!({ "method": "mps" }), "statevector and stabilizer do"),
(json!({ "method": "exact" }), "statevector and stabilizer do"),
diff --git a/wai/wai-quantum/src/engine/draw.rs b/wai/wai-quantum/src/engine/draw.rs
new file mode 100644
index 000000000..0e46061fd
--- /dev/null
+++ b/wai/wai-quantum/src/engine/draw.rs
@@ -0,0 +1,321 @@
+//! A program's circuit as text: one wire per qubit, gates packed into the
+//! earliest column their qubits are free, a control as `●`, a CX target as
+//! `⊕`, a swap as `×`, a measurement into bit `k` as `Mk`, a reset as `|0⟩`.
+//! A gate under a condition carries `?n`, and the legend under the drawing
+//! says what condition `n` is. Wide circuits wrap at `width` columns.
+
+use std::f64::consts::PI;
+use crate::qasm::{Cond, Instr, Kind, Op, Program};
+
+/// One thing drawn in a column: its label on each qubit it touches, and
+/// whether a vertical line joins them.
+struct Item {
+ cells: Vec<(u32, String)>,
+ joined: bool,
+}
+
+/// `x` as a multiple of π where it is one with a small denominator, else in
+/// decimals.
+pub fn angle(x: f64) -> String {
+ let r = x / PI;
+ for d in [1i64, 2, 3, 4, 6, 8, 12, 16] {
+ let n = r * d as f64;
+ if (n - n.round()).abs() < 1e-9 {
+ let n = n.round() as i64;
+ let g = gcd(n.abs(), d);
+ let (n, d) = (n / g, d / g);
+ return match (n, d) {
+ (0, _) => "0".into(),
+ (1, 1) => "π".into(),
+ (-1, 1) => "-π".into(),
+ (n, 1) => format!("{n}π"),
+ (1, d) => format!("π/{d}"),
+ (-1, d) => format!("-π/{d}"),
+ (n, d) => format!("{n}π/{d}"),
+ };
+ }
+ }
+ let t = format!("{x:.4}");
+ t.trim_end_matches('0').trim_end_matches('.').to_string()
+}
+
+fn gcd(a: i64, b: i64) -> i64 {
+ if b == 0 { a.max(1) } else { gcd(b, a % b) }
+}
+
+fn params(p: &[f64]) -> String {
+ p.iter().map(|&x| angle(x)).collect::>().join(",")
+}
+
+/// The cells of one gate.
+fn gate(op: &Op) -> Item {
+ use Kind::*;
+ let q = &op.qubits;
+ let p = params(&op.params);
+ let one = |l: &str| Item { cells: vec![(q[0], l.to_string())], joined: false };
+ let ctl = |rest: Vec<(u32, String)>| {
+ let mut cells = vec![(q[0], "●".to_string())];
+ cells.extend(rest);
+ Item { cells, joined: true }
+ };
+ match op.kind {
+ Id => one("I"),
+ X => one("X"),
+ Y => one("Y"),
+ Z => one("Z"),
+ H => one("H"),
+ S => one("S"),
+ Sdg => one("S†"),
+ T => one("T"),
+ Tdg => one("T†"),
+ Sx => one("√X"),
+ Sxdg => one("√X†"),
+ Rx => one(&format!("Rx({p})")),
+ Ry => one(&format!("Ry({p})")),
+ Rz => one(&format!("Rz({p})")),
+ P => one(&format!("P({p})")),
+ U => one(&format!("U({p})")),
+ Cx => ctl(vec![(q[1], "⊕".into())]),
+ Cy => ctl(vec![(q[1], "Y".into())]),
+ Cz => ctl(vec![(q[1], "●".into())]),
+ Ch => ctl(vec![(q[1], "H".into())]),
+ Cp => ctl(vec![(q[1], format!("P({p})"))]),
+ Crx => ctl(vec![(q[1], format!("Rx({p})"))]),
+ Cry => ctl(vec![(q[1], format!("Ry({p})"))]),
+ Crz => ctl(vec![(q[1], format!("Rz({p})"))]),
+ Cu => ctl(vec![(q[1], format!("U({p})"))]),
+ Swap => Item { cells: vec![(q[0], "×".into()), (q[1], "×".into())], joined: true },
+ Iswap => Item { cells: vec![(q[0], "iSWAP".into()), (q[1], "iSWAP".into())], joined: true },
+ Rxx => Item { cells: vec![(q[0], format!("Rxx({p})")), (q[1], format!("Rxx({p})"))], joined: true },
+ Ryy => Item { cells: vec![(q[0], format!("Ryy({p})")), (q[1], format!("Ryy({p})"))], joined: true },
+ Rzz => Item { cells: vec![(q[0], format!("Rzz({p})")), (q[1], format!("Rzz({p})"))], joined: true },
+ Ccx => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "⊕".into())], joined: true },
+ Ccz => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "●".into())], joined: true },
+ Cswap => Item { cells: vec![(q[0], "●".into()), (q[1], "×".into()), (q[2], "×".into())], joined: true },
+ }
+}
+
+/// A condition as the legend writes it: the bits, highest first, read as an
+/// unsigned integer.
+fn condition(c: &Cond) -> String {
+ let op = if c.equal { "=" } else { "≠" };
+ match c.bits.as_slice() {
+ [b] => format!("if bit {b} {op} {}", c.value),
+ bits => {
+ let names: Vec = bits.iter().rev().map(|b| b.to_string()).collect();
+ format!("if bits ({}) {op} {}", names.join(" "), c.value)
+ }
+ }
+}
+
+/// The items to draw, in order, and the legend of conditions.
+fn items(p: &Program) -> (Vec- , Vec
) {
+ let mut out = Vec::new();
+ let mut legend = Vec::new();
+ let Some(steps) = &p.dynamic else {
+ out.extend(p.ops.iter().map(gate));
+ out.extend(p.measured.iter().map(|&(q, b)| Item { cells: vec![(q, format!("M{b}"))], joined: false }));
+ return (out, legend);
+ };
+ // Each step's condition: a JumpUnless guards the steps up to its target;
+ // a Jump at the end of that region skips an else branch, up to its own.
+ let mut guard: Vec> = vec![None; steps.len()];
+ for (i, s) in steps.iter().enumerate() {
+ if let Instr::JumpUnless(c, to) = s {
+ legend.push(condition(c));
+ let n = legend.len();
+ let then_end = *to;
+ let else_end = match steps.get(then_end.wrapping_sub(1)) {
+ Some(Instr::Jump(e)) if then_end > i + 1 => Some(*e),
+ _ => None,
+ };
+ for g in guard.iter_mut().take(then_end.min(steps.len())).skip(i + 1) {
+ g.get_or_insert(n);
+ }
+ if let Some(e) = else_end {
+ legend.push(format!("else of ?{n}"));
+ let m = legend.len();
+ for g in guard.iter_mut().take(e.min(steps.len())).skip(then_end) {
+ g.get_or_insert(m);
+ }
+ }
+ }
+ }
+ for (i, s) in steps.iter().enumerate() {
+ let mut item = match s {
+ Instr::Gate(k) => gate(&p.ops[*k]),
+ Instr::Measure { qubit, bit } => Item { cells: vec![(*qubit, format!("M{bit}"))], joined: false },
+ Instr::Reset(q) => Item { cells: vec![(*q, "|0⟩".into())], joined: false },
+ Instr::JumpUnless(..) | Instr::Jump(_) => continue,
+ };
+ if let Some(n) = guard[i] {
+ for (_, l) in item.cells.iter_mut() {
+ l.push_str(&format!("?{n}"));
+ }
+ }
+ out.push(item);
+ }
+ (out, legend)
+}
+
+/// The drawing, wrapped at `width` columns.
+pub fn draw(p: &Program, width: usize) -> String {
+ let n = p.n_qubits as usize;
+ if n == 0 {
+ return "(no qubits)\n".into();
+ }
+ let (items, legend) = items(p);
+ // Columns: each item goes to the first column after the last one used on
+ // any wire it covers.
+ let mut free = vec![0usize; n];
+ let mut columns: Vec> = Vec::new();
+ for it in items {
+ let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap_or(0);
+ let hi = if it.joined { it.cells.iter().map(|c| c.0 as usize).max().unwrap_or(lo) } else { lo };
+ let col = free[lo..=hi].iter().copied().max().unwrap_or(0);
+ for f in &mut free[lo..=hi] {
+ *f = col + 1;
+ }
+ if columns.len() <= col {
+ columns.resize_with(col + 1, Vec::new);
+ }
+ columns[col].push(it);
+ }
+ let label_w = format!("q{}", n - 1).chars().count();
+ let prefix = |q: usize| format!("{:>label_w$}: ", format!("q{q}"));
+ let blank = " ".repeat(label_w + 2);
+ // Each column's text per wire, and its joins between wire q and q + 1.
+ let mut blocks: Vec<(usize, Vec, Vec)> = Vec::new();
+ for col in &columns {
+ let w = col.iter().flat_map(|it| it.cells.iter().map(|c| c.1.chars().count())).max().unwrap_or(1).max(1);
+ let mut wires = vec![None::; n];
+ let mut through = vec![false; n];
+ let mut joins = vec![false; n];
+ for it in col {
+ for (q, l) in &it.cells {
+ wires[*q as usize] = Some(l.clone());
+ }
+ if it.joined {
+ let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap();
+ let hi = it.cells.iter().map(|c| c.0 as usize).max().unwrap();
+ for (q, t) in through.iter_mut().enumerate().take(hi).skip(lo + 1) {
+ *t = wires[q].is_none();
+ }
+ for j in joins.iter_mut().take(hi).skip(lo) {
+ *j = true;
+ }
+ }
+ }
+ let text: Vec = (0..n)
+ .map(|q| {
+ let l = match &wires[q] {
+ Some(l) => l.clone(),
+ None if through[q] => "┼".into(),
+ None => "─".into(),
+ };
+ let pad = w - l.chars().count();
+ format!("─{}{l}{}─", "─".repeat(pad / 2), "─".repeat(pad - pad / 2))
+ })
+ .collect();
+ blocks.push((w + 2, text, joins));
+ }
+ let mut out = String::new();
+ let avail = width.saturating_sub(label_w + 4).max(8);
+ let mut start = 0;
+ while start < blocks.len() || (start == 0 && blocks.is_empty()) {
+ let mut end = start;
+ let mut used = 0;
+ while end < blocks.len() && (end == start || used + blocks[end].0 <= avail) {
+ used += blocks[end].0;
+ end += 1;
+ }
+ if start > 0 {
+ out.push('\n');
+ }
+ for q in 0..n {
+ out.push_str(&prefix(q));
+ out.push('─');
+ for b in &blocks[start..end] {
+ out.push_str(&b.1[q]);
+ }
+ out.push_str("─\n");
+ if q + 1 < n {
+ let mut line = blank.clone();
+ line.push(' ');
+ for b in &blocks[start..end] {
+ // Where a one-character label sits in this column.
+ let w = b.0;
+ let mid = 1 + (w - 3) / 2;
+ let mut cell = vec![' '; w];
+ if b.2[q] {
+ cell[mid] = '│';
+ }
+ line.extend(cell);
+ }
+ out.push_str(line.trim_end());
+ out.push('\n');
+ }
+ }
+ if end == start {
+ break;
+ }
+ start = end;
+ }
+ for (i, l) in legend.iter().enumerate() {
+ out.push_str(&format!("?{} {l}\n", i + 1));
+ }
+ out
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::qasm::parse;
+
+ #[test]
+ fn angles_are_written_as_multiples_of_pi_where_they_are() {
+ assert_eq!(angle(PI / 4.0), "π/4");
+ assert_eq!(angle(-PI / 2.0), "-π/2");
+ assert_eq!(angle(3.0 * PI / 4.0), "3π/4");
+ assert_eq!(angle(2.0 * PI), "2π");
+ assert_eq!(angle(0.0), "0");
+ assert_eq!(angle(0.3), "0.3");
+ }
+
+ #[test]
+ fn a_bell_pair_is_h_then_a_controlled_x_joined_across_the_wires() {
+ let d = draw(&parse("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];").unwrap(), 100);
+ let lines: Vec<&str> = d.lines().collect();
+ assert_eq!(lines.len(), 3, "{d}");
+ assert!(lines[0].starts_with("q0: ") && lines[0].contains('H') && lines[0].contains('●'), "{d}");
+ assert!(lines[1].contains('│'), "{d}");
+ assert!(lines[2].starts_with("q1: ") && lines[2].contains('⊕') && !lines[2].contains('H'), "{d}");
+ // The control sits after H, in the column of the target.
+ assert_eq!(lines[0].chars().position(|c| c == '●'), lines[2].chars().position(|c| c == '⊕'), "{d}");
+ }
+
+ #[test]
+ fn gates_on_different_wires_share_a_column_and_a_gate_between_crosses_the_middle() {
+ let d = draw(&parse("qubit[3] q; h q[0]; h q[1]; h q[2]; cx q[0], q[2];").unwrap(), 100);
+ let lines: Vec<&str> = d.lines().collect();
+ let h_at: Vec> = [0, 2, 4].iter().map(|&i| lines[i].chars().position(|c| c == 'H')).collect();
+ assert!(h_at.iter().all(|&p| p == h_at[0]), "the three H share a column: {d}");
+ assert!(lines[2].contains('┼'), "the CX passes through q1: {d}");
+ }
+
+ #[test]
+ fn a_measurement_names_its_bit_and_a_conditioned_gate_its_condition() {
+ let d = draw(&parse("OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; c[0] = measure q[0]; if (c[0]) x q[1]; c[1] = measure q[1];").unwrap(), 100);
+ assert!(d.contains("M0") && d.contains("M1"), "{d}");
+ assert!(d.contains("X?1") && d.contains("?1 if bit 0 = 1"), "{d}");
+ }
+
+ #[test]
+ fn a_long_circuit_wraps_and_keeps_every_gate() {
+ let src = format!("qubit[2] q; {}", "h q[0]; cx q[0], q[1]; ".repeat(40));
+ let d = draw(&parse(&src).unwrap(), 60);
+ assert!(d.lines().all(|l| l.chars().count() <= 60), "{d}");
+ assert_eq!(d.matches('H').count(), 40, "{d}");
+ assert_eq!(d.matches('⊕').count(), 40, "{d}");
+ }
+}
diff --git a/wai/wai-quantum-service/src/dynamic.rs b/wai/wai-quantum/src/engine/dynamic.rs
similarity index 99%
rename from wai/wai-quantum-service/src/dynamic.rs
rename to wai/wai-quantum/src/engine/dynamic.rs
index 2de9314e5..15acdaa50 100644
--- a/wai/wai-quantum-service/src/dynamic.rs
+++ b/wai/wai-quantum/src/engine/dynamic.rs
@@ -19,9 +19,9 @@
use crate::qasm::{Instr, Op};
use std::collections::HashMap;
-use wai_quantum::gates::Gate as Matrix;
-use wai_quantum::quantum_sv::StateVector;
-use wai_quantum::quantum_tableau::{Clifford, Tableau};
+use crate::gates::Gate as Matrix;
+use crate::quantum_sv::StateVector;
+use crate::quantum_tableau::{Clifford, Tableau};
/// The most branches the exact expectation walk follows.
pub const MAX_LEAVES: u64 = 4096;
diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs
new file mode 100644
index 000000000..f26191bd6
--- /dev/null
+++ b/wai/wai-quantum/src/engine/job.rs
@@ -0,0 +1,126 @@
+//! A job end to end, as every host gives it: planned, run, sealed with the
+//! host's key, and returned in the service's form, the request, the plan
+//! and the result with its receipt. The service, the command line, the
+//! desktop app and a browser tab each call [`run`] with their own key and
+//! their own word on energy, and [`verify`] checks any of their answers.
+
+use super::{catalog, receipt};
+use ed25519_dalek::{SigningKey, VerifyingKey};
+use serde_json::{Value, json};
+
+/// The job body for `request`: planned within `job_bytes`, run, with
+/// `energy` (the host's figure, or why it has none) inside the result, and
+/// the result sealed with `key`.
+pub fn run(request: &Value, job_bytes: u64, key: &SigningKey, energy: Value) -> Result {
+ let (plan, result) = plan_and_run(request, job_bytes)?;
+ Ok(seal(request, &plan, result, key, energy))
+}
+
+/// `request` planned within `job_bytes` and run: the plan and the result,
+/// not yet sealed. A host that runs a job in a process of its own, away from
+/// its key, gives these back to be sealed ([`seal`]).
+pub fn plan_and_run(request: &Value, job_bytes: u64) -> Result<(catalog::Plan, Value), String> {
+ if let Some(at) = receipt::inexact_number(request) {
+ return Err(format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it"));
+ }
+ let plan = catalog::plan(request, job_bytes)?;
+ let result = super::run::run(request, &plan)?;
+ Ok((plan, result))
+}
+
+/// The job body for `result` of `request` under `plan`, in the service's
+/// form: `energy` inside the result, and the result sealed with `key`.
+pub fn seal(request: &Value, plan: &catalog::Plan, mut result: Value, key: &SigningKey, energy: Value) -> Value {
+ result["energy"] = energy;
+ let seal = receipt::seal(key, request, &result);
+ result["receipt"] = seal;
+ json!({
+ "status": "done",
+ "method": plan.method,
+ "why": plan.why,
+ "estimate": { "memory_bytes": plan.memory_bytes, "work": plan.work },
+ "request": request,
+ "result": result,
+ })
+}
+
+/// Whether a job body's result carries a receipt that seals that result and
+/// its request, and the key the receipt names.
+pub fn verify(job: &Value) -> Result<(bool, Value), String> {
+ let (Some(request), Some(result)) = (job.get("request"), job.get("result")) else {
+ return Err("expected a job: {\"request\": ..., \"result\": ...}, the result carrying its receipt".into());
+ };
+ let Some(seal) = result.get("receipt") else { return Err("the result carries no receipt".into()) };
+ let mut bare = result.clone();
+ if let Some(o) = bare.as_object_mut() {
+ o.remove("receipt");
+ }
+ Ok((receipt::verify(request, &bare, seal), seal.get("key").cloned().unwrap_or(Value::Null)))
+}
+
+/// The `did:key` of a key given as 64 hex digits, as a receipt names it.
+pub fn did_of(key_hex: &str) -> Option {
+ if key_hex.len() != 64 {
+ return None;
+ }
+ let mut b = [0u8; 32];
+ for (i, x) in b.iter_mut().enumerate() {
+ *x = u8::from_str_radix(key_hex.get(2 * i..2 * i + 2)?, 16).ok()?;
+ }
+ Some(crate::quantum_receipt::did_key(&VerifyingKey::from_bytes(&b).ok()?))
+}
+
+/// A program read as the engine reads it: `{"ok": true}` with its qubits,
+/// gates, bits and measurements (and its steps, when it is dynamic), or
+/// `{"ok": false}` with the problem and its line.
+pub fn lint(src: &str) -> Value {
+ match crate::qasm::parse(src) {
+ Ok(p) => json!({
+ "ok": true,
+ "qubits": p.n_qubits,
+ "gates": p.ops.len(),
+ "bits": p.n_bits,
+ "measured": p.measured.len(),
+ "dynamic_steps": p.dynamic.as_ref().map(Vec::len),
+ }),
+ Err(e) => {
+ let line = e.strip_prefix("line ").and_then(|r| r.split(':').next()).and_then(|n| n.parse::().ok());
+ json!({ "ok": false, "error": e, "line": line })
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn a_job_is_the_services_form_and_its_receipt_holds_only_as_given() {
+ let req = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
+ let key = SigningKey::from_bytes(&[3; 32]);
+ let body = run(&req, 1 << 30, &key, json!({ "unmetered": "a test" })).unwrap();
+ assert_eq!(body["method"], "stabilizer");
+ assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
+ assert_eq!(body["result"]["energy"]["unmetered"], "a test");
+ assert!(body["result"]["counts"].as_object().unwrap().keys().all(|k| k == "00" || k == "11"));
+ let (ok, k) = verify(&body).unwrap();
+ assert!(ok);
+ assert_eq!(did_of(k.as_str().unwrap()).unwrap(), crate::quantum_receipt::did_key(&key.verifying_key()));
+ for (path, v) in [("/result/expectations/0/value", json!(0.5)), ("/request/shots", json!(999)), ("/result/energy/unmetered", json!("other"))] {
+ let mut forged = body.clone();
+ *forged.pointer_mut(path).unwrap() = v;
+ assert!(!verify(&forged).unwrap().0, "{path}");
+ }
+ let again: Value = serde_json::from_str(&body.to_string()).unwrap();
+ assert!(verify(&again).unwrap().0);
+ assert!(verify(&json!({ "request": {} })).is_err());
+ }
+
+ #[test]
+ fn lint_gives_the_shape_or_the_line_of_the_problem() {
+ let ok = lint("OPENQASM 3.0; qubit[3] q; bit[3] c; h q; c = measure q;");
+ assert!(ok["ok"] == true && ok["qubits"] == 3 && ok["gates"] == 3 && ok["dynamic_steps"].is_null(), "{ok}");
+ let bad = lint("OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n");
+ assert!(bad["ok"] == false && bad["line"] == 3, "{bad}");
+ }
+}
diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs
new file mode 100644
index 000000000..c4dea9716
--- /dev/null
+++ b/wai/wai-quantum/src/engine/mod.rs
@@ -0,0 +1,33 @@
+//! The job service's engine: a program and what is asked of it, planned onto a
+//! method, run, and answered as JSON. The service, the command line and the
+//! desktop app plan and run a job the same way, so the same job gives the same
+//! result on each.
+//!
+//! - [`catalog`]: a job read and checked ([`catalog::Request`]), the methods
+//! and their limits, and [`catalog::plan`], which picks a method for a job
+//! and says why, with the memory and work it will take.
+//! - [`run`]: [`run::run`] carries a plan out and gives the result: counts,
+//! expectation values, amplitudes, the accuracy class and what the method
+//! did.
+//! - [`dynamic`]: programs that measure in the middle, reset or branch on a
+//! measured bit, run branch by branch.
+//! - [`receipt`] (with `quantum_receipt`): a result sealed with a key over
+//! the request and the result in their canonical JSON, and checked. The
+//! service seals with its key and the command line with the user's, in one
+//! form.
+//! - [`job`] (with `quantum_receipt`): a job end to end, planned, run and
+//! sealed, in the service's form; its check; and a program's lint.
+//! - [`draw`]: a program's circuit as text.
+//!
+//! Requests and results are the service's JSON (`/v1/openapi.json` there).
+//! Keys, metering a job's energy, queueing and limits per client stay with
+//! whatever hosts the engine.
+
+pub mod catalog;
+pub mod draw;
+pub mod dynamic;
+#[cfg(feature = "quantum_receipt")]
+pub mod job;
+#[cfg(feature = "quantum_receipt")]
+pub mod receipt;
+pub mod run;
diff --git a/wai/wai-quantum/src/engine/receipt.rs b/wai/wai-quantum/src/engine/receipt.rs
new file mode 100644
index 000000000..8252fe76e
--- /dev/null
+++ b/wai/wai-quantum/src/engine/receipt.rs
@@ -0,0 +1,397 @@
+//! Signed receipts over a job: a result sealed with an Ed25519 key, binding
+//! the request as submitted and the result as returned, so anyone can be shown
+//! what was run and what came back. The service seals every result with its
+//! key; the command line and the desktop app seal theirs with the user's, in
+//! the same form, and one [`verify`] checks them all.
+//!
+//! What is signed is the JSON Canonicalization Scheme (RFC 8785) text of each:
+//! keys sorted, no whitespace, and every number written as ECMAScript writes
+//! the double it reads. A result therefore verifies after any JSON reader and
+//! writer has carried it: a browser's `JSON.parse` and `JSON.stringify` turn
+//! `1.0` into `1`, and other readers reorder keys.
+
+use serde_json::{Value, json};
+use ed25519_dalek::{Signer as _, SigningKey, Verifier as _, VerifyingKey};
+
+/// The first receipts signed serde_json's own text, which only a reader that
+/// keeps each number's spelling and each object's key order can reproduce.
+/// They still verify; new ones are canonical.
+const DOMAIN_V1: &[u8] = b"wai:quantum-service-receipt\x01";
+const DOMAIN: &[u8] = b"wai:quantum-service-receipt\x02";
+/// The receipt's name for its canonical form.
+const CANON: &str = "rfc8785";
+
+fn hex(b: &[u8]) -> String {
+ b.iter().map(|x| format!("{x:02x}")).collect()
+}
+
+fn unhex(s: &str) -> Option> {
+ s.len().is_multiple_of(2).then_some(())?;
+ (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
+}
+
+/// A length-prefixed byte string, so that no field reads as part of another.
+fn put(o: &mut Vec, bytes: &[u8]) {
+ o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
+ o.extend_from_slice(bytes);
+}
+
+/// The digest a receipt signs: BLAKE3(domain ‖ put(request) ‖ put(result)),
+/// each in its RFC 8785 canonical text.
+pub fn digest(request: &Value, result: &Value) -> [u8; 32] {
+ let mut o = DOMAIN.to_vec();
+ put(&mut o, &canonical(request));
+ put(&mut o, &canonical(result));
+ *blake3::hash(&o).as_bytes()
+}
+
+/// The digest the first receipts signed (see [`DOMAIN_V1`]).
+fn digest_v1(request: &Value, result: &Value) -> [u8; 32] {
+ let mut o = DOMAIN_V1.to_vec();
+ put(&mut o, request.to_string().as_bytes());
+ put(&mut o, result.to_string().as_bytes());
+ *blake3::hash(&o).as_bytes()
+}
+
+/// The RFC 8785 canonical text of `v`.
+pub fn canonical(v: &Value) -> Vec {
+ let mut o = Vec::new();
+ write(v, &mut o);
+ o
+}
+
+fn write(v: &Value, o: &mut Vec) {
+ match v {
+ Value::Null => o.extend_from_slice(b"null"),
+ Value::Bool(b) => o.extend_from_slice(if *b { b"true" } else { b"false" }),
+ // Every number as the double a JSON reader in ECMAScript holds.
+ Value::Number(n) => o.extend_from_slice(es_number(n.as_f64().unwrap_or(f64::NAN)).as_bytes()),
+ Value::String(s) => write_str(s, o),
+ Value::Array(a) => {
+ o.push(b'[');
+ for (i, x) in a.iter().enumerate() {
+ if i > 0 {
+ o.push(b',');
+ }
+ write(x, o);
+ }
+ o.push(b']');
+ }
+ Value::Object(m) => {
+ // Keys in the order of their UTF-16 code units.
+ let mut keys: Vec<&String> = m.keys().collect();
+ keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
+ o.push(b'{');
+ for (i, k) in keys.into_iter().enumerate() {
+ if i > 0 {
+ o.push(b',');
+ }
+ write_str(k, o);
+ o.push(b':');
+ write(&m[k], o);
+ }
+ o.push(b'}');
+ }
+ }
+}
+
+/// A string as RFC 8785 writes it: `"` and `\\` escaped, the five short
+/// control escapes, every other control character as `\u00xx`, and the rest
+/// as it is.
+fn write_str(s: &str, o: &mut Vec) {
+ o.push(b'"');
+ for c in s.chars() {
+ match c {
+ '"' => o.extend_from_slice(b"\\\""),
+ '\\' => o.extend_from_slice(b"\\\\"),
+ '\u{8}' => o.extend_from_slice(b"\\b"),
+ '\t' => o.extend_from_slice(b"\\t"),
+ '\n' => o.extend_from_slice(b"\\n"),
+ '\u{c}' => o.extend_from_slice(b"\\f"),
+ '\r' => o.extend_from_slice(b"\\r"),
+ c if (c as u32) < 0x20 => o.extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes()),
+ c => {
+ let mut b = [0u8; 4];
+ o.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
+ }
+ }
+ }
+ o.push(b'"');
+}
+
+/// A double as ECMAScript's Number::toString writes it (ECMA-262, 6.1.6.1.20):
+/// the shortest digits that read back as `x`, laid out by where the decimal
+/// point falls. JSON holds no infinity or NaN; they write as `null`.
+pub fn es_number(x: f64) -> String {
+ if !x.is_finite() {
+ return "null".into();
+ }
+ if x == 0.0 {
+ return "0".into();
+ }
+ let (s, n) = shortest(x.abs());
+ let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
+ let k = s.len() as i32;
+ let mut out = String::new();
+ if x < 0.0 {
+ out.push('-');
+ }
+ if k <= n && n <= 21 {
+ out.push_str(&digits);
+ out.extend(std::iter::repeat_n('0', (n - k) as usize));
+ } else if 0 < n && n <= 21 {
+ out.push_str(&digits[..n as usize]);
+ out.push('.');
+ out.push_str(&digits[n as usize..]);
+ } else if -6 < n && n <= 0 {
+ out.push_str("0.");
+ out.extend(std::iter::repeat_n('0', (-n) as usize));
+ out.push_str(&digits);
+ } else {
+ out.push_str(&digits[..1]);
+ if k > 1 {
+ out.push('.');
+ out.push_str(&digits[1..]);
+ }
+ out.push('e');
+ out.push(if n - 1 < 0 { '-' } else { '+' });
+ out.push_str(&(n - 1).abs().to_string());
+ }
+ out
+}
+
+/// The digits s and the exponent n, x = 0.s × 10^n, that ECMAScript chooses:
+/// the fewest that read back as `x` (> 0), and of those the nearest to `x`,
+/// a tie going to the even.
+fn shortest(x: f64) -> (Vec, i32) {
+ let e = format!("{x:e}");
+ let (mant, exp) = e.split_once('e').unwrap_or((&e, "0"));
+ let mut s: Vec = mant.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
+ while s.len() > 1 && s.last() == Some(&0) {
+ s.pop();
+ }
+ let n = exp.parse::().unwrap_or(0) + 1;
+ // Rust's digits are the fewest, but a tie between two equally near it
+ // rounds up. Only when another of the same length also reads back as x
+ // (near 17 digits) is there a choice: then take x's exact expansion (a
+ // double's has at most 767 significant digits) rounded to that length.
+ if ![false, true].into_iter().any(|up| step(&s, up).is_some_and(|t| reads_as(&t, n, x))) {
+ return (s, n);
+ }
+ let k = s.len();
+ let exact = format!("{x:.800e}");
+ let (em, ee) = exact.split_once('e').unwrap_or((&exact, "0"));
+ let all: Vec = em.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
+ let mut cn = ee.parse::().unwrap_or(0) + 1;
+ let mut c = all[..k].to_vec();
+ let rest = &all[k..];
+ let up = match rest.first().copied().unwrap_or(0) {
+ d if d > 5 => true,
+ 5 => rest[1..].iter().any(|&d| d != 0) || c[k - 1] % 2 == 1,
+ _ => false,
+ };
+ if up {
+ match step(&c, true) {
+ Some(t) => c = t,
+ None => {
+ // 99…9 rounds to 10…0.
+ c = std::iter::once(1).chain(std::iter::repeat_n(0, k - 1)).collect();
+ cn += 1;
+ }
+ }
+ }
+ if reads_as(&c, cn, x) { (c, cn) } else { (s, n) }
+}
+
+/// The k-digit decimal one unit above or below `s` in its last digit, if it
+/// still has k digits.
+fn step(s: &[u8], up: bool) -> Option> {
+ let mut t = s.to_vec();
+ for i in (0..t.len()).rev() {
+ match (up, t[i]) {
+ (true, 9) => t[i] = 0,
+ (false, 0) => t[i] = 9,
+ (true, d) | (false, d) => {
+ t[i] = if up { d + 1 } else { d - 1 };
+ return (t[0] != 0).then_some(t);
+ }
+ }
+ }
+ None
+}
+
+/// Whether 0.s × 10^n reads as `x` (std's reader is correctly rounded).
+fn reads_as(s: &[u8], n: i32, x: f64) -> bool {
+ let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
+ format!("0.{digits}e{n}").parse::().is_ok_and(|y| y == x)
+}
+
+/// The first number in `v` that a double cannot hold exactly (an integer past
+/// ±2^53), by its path. Such a number would be read differently by readers that
+/// hold every number as a double, so the receipt could not bind it.
+pub fn inexact_number(v: &Value) -> Option {
+ const SAFE: u64 = (1 << 53) - 1;
+ fn walk(v: &Value, path: &mut String) -> bool {
+ match v {
+ Value::Number(n) => n.as_u64().is_some_and(|u| u > SAFE) || n.as_i64().is_some_and(|i| i.unsigned_abs() > SAFE),
+ Value::Array(a) => a.iter().enumerate().any(|(i, x)| {
+ let at = path.len();
+ path.push_str(&format!("[{i}]"));
+ let hit = walk(x, path);
+ if !hit {
+ path.truncate(at);
+ }
+ hit
+ }),
+ Value::Object(m) => m.iter().any(|(k, x)| {
+ let at = path.len();
+ path.push('.');
+ path.push_str(k);
+ let hit = walk(x, path);
+ if !hit {
+ path.truncate(at);
+ }
+ hit
+ }),
+ _ => false,
+ }
+ }
+ let mut path = String::new();
+ walk(v, &mut path).then(|| path.trim_start_matches('.').to_string())
+}
+
+/// The receipt for `result` of `request`, sealed with `key`.
+pub fn seal(key: &SigningKey, request: &Value, result: &Value) -> Value {
+ let d = digest(request, result);
+ let sig = key.sign(&d);
+ json!({ "alg": "ed25519", "canon": CANON, "key": hex(key.verifying_key().as_bytes()), "digest": hex(&d), "signature": hex(&sig.to_bytes()) })
+}
+
+/// Whether `receipt` is a valid seal of `result` of `request` under the key
+/// it names.
+pub fn verify(request: &Value, result: &Value, receipt: &Value) -> bool {
+ let field = |k: &str| receipt.get(k).and_then(Value::as_str).and_then(unhex);
+ let (Some(key), Some(sig)) = (field("key"), field("signature")) else { return false };
+ let (Ok(key), Ok(sig)) = (<[u8; 32]>::try_from(key.as_slice()), <[u8; 64]>::try_from(sig.as_slice())) else { return false };
+ let Ok(vk) = VerifyingKey::from_bytes(&key) else { return false };
+ let d = match receipt.get("canon").and_then(Value::as_str) {
+ Some(CANON) => digest(request, result),
+ None => digest_v1(request, result),
+ Some(_) => return false,
+ };
+ field("digest").is_some_and(|g| g == d) && vk.verify(&d, &ed25519_dalek::Signature::from_bytes(&sig)).is_ok()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// A key for the tests, sealing as a host does.
+ struct Signer(SigningKey);
+ impl Signer {
+ fn from_seed(seed: [u8; 32]) -> Signer {
+ Signer(SigningKey::from_bytes(&seed))
+ }
+ fn public_key(&self) -> String {
+ hex(self.0.verifying_key().as_bytes())
+ }
+ fn seal(&self, request: &Value, result: &Value) -> Value {
+ seal(&self.0, request, result)
+ }
+ }
+
+ #[test]
+ fn a_receipt_verifies_and_any_change_breaks_it() {
+ let s = Signer::from_seed([7; 32]);
+ let req = json!({"qasm": "OPENQASM 3.0;", "shots": 10});
+ let res = json!({"method": "statevector", "counts": {"0": 10}});
+ let r = s.seal(&req, &res);
+ assert!(verify(&req, &res, &r));
+ assert!(!verify(&req, &json!({"method": "statevector", "counts": {"0": 9}}), &r));
+ assert!(!verify(&json!({"qasm": "OPENQASM 3.0;", "shots": 11}), &res, &r));
+ let mut forged = r.clone();
+ forged["key"] = json!(Signer::from_seed([8; 32]).public_key());
+ assert!(!verify(&req, &res, &forged));
+ }
+
+ #[test]
+ fn numbers_are_written_as_ecmascript_writes_them() {
+ // Bit patterns and ECMAScript's Number::toString of each, written by a
+ // JavaScript engine: RFC 8785's appendix B, the edges of each layout,
+ // and patterns spread over every exponent.
+ let mut n = 0;
+ for line in include_str!("testdata/es-number-tostring.txt").lines() {
+ let (bits, want) = line.split_once(' ').unwrap();
+ let x = f64::from_bits(u64::from_str_radix(bits, 16).unwrap());
+ assert_eq!(es_number(x), want, "{bits}");
+ // And the text reads back as the same double, as it must for a
+ // result to verify after a round trip (serde_json's
+ // float_roundtrip: its default reader rounds twice).
+ if x != 0.0 {
+ assert_eq!(serde_json::from_str::(want).unwrap().to_bits(), x.to_bits(), "{bits} {want}");
+ }
+ n += 1;
+ }
+ assert!(n > 3000);
+ }
+
+ /// The same against a larger set, in the same format, written by a
+ /// JavaScript engine: `WAI_ES_VECTORS=path cargo test -- --ignored`.
+ #[test]
+ #[ignore]
+ fn numbers_match_a_javascript_engine_on_a_large_set() {
+ let path = std::env::var("WAI_ES_VECTORS").expect("WAI_ES_VECTORS: a file of `hexbits text` lines");
+ let text = std::fs::read_to_string(path).unwrap();
+ let mut n = 0u64;
+ for line in text.lines() {
+ let (bits, want) = line.split_once(' ').unwrap();
+ assert_eq!(es_number(f64::from_bits(u64::from_str_radix(bits, 16).unwrap())), want, "{bits}");
+ n += 1;
+ }
+ eprintln!("{n} numbers agree");
+ }
+
+ #[test]
+ fn the_canonical_text_sorts_keys_and_writes_numbers_and_strings_one_way() {
+ let v = json!({ "b": 1.0, "a": [true, null, "x\n\u{1}\"\\é"], "\u{e000}": 0.000001, "\u{1f600}": -0.0, "c": 1e21 });
+ // U+1F600 is a surrogate pair, so it sorts before U+E000.
+ assert_eq!(String::from_utf8(canonical(&v)).unwrap(), "{\"a\":[true,null,\"x\\n\\u0001\\\"\\\\é\"],\"b\":1,\"c\":1e+21,\"\u{1f600}\":0,\"\u{e000}\":0.000001}");
+ }
+
+ #[test]
+ fn a_receipt_verifies_after_a_round_trip_through_another_json_writer() {
+ let s = Signer::from_seed([7; 32]);
+ let req = json!({ "qasm": "qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
+ let res = json!({ "method": "stabilizer", "expectations": [{ "observable": "Z0 Z1", "coeff": 1.0, "value": 1.0 }], "threshold": 0.00001, "elapsed_ms": 0 });
+ let r = s.seal(&req, &res);
+ // What a browser sends back: integral doubles without a fraction, keys
+ // in another order, the threshold in plain decimals.
+ let req2: Value = serde_json::from_str(r#"{"observables":["Z0 Z1"],"shots":1000,"qasm":"qubit[2] q; h q[0]; cx q[0], q[1];"}"#).unwrap();
+ let res2: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":1,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
+ assert!(verify(&req2, &res2, &r));
+ let res3: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":0.999,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
+ assert!(!verify(&req2, &res3, &r));
+ }
+
+ #[test]
+ fn the_first_receipts_still_verify_and_an_unknown_form_does_not() {
+ let s = Signer::from_seed([7; 32]);
+ let req = json!({ "qasm": "OPENQASM 3.0;", "shots": 10 });
+ let res = json!({ "method": "statevector", "counts": { "0": 10 } });
+ let d = digest_v1(&req, &res);
+ let old = json!({ "alg": "ed25519", "key": s.public_key(), "digest": hex(&d), "signature": hex(&s.0.sign(&d).to_bytes()) });
+ assert!(verify(&req, &res, &old));
+ let mut other = s.seal(&req, &res);
+ other["canon"] = json!("something-else");
+ assert!(!verify(&req, &res, &other));
+ }
+
+ #[test]
+ fn integers_a_double_cannot_hold_are_found_by_path() {
+ assert_eq!(inexact_number(&json!({ "seed": 9007199254740991u64, "shots": 10 })), None);
+ assert_eq!(inexact_number(&json!({ "seed": 9007199254740992u64 })).as_deref(), Some("seed"));
+ assert_eq!(inexact_number(&json!({ "options": { "x": [1, -9007199254740993i64] } })).as_deref(), Some("options.x[1]"));
+ assert_eq!(inexact_number(&json!({ "noise": { "readout": 1e300 } })), None);
+ }
+
+}
diff --git a/wai/wai-quantum-service/src/run.rs b/wai/wai-quantum/src/engine/run.rs
similarity index 94%
rename from wai/wai-quantum-service/src/run.rs
rename to wai/wai-quantum/src/engine/run.rs
index d87ce1194..08549f738 100644
--- a/wai/wai-quantum-service/src/run.rs
+++ b/wai/wai-quantum/src/engine/run.rs
@@ -2,10 +2,9 @@
//! terms. Bit strings are the program's classical bits, bit `n_bits − 1`
//! first; an amplitude's bit string names every qubit, qubit `n − 1` first.
-use crate::catalog::{self, MAX_OUTCOMES, Plan, Request};
+use crate::engine::catalog::{self, MAX_OUTCOMES, Plan, Request};
use serde_json::{Value, json};
use std::collections::HashMap;
-use std::time::Instant;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
@@ -47,7 +46,7 @@ fn index_of(bits: &str, n: u32, pos: impl Fn(u32) -> u32) -> usize {
/// `⟨ψ|P|ψ⟩` on the bit-exact state, exactly: the fixed-point amplitudes
/// multiplied and summed in 128-bit integers, then scaled once.
-fn expect_fixed(amps: &[wai_quantum::quantum::Amp], string: &[(u32, char)]) -> f64 {
+fn expect_fixed(amps: &[crate::quantum::Amp], string: &[(u32, char)]) -> f64 {
let (mut flip, mut zmask, mut ys) = (0usize, 0usize, 0u32);
for &(q, p) in string {
let bit = 1usize << q;
@@ -85,10 +84,35 @@ fn expectation_entries(r: &Request, value: impl Fn(&[(u32, char)]) -> f64) -> Va
Value::Array(r.observables.iter().map(|(text, coeff, string)| json!({ "observable": text, "coeff": coeff, "value": coeff * value(string) })).collect())
}
+/// When a run started, on a target with a clock std can read. A browser tab
+/// (wasm32-unknown-unknown) has none, so a result there carries no
+/// `elapsed_ms`; its host can time the call itself.
+#[derive(Clone, Copy)]
+struct Started(Option);
+
+impl Started {
+ fn now() -> Started {
+ #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+ {
+ Started(None)
+ }
+ #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+ {
+ Started(Some(std::time::Instant::now()))
+ }
+ }
+
+ fn stamp(self, out: &mut Value) {
+ if let Some(t) = self.0 {
+ out["elapsed_ms"] = json!(t.elapsed().as_millis() as u64);
+ }
+ }
+}
+
/// Runs `plan` for the job `request`: the result, or why it failed.
pub fn run(request: &Value, plan: &Plan) -> Result {
let r = catalog::read(request)?;
- let started = Instant::now();
+ let started = Started::now();
let n = r.program.n_qubits;
let mut out = json!({
"method": plan.method,
@@ -97,7 +121,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
});
if let Some(steps) = &r.program.dynamic {
run_dynamic(&r, plan, steps, &mut out)?;
- out["elapsed_ms"] = json!(started.elapsed().as_millis() as u64);
+ started.stamp(&mut out);
return Ok(out);
}
match plan.method.as_str() {
@@ -132,8 +156,11 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
);
}
}
+ #[cfg(not(feature = "quantum_sv_metal"))]
+ "statevector_single" => return Err("this build has no GPU state vector".into()),
+ #[cfg(feature = "quantum_sv_metal")]
"statevector_single" => {
- use wai_quantum::quantum_sv_metal::GpuState;
+ use crate::quantum_sv_metal::GpuState;
let gates = r.program.matrices();
// Wider fused gates run slower on the GPU than the passes they save.
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 3) as usize;
@@ -165,7 +192,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
"statevector" => {
let gates = r.program.matrices();
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as usize;
- let mut sv = wai_quantum::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
+ let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
sv.run(&gates, width).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({ "class": "exact", "fusion": width });
if r.shots > 0 {
@@ -191,7 +218,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"stabilizer" if r.noise.is_some() => {
- use wai_quantum::quantum_tableau::{FrameStep, frame_counts, frame_expectations};
+ use crate::quantum_tableau::{FrameStep, frame_counts, frame_expectations};
let noise = r.noise.ok_or("no noise model")?;
// Each op's gates, then its noise on the qubits it touched, as the
// other noisy methods apply it.
@@ -245,7 +272,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"stabilizer" => {
- use wai_quantum::quantum_tableau::Tableau;
+ use crate::quantum_tableau::Tableau;
let t = Tableau::run(n, &r.program.clifford()?).map_err(|e| e.to_string())?;
out["accuracy"] = json!({ "class": "exact" });
if r.shots > 0 {
@@ -276,7 +303,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"pauli" => {
- use wai_quantum::quantum_spd::{Pauli, SpdConfig, expectation_capped};
+ use crate::quantum_spd::{Pauli, SpdConfig, expectation_capped};
let rc = r.program.rotations();
// As many terms as the job's memory holds: a term's string (two
// words of bits a 64 qubits, padded to a power of two), its
@@ -323,7 +350,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
out["expectations"] = Value::Array(entries);
}
"mps" => {
- use wai_quantum::quantum_mpsim::{Chain, MpsConfig};
+ use crate::quantum_mpsim::{Chain, MpsConfig};
let chi = catalog::mps_bond(&r);
let cutoff = r.options.get("cutoff").and_then(Value::as_f64).unwrap_or(1e-12).clamp(0.0, 1e-2);
let mut chain = Chain::run(n, &r.program.local_matrices(), MpsConfig { max_bond: chi, cutoff }).map_err(|e| e.to_string())?;
@@ -362,7 +389,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"cutrank" => {
- use wai_quantum::quantum_cutrank::Precision;
+ use crate::quantum_cutrank::Precision;
// An order at least as cheap as the planner's (searched longer, from
// several seeds), and the precision the admitted memory allows.
let pm = catalog::product_measured(&r, true)?;
@@ -403,7 +430,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"tensor_network" => {
- use wai_quantum::quantum_tn::{Network, PathOptions, contract_plan, find_path};
+ use crate::quantum_tn::{Network, PathOptions, contract_plan, find_path};
let gates = r.program.matrices();
out["accuracy"] = json!({ "class": "exact" });
let mut entries = Vec::new();
@@ -417,14 +444,14 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
let plan = find_path(&net.shape(), &net.dims, &net.open, &PathOptions::default());
flops += plan.total_flops();
let t = contract_plan(&net, &plan).map_err(|e| format!("{e:?}"))?;
- let a = t.data.first().copied().unwrap_or(wai_quantum::gates::complex(0.0, 0.0));
+ let a = t.data.first().copied().unwrap_or(crate::gates::complex(0.0, 0.0));
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
}
out["amplitudes"] = Value::Array(entries);
out["contraction_flops"] = json!(flops);
}
"density_matrix" => {
- use wai_quantum::quantum_noisy::DensityMatrix;
+ use crate::quantum_noisy::DensityMatrix;
let noise = r.noise.ok_or("no noise model")?;
let rho = DensityMatrix::evolve(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({ "class": "exact", "note": "the noisy state itself, under the model; counts are seeded samples of its distribution" });
@@ -441,13 +468,13 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
}
"trajectories" => {
- use wai_quantum::quantum_noisy::branching_trajectories;
+ use crate::quantum_noisy::branching_trajectories;
let noise = r.noise.ok_or("no noise model")?;
let count = catalog::trajectory_count(&r);
// The plan reserved memory for this many states.
let states = ((plan.memory_bytes.saturating_sub(256 << 20)) / (catalog::amplitude_bytes(n) * if n < 16 { 8 } else { 1 }).max(1)).clamp(1, 16) as usize;
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8);
- let observables: Vec = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
+ let observables: Vec = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
let run = branching_trajectories(&r.program.matrices(), n, &noise, &observables, count, r.seed, states, threads).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({
"class": "estimate",
@@ -476,29 +503,30 @@ pub fn run(request: &Value, plan: &Plan) -> Result {
}
other => return Err(format!("unknown method {other}")),
}
- out["elapsed_ms"] = json!(started.elapsed().as_millis() as u64);
+ started.stamp(&mut out);
Ok(out)
}
/// A program that measures in the middle, resets, or acts on measured bits
-/// ([`crate::dynamic`]): counts by shot splitting, expectation values as the
+/// ([`crate::engine::dynamic`]): counts by shot splitting, expectation values as the
/// exact mixture over its branches.
fn run_dynamic(r: &catalog::Request, plan: &Plan, steps: &[crate::qasm::Instr], out: &mut Value) -> Result<(), String> {
- use crate::dynamic::{self, Dense, Stabilizer};
+ use crate::engine::dynamic::{self, Dense, Stabilizer};
let (n, nb) = (r.program.n_qubits, r.program.n_bits);
let strings: Vec> = r.observables.iter().map(|(_, _, s)| s.clone()).collect();
let (sampled, exact) = match plan.method.as_str() {
"statevector" => {
let gates = r.program.matrices();
- let states = (plan.memory_bytes / catalog::statevector_bytes(n)).max(1) as usize;
- let start = Dense { sv: wai_quantum::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?, gates: &gates };
+ // The states the plan admitted: its memory less the job's margin and the counts.
+ let states = (plan.memory_bytes.saturating_sub((256 << 20) + r.shots * 24) / catalog::amplitude_bytes(n)).max(1) as usize;
+ let start = Dense { sv: crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?, gates: &gates };
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, states)?) } else { None };
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, states)?) };
(s, e)
}
"stabilizer" => {
- let ops: Vec> = r.program.clifford_ops()?.into_iter().map(|(g, _)| g).collect();
- let start = Stabilizer { t: wai_quantum::quantum_tableau::Tableau::new(n).map_err(|e| e.to_string())?, ops: &ops };
+ let ops: Vec> = r.program.clifford_ops()?.into_iter().map(|(g, _)| g).collect();
+ let start = Stabilizer { t: crate::quantum_tableau::Tableau::new(n).map_err(|e| e.to_string())?, ops: &ops };
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, 64)?) } else { None };
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, 64)?) };
(s, e)
@@ -567,7 +595,7 @@ fn sample_distribution(p: &[f64], shots: u64, seed: u64) -> Vec {
#[cfg(test)]
mod tests {
use super::*;
- use crate::catalog::plan;
+ use crate::engine::catalog::plan;
const CAP: u64 = 8 << 30;
@@ -577,12 +605,13 @@ mod tests {
}
#[test]
+ #[cfg(feature = "quantum_toolchain")]
fn grover_as_the_ide_exports_it_finds_the_marked_item() {
// Its oracle and its diffusion are each a Z under four controls,
// written `ctrl(4) @ z`. Four rounds on five qubits find the marked
// item with probability 0.9992.
let marked = 0b10110u32;
- let qasm = wai_quantum::quantum_toolchain::to_qasm(&wai_quantum::quantum_toolchain::grover(5, marked));
+ let qasm = crate::quantum_toolchain::to_qasm(&crate::quantum_toolchain::grover(5, marked));
assert!(qasm.contains("ctrl(4) @ z"), "{qasm}");
let key: String = (0..5).rev().map(|q| if marked >> q & 1 == 1 { '1' } else { '0' }).collect();
for method in ["exact", "statevector"] {
@@ -707,7 +736,7 @@ mod tests {
fn the_gpu_state_vector_agrees_where_there_is_one_and_is_refused_where_not() {
let qasm = "qubit[14] q; bit[14] c; h q; rx(0.3) q[0]; cx q[0], q[13]; crz(0.7) q[3], q[9]; ry(1.1) q[5]; c = measure q;";
let job = json!({ "qasm": qasm, "method": "statevector_single", "shots": 2000, "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] });
- match crate::catalog::gpu_max_qubits() {
+ match crate::engine::catalog::gpu_max_qubits() {
Ok(_) => {
let g = go(job);
let d = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] }));
@@ -908,25 +937,6 @@ mod tests {
}
}
- /// The two worked examples that measure in the middle do what their
- /// `shows` says.
- #[test]
- fn the_measuring_examples_show_what_they_say() {
- let ex = crate::discovery::examples();
- let job = |name: &str| ex.as_array().unwrap().iter().find(|e| e["name"] == name).unwrap()["job"].clone();
- let t = go(job("teleport"));
- assert_eq!(t["method"], "statevector", "{t}");
- assert!((t["expectations"][0]["value"].as_f64().unwrap() - 0.7f64.cos()).abs() < 1e-12, "{t}");
- assert!((t["expectations"][1]["value"].as_f64().unwrap() - 0.7f64.sin()).abs() < 1e-12, "{t}");
- assert_eq!(t["dynamic"]["branches_weighed"], 4, "{t}");
- let b = go(job("bit-flip-code"));
- assert_eq!(b["method"], "stabilizer", "{b}");
- assert_eq!(b["expectations"][0]["value"], 1.0, "{b}");
- let counts = b["counts"].as_object().unwrap();
- assert!(counts.keys().all(|k| k.ends_with("11")), "the syndrome names the middle qubit on every shot: {b}");
- assert_eq!(counts.values().map(|v| v.as_u64().unwrap()).sum::(), 1000);
- }
-
/// A Clifford circuit on `n` qubits (Hadamards, phases, controlled-Z and
/// CX between pseudo-random pairs), then one layer of one-qubit gates at
/// generic angles on every qubit.
diff --git a/wai/wai-quantum-service/testdata/es-number-tostring.txt b/wai/wai-quantum/src/engine/testdata/es-number-tostring.txt
similarity index 100%
rename from wai/wai-quantum-service/testdata/es-number-tostring.txt
rename to wai/wai-quantum/src/engine/testdata/es-number-tostring.txt
diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs
index 876f07274..8a2933c51 100644
--- a/wai/wai-quantum/src/lib.rs
+++ b/wai/wai-quantum/src/lib.rs
@@ -47,6 +47,15 @@ mod linalg;
#[cfg(any(feature = "quantum_tn", feature = "quantum_sv"))]
pub mod gates;
+// OpenQASM 3 (and the OpenQASM 2 forms in common use): one front end for every
+// surface, lowered to each method's input.
+#[cfg(feature = "quantum_qasm")]
+pub mod qasm;
+
+// The job service's engine: plan, run and answer a job, the same on every host.
+#[cfg(feature = "quantum_engine")]
+pub mod engine;
+
#[cfg(any(
feature = "quantum_spd",
feature = "quantum_pauli",
diff --git a/wai/wai-quantum-service/src/qasm.rs b/wai/wai-quantum/src/qasm.rs
similarity index 98%
rename from wai/wai-quantum-service/src/qasm.rs
rename to wai/wai-quantum/src/qasm.rs
index b79f1730c..37bfe5a3f 100644
--- a/wai/wai-quantum-service/src/qasm.rs
+++ b/wai/wai-quantum/src/qasm.rs
@@ -16,7 +16,7 @@
//! OpenQASM 2's `if(c==n) gate;`. A program that measures a qubit and then
//! acts on it, resets one after its first gate, or uses `if` is dynamic: it
//! is kept as steps ([`Instr`]) as well as gates, and runs by
-//! [`crate::dynamic`]. Every other program is exactly what it was before.
+//! the service's `dynamic` module. Every other program is exactly what it was before.
//!
//! Control modifiers, `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @`,
//! on `x y z h s sdg t tdg p` and on the controlled names `cx cy cz ch cp ccx
@@ -35,10 +35,10 @@
//! lowering to the others.
use std::collections::HashMap;
-use wai_quantum::gates::{C, Gate as Matrix, complex};
-use wai_quantum::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
-use wai_quantum::quantum_spd::{Op as Rot, Pauli, RotCircuit};
-use wai_quantum::quantum_tableau::Clifford;
+use crate::gates::{C, Gate as Matrix, complex};
+use crate::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
+use crate::quantum_spd::{Op as Rot, Pauli, RotCircuit};
+use crate::quantum_tableau::Clifford;
/// The most qubits a program declares.
pub const MAX_QUBITS: u32 = 65_536;
@@ -1119,6 +1119,20 @@ impl Parser {
}
/// Parses an OpenQASM program.
+/// A program whose measurements all come at the end, as the dyadic circuit:
+/// the reader of the bit-exact state vector, the in-browser IDE and the CLI.
+/// Every register, broadcast, gate definition and modifier [`parse`] reads is
+/// read the same way here. A program that measures a qubit and then acts on
+/// it, resets a qubit after its first gate, or acts on measured bits is
+/// refused, never rewritten: its meaning needs the measurements.
+pub fn dyadic_circuit(src: &str) -> Result {
+ let p = parse(src)?;
+ if p.dynamic.is_some() {
+ return Err("this program measures a qubit and then acts on it, resets a qubit after its first gate, or acts on measured bits; the exact state vector here takes programs whose measurements all come at the end".into());
+ }
+ p.dyadic()
+}
+
pub fn parse(src: &str) -> Result {
let toks = lex(src)?;
let mut p = Parser {
@@ -1292,7 +1306,7 @@ fn lowering_phase(op: &Op, gates: &[Clifford]) -> C {
let want = op.matrix().matrix;
let mut best = (0.0f64, complex(1.0, 0.0));
for col in 0..d {
- let mut sv = wai_quantum::quantum_sv::StateVector::zero(k).expect("at most three qubits");
+ let mut sv = crate::quantum_sv::StateVector::zero(k).expect("at most three qubits");
sv.re[0] = 0.0;
sv.re[col] = 1.0;
for g in &local_gates {
@@ -2119,8 +2133,8 @@ mod tests {
Program { n_qubits: n, ops, measured: (0..n).map(|q| (q, q)).collect(), n_bits: n, dynamic: None }
}
- fn dense(p: &Program) -> wai_quantum::quantum_sv::StateVector {
- let mut sv = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
+ fn dense(p: &Program) -> crate::quantum_sv::StateVector {
+ let mut sv = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
sv.run(&p.matrices(), 1).unwrap();
sv
}
@@ -2276,7 +2290,7 @@ mod tests {
#[test]
fn the_clifford_lowering_gives_the_matrices_expectation_values() {
- use wai_quantum::quantum_tableau::Tableau;
+ use crate::quantum_tableau::Tableau;
for seed in 0..12 {
let p = clifford_kinds(5, seed);
let sv = dense(&p);
@@ -2304,7 +2318,7 @@ mod tests {
for seed in 0..6 {
let p = every_kind(4, seed + 300);
let want = dense(&p);
- let mut got = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
+ let mut got = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
let local = p.local_matrices();
assert!(local.iter().all(|g| g.qubits.len() <= 2));
got.run(&local, 1).unwrap();
@@ -2319,7 +2333,7 @@ mod tests {
#[test]
fn the_rotation_lowering_gives_the_matrices_expectation_values() {
- use wai_quantum::quantum_spd::{SpdConfig, expectation};
+ use crate::quantum_spd::{SpdConfig, expectation};
for seed in 0..6 {
let p = every_kind(4, seed + 100);
let sv = dense(&p);
diff --git a/wai/wai-quantum/src/quantum_receipt.rs b/wai/wai-quantum/src/quantum_receipt.rs
index e7832dd92..211c6cfd1 100644
--- a/wai/wai-quantum/src/quantum_receipt.rs
+++ b/wai/wai-quantum/src/quantum_receipt.rs
@@ -391,6 +391,34 @@ impl QuantumReceipt {
}
}
+/// The `did:key` identifier of an Ed25519 public key, as the W3C did:key
+/// method writes it: `did:key:z` and the base58btc text of the multicodec
+/// prefix `0xed 0x01` followed by the key's 32 bytes. A signer that names itself
+/// this way names the key that signed, so the name cannot claim another key.
+pub fn did_key(key: &VerifyingKey) -> String {
+ const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ let mut bytes = vec![0xed, 0x01];
+ bytes.extend_from_slice(key.as_bytes());
+ // Base 58 digits, least significant first. The prefix's first byte is not
+ // zero, so the text has no leading `1`s.
+ let mut digits: Vec = Vec::with_capacity(48);
+ for &b in &bytes {
+ let mut carry = u32::from(b);
+ for d in digits.iter_mut() {
+ carry += u32::from(*d) << 8;
+ *d = (carry % 58) as u8;
+ carry /= 58;
+ }
+ while carry > 0 {
+ digits.push((carry % 58) as u8);
+ carry /= 58;
+ }
+ }
+ let mut s = String::from("did:key:z");
+ s.extend(digits.iter().rev().map(|&d| ALPHABET[usize::from(d)] as char));
+ s
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -558,6 +586,18 @@ mod tests {
}
}
+ /// The W3C did:key method's Ed25519 example, from its raw key.
+ #[test]
+ fn did_key_writes_the_methods_example() {
+ let raw: [u8; 32] = core::array::from_fn(|i| {
+ u8::from_str_radix(&"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"[2 * i..2 * i + 2], 16).unwrap()
+ });
+ let vk = VerifyingKey::from_bytes(&raw).unwrap();
+ assert_eq!(did_key(&vk), "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp");
+ assert!(did_key(&key(5).verifying_key()).starts_with("did:key:z6Mk"));
+ assert_ne!(did_key(&key(5).verifying_key()), did_key(&key(6).verifying_key()));
+ }
+
#[test]
fn json_refuses_counts_that_do_not_fit() {
let r = QuantumReceipt::seal(&key(5), "m", &ghz3(Gateset::V2), None, 0, None).unwrap();
diff --git a/wai/wai-quantum/src/quantum_toolchain.rs b/wai/wai-quantum/src/quantum_toolchain.rs
index 978c8021a..6064818ff 100644
--- a/wai/wai-quantum/src/quantum_toolchain.rs
+++ b/wai/wai-quantum/src/quantum_toolchain.rs
@@ -674,7 +674,15 @@ pub fn to_qasm(c: &Circuit) -> String {
/// if its gateset is then changed.
pub fn from_qasm(src: &str) -> Result {
let mut n: Option = None;
+ let mut registers = 0;
// first pass: qubit count
+ // Registers counted statement by statement, however lines are broken.
+ for stmt in src.lines().map(|l| l.split("//").next().unwrap_or("")).flat_map(|l| l.split(';')) {
+ let stmt = stmt.trim();
+ if stmt.starts_with("qubit") || stmt.starts_with("qreg ") {
+ registers += 1;
+ }
+ }
for raw in src.lines() {
let line = strip(raw);
if let Some(rest) = line.strip_prefix("qubit[")
@@ -688,20 +696,42 @@ pub fn from_qasm(src: &str) -> Result {
}
}
let n = n.ok_or("no qubit register declaration (qubit[n] q; or qreg q[n];)")?;
+ // What this reader does not take is refused, never dropped: the program
+ // would mean something else. `wai_quantum::qasm` reads all of it.
+ let fuller = "; the full reader (wai_quantum::qasm, the IDE's and the service's) takes it";
+ if registers > 1 {
+ return Err(format!("{registers} qubit registers; this reader takes one{fuller}"));
+ }
let mut c = Circuit::new(n);
+ let mut measured = false;
for raw in src.lines() {
let line = strip(raw);
if line.is_empty() {
continue;
}
+ if line.contains(';') && !line.starts_with("OPENQASM") && !line.starts_with("include") {
+ return Err(format!("several statements on one line (`{line}`); this reader takes one a line{fuller}"));
+ }
+ let first: String = line.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
+ match first.as_str() {
+ "reset" => return Err(format!("`reset` changes the state; this reader does not take it{fuller}")),
+ "gate" => return Err(format!("gate definitions are not read here{fuller}")),
+ "if" | "while" | "for" => return Err(format!("`{first}` acts on measured bits; this reader does not take it{fuller}")),
+ _ => {}
+ }
// Measurement is recorded, not simulated here, and the exporters disagree
// about how to spell it: OpenQASM 2 writes `measure q[0] -> c[0];` while
// Qiskit's OpenQASM 3 exporter writes `c[0] = measure q[0];`, whose first
// token is the *classical* register. Skip either shape before parsing a
// gate name out of the line.
if line.contains("measure") {
+ measured = true;
continue;
}
+ let gate_line = !matches!(first.as_str(), "OPENQASM" | "include" | "qubit" | "qreg" | "creg" | "bit" | "barrier");
+ if measured && gate_line {
+ return Err(format!("a gate after a measurement (`{line}`): this reader takes measurements only at the end{fuller}"));
+ }
// Gate modifiers: `ctrl @`, `ctrl(n) @`, `negctrl @`, `negctrl(n) @`.
if let Some((polarity, call)) = modifiers(&line)? {
push_modified(&mut c, &polarity, &call)?;
@@ -1322,6 +1352,24 @@ mod tests {
assert_eq!(recommend_backend(&Circuit::qft(6)).0, Backend::Statevector);
}
+ /// What this reader cannot take is refused, never dropped.
+ #[test]
+ fn what_the_reader_cannot_take_is_refused() {
+ for src in [
+ "OPENQASM 3.0;\nqubit[1] a;\nqubit[1] b;\nx a[0];\nx b[0];\n",
+ "OPENQASM 3.0;\nqubit[1] q;\nx q[0];\nreset q[0];\n",
+ "OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\nh q[0];\n",
+ "OPENQASM 3.0;\ngate flip a { x a; }\nqubit[1] q;\nflip q[0];\n",
+ "OPENQASM 3.0;\nqubit[2] q;\nh q[0]; cx q[0], q[1];\n",
+ ] {
+ let e = from_qasm(src).unwrap_err();
+ assert!(e.contains("wai_quantum::qasm"), "{src}: {e}");
+ }
+ // Measurements at the end are still read (and dropped, as they change nothing).
+ let c = from_qasm("OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n").unwrap();
+ assert_eq!(c.ops.len(), 1);
+ }
+
#[test]
fn qasm_round_trip() {
// Grover's search past three qubits uses a Z with three or more
diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock
index 890a68780..701ee84c4 100644
--- a/wai/wai-rs/Cargo.lock
+++ b/wai/wai-rs/Cargo.lock
@@ -4310,10 +4310,11 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
+ "serde",
"serde_json",
"wgpu",
]
diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml
index 4f7b8b82a..be10061f3 100644
--- a/wai/wai-rs/Cargo.toml
+++ b/wai/wai-rs/Cargo.toml
@@ -444,6 +444,11 @@ quantum_atom = ["quantum_ops", "wai-quantum/quantum_atom"]
# selection (Clifford → statevector → tensor-network cascade) + OpenQASM interop,
# all over the byte-exact fixed-point simulator. Framework-neutral, no cloud.
quantum_toolchain = ["quantum", "wai-quantum/quantum_toolchain"]
+# OpenQASM 3 read one way on every surface (wai_quantum::qasm).
+quantum_qasm = ["quantum", "wai-quantum/quantum_qasm"]
+# The job service's engine (wai_quantum::engine): plan, run and sign a job as the
+# service does, re-exported as wai::engine for the browser binding.
+quantum_engine = ["quantum_qasm", "quantum_receipt", "wai-quantum/quantum_engine"]
# Matrix Product State backend (extensions/quantum-sim § MPS, `wai.quantum.mps`):
# the tensor-network "classical referee" — evolve a bounded-bond MPS gate-by-gate
# with a deterministic complex Jacobi SVD truncation, reproducible on every machine
@@ -610,31 +615,6 @@ required-features = ["codecs"]
name = "wai"
path = "src/bin/wai.rs"
-# The quantum stack as a CLI. Pure Rust, no host bindings — the same source builds
-# for native AND `wasm32-wasip2`, producing byte-identical output under wasmtime.
-[[bin]]
-name = "wai-quantum"
-path = "src/bin/wai_quantum.rs"
-required-features = [
- "quantum_toolchain",
- "quantum_receipt",
- "quantum_qir",
- "quantum_channel",
- "quantum_comm",
- "quantum_qudit",
- "quantum_spd_receipt",
- "quantum_bptn",
- "quantum_resource_receipt",
- "quantum_frame",
- "quantum_tn",
- "quantum_gbs",
- "quantum_sv",
- "quantum_arch",
- "quantum_ldpc",
- "quantum_qio",
- "quantum_fabric",
-]
-
[[bin]]
name = "wai_int_synth_vectors"
path = "src/bin/wai_int_synth_vectors.rs"
diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs
index 0ddd880ed..b089bdbdb 100644
--- a/wai/wai-rs/src/lib.rs
+++ b/wai/wai-rs/src/lib.rs
@@ -775,6 +775,10 @@ pub use wai_quantum::quantum_atom;
// byte-exact fixed-point simulator. Framework-neutral and offline.
#[cfg(feature = "quantum_toolchain")]
pub use wai_quantum::quantum_toolchain;
+#[cfg(feature = "quantum_qasm")]
+pub use wai_quantum::qasm;
+#[cfg(feature = "quantum_engine")]
+pub use wai_quantum::engine;
// Matrix Product State backend (`wai.quantum.mps`) — the tensor-network classical
// referee: a bounded-bond MPS evolved with a deterministic complex Jacobi SVD
diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock
index 2bc7cea66..e900293eb 100644
--- a/wai/wai-web/Cargo.lock
+++ b/wai/wai-web/Cargo.lock
@@ -1241,10 +1241,11 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
-version = "0.6.9"
+version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
+ "serde",
"serde_json",
"wgpu",
]
diff --git a/wai/wai-web/Cargo.toml b/wai/wai-web/Cargo.toml
index 30b4bdc43..27d6dc846 100644
--- a/wai/wai-web/Cargo.toml
+++ b/wai/wai-web/Cargo.toml
@@ -19,7 +19,7 @@ js-sys = "0.3"
# with the codec chain off. No second implementation: replay-equivalence
# in the browser is the identical Rust that generated the conformance
# corpus hashes.
-wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
+wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_qasm", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs)
blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest
# Awaits the WebGPU member's futures behind the promises of src/quantum_webgpu.rs;
@@ -33,6 +33,11 @@ wasm-bindgen-futures = { version = "0.4.77", optional = true }
# --out-name (demo/quantum_webgpu.html); off by default, so the main bundle is
# unchanged.
quantum_webgpu = ["wai/quantum_fabric", "wai/quantum_fabric_webgpu", "dep:wasm-bindgen-futures"]
+# The job service's engine in a page (src/quantum_engine.rs): a job planned, run
+# and signed in the tab as the service runs it, its receipt checked there. Built
+# into the IDE's bundle (scripts/wasm-pkgs.tsv on the site), which the console
+# also loads; off by default, so the main bundle is unchanged.
+quantum_engine = ["wai/quantum_engine"]
[dev-dependencies]
# Pinned to the wasm-bindgen release Cargo.lock holds, so the test runner and the
diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs
index 1ceb23f63..fada30dd4 100644
--- a/wai/wai-web/src/lib.rs
+++ b/wai/wai-web/src/lib.rs
@@ -160,6 +160,11 @@ pub mod quantum_qbom;
#[cfg(all(feature = "quantum_webgpu", target_arch = "wasm32"))]
mod quantum_webgpu;
+// The job service's engine in the page: plan, run and sign a job in the tab, and
+// check any job's receipt there, the service's included. The IDE's bundle only.
+#[cfg(feature = "quantum_engine")]
+pub mod quantum_engine;
+
// Staged delivery (extensions/staged-delivery.md): wai::staged::StageSink, the sink a
// native player runs, bound for JS — bases added, refinement layers offered, holds
// expired and bases removed in any order, every unit's stage, producer, tier, output
diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs
new file mode 100644
index 000000000..ab3d04ee6
--- /dev/null
+++ b/wai/wai-web/src/quantum_engine.rs
@@ -0,0 +1,216 @@
+//! The quantum job service's engine in a page ([`wai::engine`]): a job
+//! planned, run and sealed in the tab exactly as the service plans, runs and
+//! seals it, with the page's own key; the receipt of any job, the service's
+//! included, checked in the tab; a program linted and drawn. Requests and
+//! answers are the service's JSON, so a page shows a job run here and one run
+//! there with the same code.
+
+use ed25519_dalek::SigningKey;
+use serde_json::{Value, json};
+use wai::engine::{catalog, draw, job};
+use wasm_bindgen::prelude::*;
+
+/// What a result run in a tab says of its energy.
+const UNMETERED: &str = "a browser tab does not meter a run's energy";
+
+fn error_json(e: impl std::fmt::Display) -> String {
+ json!({ "error": e.to_string() }).to_string()
+}
+
+fn read(request: &str) -> Result {
+ serde_json::from_str(request).map_err(|e| format!("the job is not JSON: {e}"))
+}
+
+/// The engine, with the key it signs with and the memory a job may take.
+#[wasm_bindgen]
+pub struct QuantumEngine {
+ key: SigningKey,
+ job_bytes: u64,
+}
+
+#[wasm_bindgen]
+impl QuantumEngine {
+ /// An engine that signs with the Ed25519 key whose 32-byte seed is
+ /// `seed` (the page makes one at random and keeps it in this browser),
+ /// and admits a job of at most `memory_mb` MiB.
+ #[wasm_bindgen(constructor)]
+ pub fn new(seed: &[u8], memory_mb: u32) -> Result {
+ let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?;
+ Ok(QuantumEngine { key: SigningKey::from_bytes(&bytes), job_bytes: u64::from(memory_mb.max(1)) << 20 })
+ }
+
+ /// The signer the receipts name: the key as a `did:key`.
+ pub fn signer_did(&self) -> String {
+ wai::quantum_receipt::did_key(&self.key.verifying_key())
+ }
+
+ /// Run a job (the service's request JSON): the job as the service
+ /// returns it, the request, the plan and the result with its receipt; or
+ /// `{"error"}` with why it did not run.
+ pub fn run(&self, request: &str) -> String {
+ match read(request).and_then(|req| job::run(&req, self.job_bytes, &self.key, json!({ "unmetered": UNMETERED }))) {
+ Ok(body) => body.to_string(),
+ Err(e) => error_json(e),
+ }
+ }
+
+ /// The method a job would run on and why, its memory and work, without
+ /// running it; or `{"error"}`.
+ pub fn plan(&self, request: &str) -> String {
+ match read(request).and_then(|req| catalog::plan(&req, self.job_bytes)) {
+ Ok(p) => serde_json::to_value(p).map(|v| v.to_string()).unwrap_or_else(error_json),
+ Err(e) => error_json(e),
+ }
+ }
+
+ /// Each method, its accuracy, what it takes, and the most qubits it holds
+ /// in a job's memory here.
+ pub fn methods(&self) -> String {
+ json!({ "methods": catalog::describe(self.job_bytes) }).to_string()
+ }
+}
+
+/// A program read as the engine reads it: `{"ok": true, ...}` with its shape,
+/// or `{"ok": false, "error", "line"}`.
+#[wasm_bindgen]
+pub fn engine_lint(src: &str) -> String {
+ job::lint(src).to_string()
+}
+
+/// A program's circuit as text, wrapped at `width` columns: `{"text"}`, or
+/// `{"error"}`.
+#[wasm_bindgen]
+pub fn engine_draw(src: &str, width: u32) -> String {
+ match wai::qasm::parse(src) {
+ Ok(p) => json!({ "text": draw::draw(&p, width.max(20) as usize) }).to_string(),
+ Err(e) => error_json(e),
+ }
+}
+
+/// Whether a job's receipt seals its request and its result, checked here:
+/// `{"valid", "key", "signer"}` (the signer as a `did:key`), or `{"error"}`.
+#[wasm_bindgen]
+pub fn engine_verify(job_json: &str) -> String {
+ let v: Value = match serde_json::from_str(job_json) {
+ Ok(v) => v,
+ Err(e) => return error_json(format!("not JSON: {e}")),
+ };
+ match job::verify(&v) {
+ Ok((valid, key)) => {
+ let signer = key.as_str().and_then(job::did_of);
+ json!({ "valid": valid, "key": key, "signer": signer }).to_string()
+ }
+ Err(e) => error_json(e),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn bell(extra: &str) -> String {
+ format!("{{\"qasm\": \"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];\"{extra}}}")
+ }
+
+ /// A job run in the tab is the service's form, signed with the page's key,
+ /// and its receipt checks there; a changed result does not.
+ #[test]
+ fn a_job_runs_signs_and_verifies_in_the_page() {
+ let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
+ let body: Value = serde_json::from_str(&e.run(&bell(", \"shots\": 500, \"observables\": [\"Z0 Z1\"]"))).unwrap();
+ assert_eq!(body["method"], "stabilizer", "{body}");
+ assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
+ assert_eq!(body["result"]["energy"]["unmetered"], UNMETERED);
+ let v: Value = serde_json::from_str(&engine_verify(&body.to_string())).unwrap();
+ assert_eq!(v["valid"], true, "{v}");
+ assert_eq!(v["signer"], e.signer_did());
+ let mut forged = body.clone();
+ forged["result"]["expectations"][0]["value"] = json!(-1.0);
+ let v: Value = serde_json::from_str(&engine_verify(&forged.to_string())).unwrap();
+ assert_eq!(v["valid"], false, "{v}");
+ }
+
+ /// Any angle runs, on the double-precision state vector.
+ #[test]
+ fn a_rotation_runs_on_the_state_vector() {
+ let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
+ let req = r#"{"qasm": "OPENQASM 3.0; qubit[1] q; rx(0.3) q[0];", "observables": ["Z0"]}"#;
+ let body: Value = serde_json::from_str(&e.run(req)).unwrap();
+ assert_eq!(body["method"], "statevector", "{body}");
+ assert!((body["result"]["expectations"][0]["value"].as_f64().unwrap() - 0.3f64.cos()).abs() < 1e-12, "{body}");
+ }
+
+ /// What the page cannot run says why: a job too large for the tab's
+ /// memory, a program the reader refuses, a request that is not JSON.
+ #[test]
+ fn what_does_not_run_says_why() {
+ let e = QuantumEngine::new(&[9u8; 32], 64).unwrap();
+ let wide = r#"{"qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[29];", "amplitudes": ["000000000000000000000000000000"], "method": "statevector"}"#;
+ let r: Value = serde_json::from_str(&e.run(wide)).unwrap();
+ assert!(r["error"].as_str().unwrap().contains("statevector holds at most"), "{r}");
+ let r: Value = serde_json::from_str(&e.run(r#"{"qasm": "qubit[1] q; foo q[0];"}"#)).unwrap();
+ assert!(r["error"].as_str().unwrap().contains("foo"), "{r}");
+ let r: Value = serde_json::from_str(&e.run("not json")).unwrap();
+ assert!(r["error"].as_str().unwrap().starts_with("the job is not JSON"), "{r}");
+ assert!(QuantumEngine::new(&[1u8; 31], 64).is_err());
+ }
+
+ #[test]
+ fn plan_lint_draw_and_methods_answer_as_json() {
+ let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
+ let p: Value = serde_json::from_str(&e.plan(&bell(""))).unwrap();
+ assert_eq!(p["method"], "stabilizer", "{p}");
+ let l: Value = serde_json::from_str(&engine_lint("qubit[1] q;\nfoo q[0];")).unwrap();
+ assert_eq!(l["line"], 2, "{l}");
+ let d: Value = serde_json::from_str(&engine_draw("qubit[2] q; h q[0]; cx q[0], q[1];", 80)).unwrap();
+ assert!(d["text"].as_str().unwrap().contains('⊕'), "{d}");
+ let m: Value = serde_json::from_str(&e.methods()).unwrap();
+ assert!(m["methods"].as_array().unwrap().iter().any(|x| x["method"] == "cutrank"), "{m}");
+ }
+}
+
+/// The engine compiled to wasm32 and run under Node, as a page runs it: no
+/// clock std can read and no threads, on every method a tab holds.
+#[cfg(all(test, target_arch = "wasm32"))]
+mod wasm_tests {
+ use super::*;
+ use wasm_bindgen_test::*;
+
+ fn run(e: &QuantumEngine, req: Value) -> Value {
+ serde_json::from_str(&e.run(&req.to_string())).unwrap()
+ }
+
+ #[wasm_bindgen_test]
+ fn a_job_runs_and_verifies_in_wasm() {
+ let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
+ let body = run(&e, json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 200 }));
+ assert_eq!(body["method"], "stabilizer", "{body}");
+ // A tab has no clock std reads, so no time is claimed.
+ assert!(body["result"].get("elapsed_ms").is_none(), "{body}");
+ let v: Value = serde_json::from_str(&engine_verify(&body.to_string())).unwrap();
+ assert_eq!(v["valid"], true, "{v}");
+ }
+
+ #[wasm_bindgen_test]
+ fn every_method_a_tab_holds_runs_in_wasm() {
+ let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
+ let ring = "OPENQASM 3.0; qubit[12] q; bit[12] c; h q; cz q[0], q[1]; cz q[1], q[2]; cz q[2], q[3]; cz q[3], q[4]; cz q[4], q[5]; cz q[5], q[6]; cz q[6], q[7]; cz q[7], q[8]; cz q[8], q[9]; cz q[9], q[10]; cz q[10], q[11]; t q; h q; c = measure q;";
+ let jobs = [
+ ("exact", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; h q[0]; t q[0]; cx q[0], q[1];", "shots": 100 })),
+ ("statevector", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; rx(0.3) q[0]; cx q[0], q[1];", "observables": ["Z0"] })),
+ ("stabilizer", json!({ "qasm": "OPENQASM 3.0; qubit[40] q; h q[0]; cx q[0], q[39];", "shots": 100 })),
+ ("mps", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "shots": 50, "method": "mps" })),
+ ("pauli", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "observables": ["Z0 Z1"], "method": "pauli" })),
+ ("tensor_network", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "amplitudes": ["0".repeat(30)], "method": "tensor_network" })),
+ ("cutrank", json!({ "qasm": ring, "shots": 20, "method": "cutrank" })),
+ ("density_matrix", json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 100, "noise": { "depolarizing_2q": 0.02 } })),
+ ("trajectories", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; h q[0]; cx q[0], q[1];", "shots": 100, "noise": { "depolarizing_1q": 0.01 }, "method": "trajectories" })),
+ ("statevector", json!({ "qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; rx(0.4) q[0]; c[0] = measure q[0]; if (c[0]) x q[1]; c[1] = measure q[1];", "shots": 100 })),
+ ];
+ for (want, req) in jobs {
+ let body = run(&e, req.clone());
+ assert_eq!(body["method"], want, "{req}: {body}");
+ assert_eq!(body["status"], "done", "{req}: {body}");
+ }
+ }
+}
diff --git a/wai/wai-web/src/quantum_toolchain.rs b/wai/wai-web/src/quantum_toolchain.rs
index 8756a19b5..2663ec2e8 100644
--- a/wai/wai-web/src/quantum_toolchain.rs
+++ b/wai/wai-web/src/quantum_toolchain.rs
@@ -14,15 +14,24 @@ use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, Stat
use wai::quantum_mps;
use wai::quantum_pauli;
use wai::quantum_receipt::QuantumReceipt;
+use wai::signer_id::did_key;
use wai::quantum_stabilizer;
use wai::quantum_toolchain as tc;
use wasm_bindgen::prelude::*;
const SCALE: f64 = (1u64 << FRAC) as f64;
-fn signer() -> SigningKey {
+/// The key a receipt is signed with before the page gives the IDE its own
+/// ([`QuantumIde::set_signing_key`]). It is public, so a receipt it signs
+/// proves nothing about who made it; [`QuantumIde::key_is_demo`] says so.
+fn demo_signer() -> SigningKey {
SigningKey::from_bytes(&[0x51; 32])
}
+/// `{"error": message}`, the message escaped as JSON requires.
+fn error_json(message: impl std::fmt::Display) -> String {
+ serde_json::json!({ "error": message.to_string() }).to_string()
+}
+
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
@@ -98,6 +107,8 @@ pub struct QuantumIde {
sv: Option,
/// Whether the console note has been printed for the circuit as it stands.
noted: bool,
+ /// The key receipts are signed with: this browser's, once the page sets it.
+ key: Option,
}
#[wasm_bindgen]
@@ -105,7 +116,30 @@ impl QuantumIde {
/// An empty circuit on `n_qubits` (1 to 12), in circuit2.
#[wasm_bindgen(constructor)]
pub fn new(n_qubits: u8) -> QuantumIde {
- QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false }
+ QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false, key: None }
+ }
+
+ // ---- signing key ----
+ /// Sign receipts with the Ed25519 key whose 32-byte seed is `seed`: the
+ /// page makes one at random and keeps it in this browser. Refuses any
+ /// other length.
+ pub fn set_signing_key(&mut self, seed: &[u8]) -> Result<(), String> {
+ let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?;
+ self.key = Some(SigningKey::from_bytes(&bytes));
+ Ok(())
+ }
+ /// The public key receipts are signed with, as hex.
+ pub fn public_key_hex(&self) -> String {
+ hex(self.signer().verifying_key().as_bytes())
+ }
+ /// The signer a receipt names: that public key as a `did:key`.
+ pub fn signer_did(&self) -> String {
+ did_key(self.signer().verifying_key().as_bytes())
+ }
+ /// Whether receipts are still signed with the published demo key, which
+ /// proves nothing about who made them.
+ pub fn key_is_demo(&self) -> bool {
+ self.key.is_none()
}
// ---- gateset ----
@@ -180,10 +214,20 @@ impl QuantumIde {
}
}
/// Replace the circuit from an OpenQASM source, built in the IDE's gateset.
- /// Returns an error string on failure (unsupported gate, non-dyadic phase,
- /// bad register, a gate that names one qubit twice).
+ /// The program is read as the job service reads it ([`wai::qasm`]): every
+ /// register, broadcast, gate definition and modifier. Returns an error
+ /// string on failure (an unsupported construct with its line, a
+ /// non-dyadic angle, more qubits than the page holds). A program that
+ /// measures in the middle, resets after a gate or acts on measured bits is
+ /// refused, never rewritten, with a pointer to the console that runs it.
pub fn load_qasm(&mut self, src: &str) -> Result<(), String> {
- let c = tc::from_qasm(src)?;
+ let c = wai::qasm::dyadic_circuit(src).map_err(|e| {
+ if e.starts_with("this program measures") {
+ format!("{e}. The console at /quantum-run/ runs it.")
+ } else {
+ e
+ }
+ })?;
if c.n_qubits > 12 {
return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits));
}
@@ -192,22 +236,13 @@ impl QuantumIde {
}
/// The op-log as JSON, for the editor to render.
pub fn gates_json(&self) -> String {
- let items: Vec = self
+ let items: Vec = self
.circuit
.ops
.iter()
- .map(|g| {
- let ctrls: Vec = g.controls.iter().map(|c| c.to_string()).collect();
- format!(
- "{{\"name\":\"{}\",\"controls\":[{}],\"target\":{},\"param\":{}}}",
- name_of(g.base),
- ctrls.join(","),
- g.target,
- g.param
- )
- })
+ .map(|g| serde_json::json!({ "name": name_of(g.base), "controls": g.controls, "target": g.target, "param": g.param }))
.collect();
- format!("[{}]", items.join(","))
+ serde_json::Value::Array(items).to_string()
}
// ---- run ----
@@ -314,13 +349,7 @@ impl QuantumIde {
pub fn transpile_line(&self) -> String {
let r = tc::route_line(&self.circuit);
let eq = tc::routing_equivalent(&self.circuit, &r);
- format!(
- "{{\"source_2q\":{},\"nonlocal\":{},\"routed_gates\":{},\"swaps\":{},\"equivalent\":{eq}}}",
- r.source_2q,
- r.source_nonlocal,
- r.circuit.ops.len(),
- r.swaps
- )
+ serde_json::json!({ "source_2q": r.source_2q, "nonlocal": r.source_nonlocal, "routed_gates": r.circuit.ops.len(), "swaps": r.swaps, "equivalent": eq }).to_string()
}
/// Run the circuit through the tensor-network (MPS) backend at bond cap
/// `chi_max` and report `{bond, retained, fidelity}` against the exact dense
@@ -328,17 +357,14 @@ impl QuantumIde {
pub fn run_mps(&self, chi_max: u32) -> String {
let dense = match self.circuit.simulate() {
Ok(d) => d,
- Err(e) => return format!("{{\"error\":\"{e}\"}}"),
+ Err(e) => return error_json(e),
};
match quantum_mps::run(&self.circuit, chi_max.max(1) as usize) {
Ok((mps, r)) => {
let f = quantum_mps::fidelity(&mps, &dense);
- format!(
- "{{\"bond\":{},\"retained\":{:.6},\"fidelity\":{:.6}}}",
- r.max_bond, r.retained_weight, f
- )
+ serde_json::json!({ "bond": r.max_bond, "retained": r.retained_weight, "fidelity": f }).to_string()
}
- Err(e) => format!("{{\"error\":\"{e:?}\"}}"),
+ Err(e) => error_json(format!("{e:?}")),
}
}
/// Sample the current circuit on the O(n²) stabilizer backend (if Clifford):
@@ -347,9 +373,9 @@ impl QuantumIde {
match quantum_stabilizer::sample(&self.circuit, seed as u64, shots.max(1) as u64) {
Ok(v) => {
let (o, c) = v.first().copied().unwrap_or((0, 0));
- format!("{{\"distinct\":{},\"top_index\":{o},\"top_count\":{c}}}", v.len())
+ serde_json::json!({ "distinct": v.len(), "top_index": o, "top_count": c }).to_string()
}
- Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
+ Err(e) => error_json(e.0),
}
}
/// Scale demonstration: build an `n`-qubit GHZ and sample it on the stabilizer
@@ -364,16 +390,16 @@ impl QuantumIde {
c.cx(i - 1, i);
}
match quantum_stabilizer::sample(&c, seed as u64, shots.max(1) as u64) {
- Ok(v) => format!("{{\"n\":{n},\"distinct\":{},\"gates\":{}}}", v.len(), c.ops.len()),
- Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
+ Ok(v) => serde_json::json!({ "n": n, "distinct": v.len(), "gates": c.ops.len() }).to_string(),
+ Err(e) => error_json(e.0),
}
}
/// Compute ⟨Z_q⟩ on the current circuit via sparse-Pauli / Heisenberg
/// propagation: `{value, peak}` (peak Pauli-sum size) or `{error}`.
pub fn expect_z_pauli(&self, q: u8, threshold: f64) -> String {
match quantum_pauli::expect_z(&self.circuit, q, threshold.max(0.0)) {
- Ok((v, peak)) => format!("{{\"value\":{v:.6},\"peak\":{peak}}}"),
- Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
+ Ok((v, peak)) => serde_json::json!({ "value": v, "peak": peak }).to_string(),
+ Err(e) => error_json(e.0),
}
}
/// Scale demonstration: ⟨Z_0⟩ on an `n`-qubit low-magic circuit — a single
@@ -401,8 +427,8 @@ impl QuantumIde {
}
}
match quantum_pauli::expect_z(&c, 0, threshold.max(0.0)) {
- Ok((v, peak)) => format!("{{\"n\":{n},\"value\":{v:.6},\"peak\":{peak}}}"),
- Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
+ Ok((v, peak)) => serde_json::json!({ "n": n, "value": v, "peak": peak }).to_string(),
+ Err(e) => error_json(e.0),
}
}
/// Seal a signed run receipt over the byte-exact reconstruction (and a shot
@@ -445,10 +471,14 @@ impl QuantumIde {
}
}
+ fn signer(&self) -> SigningKey {
+ self.key.clone().unwrap_or_else(demo_signer)
+ }
+
fn seal(&self, seed: u64, shots: u64) -> Option {
QuantumReceipt::seal(
- &signer(),
- "did:key:quantum-ide",
+ &self.signer(),
+ did_key(self.signer().verifying_key().as_bytes()),
&self.circuit,
Some(Measure { seed, shots }),
0,
@@ -461,11 +491,8 @@ impl QuantumIde {
/// The algorithm library catalog as JSON `[{id,name,desc}]`, for the IDE dropdown.
#[wasm_bindgen]
pub fn ide_catalog() -> String {
- let items: Vec = tc::catalog()
- .into_iter()
- .map(|(id, name, desc)| format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"desc\":\"{desc}\"}}"))
- .collect();
- format!("[{}]", items.join(","))
+ let items: Vec = tc::catalog().into_iter().map(|(id, name, desc)| serde_json::json!({ "id": id, "name": name, "desc": desc })).collect();
+ serde_json::Value::Array(items).to_string()
}
#[cfg(test)]
@@ -600,4 +627,82 @@ mod tests {
assert!(ide.set_gateset(2));
assert_eq!(ide.note(), "");
}
+
+ fn loaded(src: &str) -> QuantumIde {
+ let mut ide = QuantumIde::new(2);
+ ide.load_qasm(src).unwrap_or_else(|e| panic!("{src}: {e}"));
+ ide.run().unwrap();
+ ide
+ }
+
+ /// The outcome with probability 1, or None.
+ fn certain(ide: &QuantumIde) -> Option {
+ let p = ide.probabilities();
+ p.iter().position(|&x| (x - 1.0).abs() < 1e-9)
+ }
+
+ /// OpenQASM means what it says: registers keep their own qubits, broadcast,
+ /// comments, single-qubit declarations and gate definitions are read, and a
+ /// program whose meaning needs its measurements is refused, not rewritten.
+ #[test]
+ fn imported_programs_mean_what_they_say() {
+ // Two registers are two qubits: |11⟩.
+ let ide = loaded("OPENQASM 3.0; qubit[1] a; qubit[1] b; x a[0]; x b[0];");
+ assert_eq!(ide.circuit.n_qubits, 2);
+ assert_eq!(certain(&ide), Some(3));
+ // OpenQASM 2's registers too: three qubits, one of them flipped.
+ let ide = loaded("OPENQASM 2.0; include \"qelib1.inc\"; qreg a[2]; qreg b[1]; x b[0];");
+ assert_eq!(ide.circuit.n_qubits, 3);
+ assert_eq!(certain(&ide).map(|i| i.count_ones()), Some(1));
+ // Broadcast, a single-qubit declaration, a block comment, a definition.
+ let ide = loaded("OPENQASM 3.0; qubit[2] q; /* both */ h q;");
+ assert!(ide.probabilities().iter().all(|&p| (p - 0.25).abs() < 1e-9), "{:?}", ide.probabilities());
+ let ide = loaded("OPENQASM 3.0; qubit q; x q;");
+ assert_eq!((ide.circuit.n_qubits, certain(&ide)), (1, Some(1)));
+ let ide = loaded("OPENQASM 3.0; gate flip a { x a; } qubit[1] q; flip q[0];");
+ assert_eq!(certain(&ide), Some(1));
+ // A final measurement leaves the state as it was.
+ let ide = loaded("OPENQASM 3.0; qubit[1] q; bit[1] c; x q[0]; c[0] = measure q[0];");
+ assert_eq!(certain(&ide), Some(1));
+ // A measurement acted on afterwards, or a reset after a gate, is refused.
+ let mut ide = QuantumIde::new(2);
+ for src in ["OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c[0] = measure q[0]; h q[0];", "OPENQASM 3.0; qubit[1] q; x q[0]; reset q[0];"] {
+ let e = ide.load_qasm(src).unwrap_err();
+ assert!(e.contains("measures a qubit and then acts on it") && e.contains("/quantum-run/"), "{src}: {e}");
+ }
+ // A missing semicolon is an error with its line, as in the service.
+ assert!(ide.load_qasm("OPENQASM 3.0;\nqubit[2] q\nh q[0];").is_err());
+ }
+
+ /// Every JSON the panels read parses, whatever an error message holds.
+ #[test]
+ fn the_panels_get_json_that_parses() {
+ let mut ide = QuantumIde::new(8);
+ assert!(ide.load_algorithm("grover", 8, 0));
+ ide.run().unwrap();
+ for text in [ide.run_mps(8), ide.transpile_line(), ide.sample_stabilizer(1.0, 16), ide.stabilizer_scale(20, 1.0, 16), ide.expect_z_pauli(0, 1e-6), ide.pauli_scale(8, 1e-4), ide.gates_json(), super::ide_catalog()] {
+ serde_json::from_str::(&text).unwrap_or_else(|e| panic!("{text}: {e}"));
+ }
+ assert_eq!(super::error_json("say \"no\" \\ here"), r#"{"error":"say \"no\" \\ here"}"#);
+ }
+
+ /// Receipts are signed with the key the page gives, and say when they are
+ /// not.
+ #[test]
+ fn receipts_use_the_browsers_key() {
+ let mut ide = QuantumIde::new(2);
+ assert!(ide.load_algorithm("bell", 2, 0));
+ ide.run().unwrap();
+ assert!(ide.key_is_demo());
+ let demo = ide.public_key_hex();
+ ide.set_signing_key(&[7u8; 32]).unwrap();
+ assert!(!ide.key_is_demo());
+ assert_ne!(ide.public_key_hex(), demo);
+ assert!(ide.receipt_verifies(1.0, 64));
+ let r: serde_json::Value = serde_json::from_str(&ide.receipt_json(1.0, 64)).unwrap();
+ assert_eq!(r["signer_pubkey"], ide.public_key_hex());
+ assert_eq!(r["signer_id"], ide.signer_did());
+ assert!(ide.signer_did().starts_with("did:key:z6Mk"));
+ assert!(ide.set_signing_key(&[1u8; 31]).is_err());
+ }
}