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 2e9d37386..279a575d7 100644
--- a/wai/CHANGELOG.md
+++ b/wai/CHANGELOG.md
@@ -6,6 +6,72 @@ 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.
+- **Quantum Studio** (`wai/quantum-studio`) is one interface for the browser and the desktop app (`wai/wai-quantum-desktop`). Its editor:
+ - highlights and completes OpenQASM;
+ - marks the engine's diagnostics on their lines;
+ - draws the circuit and shows the plan as the program is typed.
+
+ It runs a job in the browser, on the desktop's native engine, or on the service, and checks each receipt where it runs. The desktop app runs each job in a process of its own and seals it with the user's key.
+
+ Its assistant uses a local model with the engine as its tools. It checks and runs every program the model proposes before offering it, and marks any figure the engine did not compute.
+- **`engine::job`** also splits into `plan_and_run` and `seal`, for hosts that run a job away from their key.
+- **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.live.watermark`: forensic watermark sessions, matched and disclosed one at a time
- **The extension** ([forensic-watermark](extensions/forensic-watermark.md)) registers the
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..2ae1f6abb
--- /dev/null
+++ b/wai/quantum-studio/README.md
@@ -0,0 +1,75 @@
+# 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.
+
+## The assistant
+
+The Assistant pane talks to a language model on this machine: any server that speaks the OpenAI chat API with tool calls, such as the JouleClaw runtime or a llama.cpp server. The address goes under "Model…" and is `http://127.0.0.1:8080/v1` by default. Nothing leaves the machine unless the address points elsewhere. In the desktop app the app makes the call itself, so no browser origin rule stands in the way.
+
+The engine is the model's tool, not an afterthought:
+
+- **Its tools are the Studio's engine:** read the program, lint, draw, plan and run. Its first turn must call one of them, so every answer starts from the engine.
+- **Every program it proposes is checked before it is offered.** The engine reads it here. One that does not parse goes back to the model once, with the engine's reason, to be corrected. One that parses is run, with the settings the model used, so the counts it quoted are the counts shown. Then "Apply" puts it in the editor.
+- **It may state only figures the engine returned.** A figure in its prose that no tool returned is marked "Not from a run".
+- **Answers are plain text.** LaTeX a model writes anyway is shown as Unicode.
+
+`node test/assistant-live.mjs URL` drives the assistant against a real model server and prints what it did. It is not part of CI, which has no model.
+
+## 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-embedded` tells the Studio the host page has its own heading, so the Studio's name is not a second `h1`. `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 assistant, against a scripted model server: a tool call run on the engine, a proposal the engine cannot read sent back with its reason, the corrected one checked and run before it is offered, and Apply;
+- 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/assistant.js b/wai/quantum-studio/src/assistant.js
new file mode 100644
index 000000000..4e218ce4f
--- /dev/null
+++ b/wai/quantum-studio/src/assistant.js
@@ -0,0 +1,232 @@
+// The Studio's assistant: a language model on this machine, with the engine
+// as its tools. The model is any server that speaks the OpenAI chat API, tool
+// calls included: the JouleClaw runtime, a llama.cpp server, others. By default
+// it is local, and nothing leaves the machine.
+//
+// Tools first: the model asks the engine (lint, draw, plan, run) and may state
+// only numbers the engine returned. Every program it proposes is checked by the
+// engine before the Studio offers it: read as the engine reads it, and run on
+// this device. A proposal that does not parse goes back to the model once, with
+// the engine's reason, to be corrected.
+
+const SYSTEM = `You are the assistant in Quantum Studio, an editor for OpenQASM programs.
+Tools run the Studio's own engine:
+- get_program: the program in the editor, the job's settings and the last result.
+- lint_qasm: read a program as the engine reads it (its qubits and gates, or the first problem and its line).
+- draw_circuit: the circuit as text.
+- plan_job: the method that would run a job, why, and the memory it needs.
+- run_job: run a job on this device; counts, expectation values, amplitudes and the accuracy of each.
+Use them. Never state a probability, count, amplitude, expectation value or cost that a tool did not return; quote the tool's figure.
+The engine reads OpenQASM 3 and the OpenQASM 2 forms in common use: registers, the standard gates, gate definitions, ctrl and negctrl modifiers, measurement and reset anywhere, and if on measured bits. It refuses loops, inv and pow.
+When you write a program, run it with run_job and also show the whole program in one \`\`\`qasm block; the Studio checks it and runs it before offering it.
+For a state, a probability or an amplitude, run the program and quote the result rather than working it out yourself.
+Write plain text: no Markdown headings, no LaTeX. Write states as |00⟩ and roots as √2. Qubit 0 is the rightmost bit of a basis state.
+Answer briefly.`;
+
+export const TOOLS = [
+ { type: 'function', function: { name: 'get_program', description: 'The program in the editor, the job settings, and a summary of the last result.', parameters: { type: 'object', properties: {} } } },
+ { type: 'function', function: { name: 'lint_qasm', description: 'Read an OpenQASM program as the engine reads it: its qubits, gates and measurements, or the first problem with its line.', parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' } } } } },
+ { type: 'function', function: { name: 'draw_circuit', description: 'A program\'s circuit drawn as text.', parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' } } } } },
+ {
+ type: 'function',
+ function: {
+ name: 'plan_job',
+ description: 'The method a job would run on and why, with the memory it needs, without running it.',
+ parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' }, shots: { type: 'integer' }, observables: { type: 'array', items: { type: 'string' } }, method: { type: 'string' } } },
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'run_job',
+ description: 'Run a job on this device: measurement counts, Pauli expectation values (as "Z0 Z1") and/or amplitudes (bit strings, highest qubit first), with the method used and its accuracy.',
+ parameters: {
+ type: 'object',
+ required: ['qasm'],
+ properties: { qasm: { type: 'string' }, shots: { type: 'integer' }, observables: { type: 'array', items: { type: 'string' } }, amplitudes: { type: 'array', items: { type: 'string' } }, method: { type: 'string' } },
+ },
+ },
+ },
+];
+
+const clip = (s, n = 6000) => (s.length > n ? s.slice(0, n) + `… (${s.length - n} more characters)` : s);
+
+// A result as a model reads it: what was computed, not the receipt.
+export function summary(job) {
+ const r = job?.result || {};
+ const counts = r.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 16) : undefined;
+ return {
+ method: job?.method, why: job?.why, accuracy: r.accuracy, qubits: r.qubits, gates: r.gates,
+ counts: counts && Object.fromEntries(counts), distinct_outcomes: r.distinct_outcomes,
+ expectations: r.expectations, amplitudes: r.amplitudes?.slice(0, 16), dynamic: r.dynamic, warning: r.warning,
+ };
+}
+
+// The ```qasm (or ```openqasm) blocks of an answer.
+export function proposals(text) {
+ const out = [];
+ const re = /```(?:qasm|openqasm|openqasm3)?\s*\n([\s\S]*?)```/gi;
+ for (let m; (m = re.exec(text)); ) if (/\b(qubit|qreg|OPENQASM)\b/.test(m[1])) out.push(m[1].trim() + '\n');
+ return out;
+}
+
+// A model behind an OpenAI-compatible endpoint, called from this page.
+export class HttpModel {
+ constructor(endpoint, name) { this.endpoint = endpoint.replace(/\/+$/, ''); this.name = name; }
+ async chat(body) {
+ const res = await fetch(`${this.endpoint}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+ const v = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(v.error?.message || v.error || `the model answered ${res.status}`);
+ return v;
+ }
+ async models() {
+ const res = await fetch(`${this.endpoint}/models`);
+ if (!res.ok) throw new Error(`the model server answered ${res.status}`);
+ return (await res.json()).data?.map((m) => m.id) ?? [];
+ }
+}
+
+// The same, called by the desktop app (no browser origin rules in the way).
+export class DesktopModel {
+ constructor(endpoint, name) { this.endpoint = endpoint.replace(/\/+$/, ''); this.name = name; this.invoke = window.__TAURI__.core.invoke; }
+ async chat(body) {
+ const v = JSON.parse(await this.invoke('chat', { endpoint: this.endpoint, body: JSON.stringify(body) }));
+ if (v.error && !v.choices) throw new Error(v.error.message || v.error);
+ return v;
+ }
+ async models() {
+ const v = JSON.parse(await this.invoke('models', { endpoint: this.endpoint }));
+ if (v.error && !v.data) throw new Error(v.error.message || v.error);
+ return v.data?.map((m) => m.id) ?? [];
+ }
+}
+
+export class Assistant {
+ // engine: the Studio's local engine; context(): {qasm, job, last} as the editor holds them.
+ constructor({ engine, model, context }) {
+ this.engine = engine;
+ this.model = model;
+ this.context = context;
+ this.history = [];
+ }
+
+ async tool(name, args) {
+ const ctx = this.context();
+ const job = (a) => {
+ const j = { qasm: a.qasm };
+ for (const k of ['shots', 'observables', 'amplitudes', 'method']) if (a[k] != null && !(Array.isArray(a[k]) && !a[k].length)) j[k] = a[k];
+ if (!j.shots && !j.observables && !j.amplitudes) j.shots = 1000;
+ return j;
+ };
+ switch (name) {
+ case 'get_program': return { qasm: ctx.qasm, job: ctx.job, last_result: ctx.last ? summary(ctx.last) : null };
+ case 'lint_qasm': return this.engine.lint(args.qasm ?? ctx.qasm);
+ case 'draw_circuit': return { text: (await this.engine.draw(args.qasm ?? ctx.qasm, 80)).text };
+ case 'plan_job': return this.engine.plan(job({ ...args, qasm: args.qasm ?? ctx.qasm }));
+ case 'run_job': return summary(await this.engine.run(job({ ...args, qasm: args.qasm ?? ctx.qasm })));
+ default: throw new Error(`no tool ${name}`);
+ }
+ }
+
+ // Ask; `on(event)` hears each step: {type: 'tool', name, args, result} and
+ // finally {type: 'answer', text, proposals: [{qasm, lint, job}]}.
+ async ask(text, on = () => {}) {
+ const ctx = this.context();
+ const used = []; // programs the model passed to the tools, other than the editor's
+ const ranWith = new Map(); // program → the settings the model ran it with
+ const heard = [ctx.qasm, JSON.stringify(ctx.job ?? {}), JSON.stringify(ctx.last ? summary(ctx.last) : {})]; // what the engine and the editor told it
+ const messages = [
+ { role: 'system', content: SYSTEM },
+ ...this.history,
+ { role: 'user', content: `${text}\n\n(The program in the editor:)\n\`\`\`qasm\n${ctx.qasm}\n\`\`\`` },
+ ];
+ let retried = false;
+ for (let round = 0; round < 8; round++) {
+ // The first turn must ask the engine something, so every answer starts from it.
+ const res = await this.model.chat({ model: this.model.name || undefined, messages, tools: TOOLS, tool_choice: round === 0 ? 'required' : 'auto', temperature: 0.2 });
+ const msg = res.choices?.[0]?.message;
+ if (!msg) throw new Error('the model gave no answer');
+ messages.push({ role: 'assistant', content: msg.content ?? '', ...(msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}) });
+ if (msg.tool_calls?.length) {
+ for (const call of msg.tool_calls) {
+ let args = {};
+ try { args = JSON.parse(call.function?.arguments || '{}'); } catch (e) {}
+ let result;
+ try { result = await this.tool(call.function?.name, args); } catch (e) { result = { error: e.message }; }
+ if (typeof args.qasm === 'string' && args.qasm.trim() && args.qasm.trim() !== ctx.qasm.trim()) used.push(args.qasm.trim() + '\n');
+ if (call.function?.name === 'run_job') {
+ const eff = pick(args, ['shots', 'observables', 'amplitudes', 'method']);
+ if (!eff.shots && !eff.observables && !eff.amplitudes) eff.shots = 1000; // as the tool ran it
+ ranWith.set((args.qasm ?? ctx.qasm).trim(), eff);
+ }
+ on({ type: 'tool', name: call.function?.name, args, result });
+ messages.push({ role: 'tool', tool_call_id: call.id, content: clip(JSON.stringify(result)) });
+ heard.push(JSON.stringify(args), JSON.stringify(result)); // what it asked, and what it was told
+ }
+ continue;
+ }
+ const answer = unlatex((msg.content || '').replace(/[\s\S]*?<\/think>\s*/g, '').trim());
+ // Check each proposal as the engine reads it; a broken one goes back once.
+ // The answer's programs, and the last one the model ran or checked through a tool
+ // when the answer shows none: either way, read by the engine here.
+ const written = proposals(answer);
+ const shown = written.length ? written : used.slice(-1);
+ const checked = [];
+ for (const qasm of shown) checked.push({ qasm, lint: await this.engine.lint(qasm) });
+ const broken = checked.find((p) => !p.lint.ok);
+ if (broken && !retried) {
+ retried = true;
+ on({ type: 'retry', error: broken.lint.error });
+ messages.push({ role: 'user', content: `The engine could not read the program you proposed: ${broken.lint.error}. Give the corrected whole program in one \`\`\`qasm block.` });
+ continue;
+ }
+ for (const p of checked) {
+ if (!p.lint.ok) continue;
+ // Run as the model ran it, so the counts it quoted are the counts shown; else
+ // with the editor's settings.
+ const theirs = ranWith.get(p.qasm.trim());
+ const settings = theirs ?? { ...(ctx.job?.shots ? {} : { shots: 1000 }), ...pick(ctx.job, ['shots', 'observables']) };
+ try { p.job = await this.engine.run({ qasm: p.qasm, ...settings }); } catch (e) { p.error = e.message; }
+ }
+ this.history.push({ role: 'user', content: text }, { role: 'assistant', content: answer });
+ this.history = this.history.slice(-12);
+ on({ type: 'answer', text: answer, proposals: checked, usage: res.usage, unsourced: unsourced(answer, heard.join(' ')) });
+ return;
+ }
+ throw new Error('the model kept calling tools without answering');
+ }
+}
+
+// The figures in an answer's prose that no tool returned and the program does not
+// hold: a decimal, a percentage, or a whole number of two digits or more.
+export function unsourced(answer, heard) {
+ const prose = answer.replace(/```[\s\S]*?```/g, ' ');
+ const figures = prose.match(/(? ({ Phi: 'Φ', Psi: 'Ψ', psi: 'ψ', phi: 'φ', theta: 'θ', pi: 'π', alpha: 'α', beta: 'β', gamma: 'γ', otimes: '⊗', cdot: '·', pm: '±' })[w])
+ .replace(/\^\{?\+\}?/g, '⁺').replace(/\^\{?-\}?/g, '⁻')
+ .replace(/\\(left|right)/g, '');
+}
+
+function pick(o, keys) {
+ const out = {};
+ for (const k of keys) if (o && o[k] != null && !(Array.isArray(o[k]) && !o[k].length)) out[k] = o[k];
+ return out;
+}
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..8102d079e
--- /dev/null
+++ b/wai/quantum-studio/src/main.js
@@ -0,0 +1,513 @@
+// 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';
+import { Assistant, HttpModel, DesktopModel } from './assistant.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();
+ // A host page with a heading of its own mounts the Studio with data-embedded, and its name is not a second h1.
+ const embedded = 'embedded' in root.dataset;
+ 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(embedded ? 'span' : 'h1', { class: 'qs-title', 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);
+
+ // ---- the assistant ----
+ const AI_ITEM = 'wai.quantum-studio.assistant';
+ const aiConf = { endpoint: 'http://127.0.0.1:8080/v1', model: '' };
+ try { Object.assign(aiConf, JSON.parse(localStorage.getItem(AI_ITEM) || '{}')); } catch (e) {}
+ const [aiEndpoint, aiEndpointF] = field('qs-ai-endpoint', 'Model server', { value: aiConf.endpoint, spellcheck: 'false', autocomplete: 'off' });
+ const [aiModel, aiModelF] = field('qs-ai-model', 'Model', { value: aiConf.model, placeholder: 'the server’s own', spellcheck: 'false', autocomplete: 'off' });
+ const aiState = el('span', { class: 'qs-badge', id: 'qs-ai-state', text: 'not connected' });
+ const aiConnect = el('button', { class: 'qs-btn qs-small', id: 'qs-ai-connect', type: 'button', text: 'Connect' });
+ const aiSettings = el('div', { class: 'qs-ai-settings', id: 'qs-ai-settings', hidden: true },
+ el('div', { class: 'qs-row' }, aiEndpointF, aiModelF),
+ el('div', { class: 'qs-actions' }, aiConnect),
+ el('p', { class: 'qs-note', text: 'A model on this machine by default: any server that speaks the OpenAI chat API with tool calls. Nothing leaves this machine unless the address points elsewhere.' }));
+ const aiLog = el('ol', { class: 'qs-ai-log', id: 'qs-ai-log', 'aria-live': 'polite' },
+ el('li', { class: 'qs-note', text: 'Ask about the program. The assistant uses the engine to check what it says: every number comes from a run, and every program it proposes is read and run here before it is offered.' }));
+ const aiInput = el('textarea', { class: 'qs-input qs-ai-input', id: 'qs-ai-input', rows: '2', placeholder: 'Ask about this program…', 'aria-label': 'Ask the assistant' });
+ const aiSend = el('button', { class: 'qs-btn qs-primary', id: 'qs-ai-send', type: 'button', text: 'Ask' });
+ const QUICK = [
+ ['Explain this circuit', 'Explain what this program does, step by step, and what its result shows.'],
+ ['Why this method?', 'Why does the engine pick this method for this job, and what would make it pick another?'],
+ ['Fix the problem', 'The program has a problem. Find it with the tools and give the corrected whole program.'],
+ ];
+ const aiPane = el('section', { class: 'qs-pane qs-ai', 'aria-labelledby': 'qs-ai-h' },
+ el('div', { class: 'qs-head' }, el('h2', { id: 'qs-ai-h', text: 'Assistant' }), aiState, el('span', { class: 'qs-grow' }),
+ el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-ai-gear', text: 'Model…', 'aria-controls': 'qs-ai-settings', onclick: () => { aiSettings.hidden = !aiSettings.hidden; } })),
+ aiSettings,
+ el('div', { class: 'qs-ai-quick' }, ...QUICK.map(([label, q]) => el('button', { class: 'qs-btn qs-small', type: 'button', text: label, onclick: () => ask(q, label) }))),
+ aiLog,
+ el('div', { class: 'qs-ai-ask' }, aiInput, aiSend));
+
+ 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, aiPane, 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);
+
+ // ---- asking ----
+ const model = () => {
+ const endpoint = aiEndpoint.value.trim(), name = aiModel.value.trim();
+ try { localStorage.setItem(AI_ITEM, JSON.stringify({ endpoint, model: name })); } catch (e) {}
+ return desktop ? new DesktopModel(endpoint, name) : new HttpModel(endpoint, name);
+ };
+ let assistant = null;
+ const assistantFor = () => {
+ const m = model();
+ if (!assistant || assistant.model.endpoint !== m.endpoint || assistant.model.name !== m.name) {
+ assistant = new Assistant({ engine: local, model: m, context: () => ({ qasm: text(), job: request(), last: current?.job }) });
+ }
+ return assistant;
+ };
+ const say = (cls, ...kids) => { const li = el('li', { class: cls }, ...kids); aiLog.append(li); li.scrollIntoView({ block: 'nearest' }); return li; };
+ // An answer's text, its code in blocks.
+ // **bold** and `code` within a line, built as nodes, never as HTML.
+ const inline = (line) => line.split(/(\*\*[^*]+\*\*|`[^`]+`)/).filter(Boolean).map((t) =>
+ t.startsWith('**') && t.endsWith('**') ? el('strong', { text: t.slice(2, -2) }) : t.startsWith('`') && t.endsWith('`') ? el('code', { text: t.slice(1, -1) }) : document.createTextNode(t.replace(/\$([^$]+)\$/g, '$1')));
+ const prose = (t) => t.split(/```[a-z0-9]*\n?/i).map((part, i) => (i % 2 ? el('pre', { class: 'qs-ai-code', text: part.replace(/\n$/, '') }) : part.trim() ? el('p', { class: 'qs-ai-text' }, ...part.trim().split('\n').flatMap((l, j) => [j ? el('br') : null, ...inline(l)])) : null));
+ async function ask(q, label = q) {
+ if (aiSend.disabled) return;
+ aiSend.disabled = true;
+ say('qs-ai-user', el('p', { text: label }));
+ const thinking = say('qs-ai-tool', el('span', { class: 'qs-note', text: 'thinking…' }));
+ try {
+ await assistantFor().ask(q, (ev) => {
+ if (ev.type === 'tool') {
+ const res = JSON.stringify(ev.result);
+ aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('details', {}, el('summary', { text: `used ${ev.name}` }), el('pre', { class: 'qs-ai-code', text: res.length > 2000 ? res.slice(0, 2000) + '…' : res }))), thinking);
+ } else if (ev.type === 'retry') {
+ aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('span', { class: 'qs-note', text: `The engine could not read the proposed program (${ev.error}); asked for a correction.` })), thinking);
+ } else if (ev.type === 'answer') {
+ thinking.remove();
+ const li = say('qs-ai-answer', ...prose(ev.text));
+ for (const p of ev.proposals) {
+ const box = el('div', { class: 'qs-ai-proposal' });
+ if (!p.lint.ok) box.append(el('span', { class: 'qs-badge bad', text: 'does not parse' }), el('span', { class: 'qs-note', text: ' ' + p.lint.error }));
+ else {
+ const r = p.job?.result;
+ const counts = r?.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, n]) => `${compact(k)}: ${n}`).join(', ') : '';
+ box.append(el('span', { class: 'qs-badge ok', text: `checked: ${p.lint.qubits} qubits, ${p.lint.gates} gates` }),
+ el('span', { class: 'qs-note', text: p.job ? ` ran here on ${METHOD_NAMES[p.job.method] || p.job.method}${counts ? '; ' + counts : ''}` : p.error ? ` did not run: ${p.error}` : '' }),
+ el('button', { class: 'qs-btn qs-small qs-ai-apply', type: 'button', text: 'Apply', onclick: () => { setText(p.qasm); shows.textContent = 'The assistant’s program, checked and run before it was offered.'; setStatus('Applied the assistant’s program.', 'ok'); } }));
+ }
+ li.append(box);
+ }
+ if (ev.unsourced?.length) li.append(el('p', { class: 'qs-ai-unsourced', text: `Not from a run: ${ev.unsourced.join(', ')}. The model gave these itself; the engine did not compute them.` }));
+ if (ev.usage?.total_tokens) li.append(el('p', { class: 'qs-note', text: `${ev.usage.prompt_tokens ?? '?'} tokens in, ${ev.usage.completion_tokens ?? '?'} out` }));
+ }
+ });
+ } catch (e) {
+ thinking.remove();
+ say('qs-ai-error', el('p', { text: `The assistant could not answer: ${e.message}. Check the model server under “Model…”.` }));
+ aiState.textContent = 'not connected'; aiState.className = 'qs-badge bad';
+ } finally { aiSend.disabled = false; }
+ }
+ aiSend.addEventListener('click', () => { const q = aiInput.value.trim(); if (q) { aiInput.value = ''; ask(q); } });
+ aiInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); aiSend.click(); } });
+ aiConnect.addEventListener('click', async () => {
+ aiState.textContent = 'connecting…'; aiState.className = 'qs-badge run';
+ try {
+ const ids = await model().models();
+ const name = ids[0] ? ids[0].split(/[\\/]/).pop().replace(/\.gguf$/i, '') : '';
+ aiState.textContent = `connected${name ? ' · ' + name : ''}`; aiState.className = 'qs-badge ok'; aiState.title = ids[0] || '';
+ if (!aiModel.value && ids[0]) aiModel.value = ids[0];
+ } catch (e) { aiState.textContent = 'not connected'; aiState.className = 'qs-badge bad'; aiState.title = e.message; }
+ });
+
+ // ---- 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..bc8f814ef
--- /dev/null
+++ b/wai/quantum-studio/src/studio.css
@@ -0,0 +1,119 @@
+/* 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 .qs-title { margin: 0; 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); }
+.qs-ai { min-height: 200px; }
+.qs-ai-settings { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); }
+.qs-ai-settings[hidden] { display: none; }
+.qs-ai-quick { display: flex; gap: 6px; flex-wrap: wrap; padding: 8px 12px 0; }
+.qs-ai-log { list-style: none; margin: 0; padding: 8px 12px; display: flex; flex-direction: column; gap: 8px; max-height: 420px; overflow: auto; }
+.qs-ai-log > li { font-size: 13px; line-height: 1.5; }
+.qs-ai-log p { margin: 0; }
+.qs-ai-user { align-self: flex-end; max-width: 85%; padding: 6px 10px; border-radius: 8px; background: color-mix(in srgb, var(--accent) 14%, var(--bg-base)); }
+.qs-ai-answer { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-base); }
+.qs-ai-text { white-space: pre-wrap; }
+.qs-ai-tool { font-size: 12px; color: var(--text-secondary); }
+.qs-ai-tool summary { cursor: pointer; font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; color: var(--text-muted); }
+.qs-ai-error { color: var(--bad-text); }
+.qs-ai-unsourced { font-size: 12px; color: var(--warn-text); }
+.qs-ai-code { margin: 0; padding: 8px 10px; border-radius: 6px; background: var(--bg-surface); border: 1px solid var(--border); font: 12px/1.4 var(--font-mono, ui-monospace, monospace); overflow: auto; white-space: pre; }
+.qs-ai-proposal { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 4px; }
+.qs-ai-ask { display: flex; gap: 8px; align-items: flex-end; padding: 8px 12px 12px; border-top: 1px solid var(--border); }
+.qs-ai-input { min-height: 40px; resize: vertical; font-family: var(--font-sans, system-ui, sans-serif); font-size: 13px; }
+@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/assistant-live.mjs b/wai/quantum-studio/test/assistant-live.mjs
new file mode 100644
index 000000000..bac797215
--- /dev/null
+++ b/wai/quantum-studio/test/assistant-live.mjs
@@ -0,0 +1,57 @@
+// The assistant against a real model server, as a reader uses it: not part of CI, which
+// has no model. Prints what the model did and how long it took.
+//
+// node build.mjs --pkg PATH/TO/PKG && node test/assistant-live.mjs http://127.0.0.1:8080/v1
+//
+// Asks it to explain the Bell example, then to write a three-qubit GHZ program; each
+// program it proposes is checked and run by the engine before it is shown, as always.
+import { createServer } from 'node:http';
+import { existsSync, readFileSync, statSync } from 'node:fs';
+import { dirname, extname, join, normalize, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const endpoint = process.argv[2] ?? 'http://127.0.0.1:8080/v1';
+const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
+const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
+const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm' };
+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(); 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 browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
+const page = await (await browser.newContext({ viewport: { width: 1400, height: 1000 } })).newPage();
+await page.goto(`http://127.0.0.1:${server.address().port}/`, { waitUntil: 'domcontentloaded' });
+await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: 120_000 });
+await page.selectOption('#qs-example', 'bell');
+await page.click('#qs-ai-gear');
+await page.fill('#qs-ai-endpoint', endpoint);
+await page.click('#qs-ai-connect');
+await page.waitForFunction(() => !/connecting/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: 60_000 });
+console.log('model:', await page.textContent('#qs-ai-state'));
+
+async function ask(q) {
+ const before = await page.$$eval('.qs-ai-answer, .qs-ai-error', (a) => a.length);
+ const t0 = Date.now();
+ await page.fill('#qs-ai-input', q);
+ await page.click('#qs-ai-send');
+ await page.waitForFunction((n) => document.querySelectorAll('.qs-ai-answer, .qs-ai-error').length > n, before, { timeout: 600_000 });
+ const items = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => `[${l.className}] ${l.textContent.replace(/\s+/g, ' ').slice(0, 900)}`));
+ console.log(`\n=== ${q} (${((Date.now() - t0) / 1000).toFixed(1)} s)`);
+ for (const i of items.slice(-8)) console.log(i);
+}
+await ask('Explain this circuit.');
+await ask('Write a three-qubit GHZ program, measured, and tell me its counts.');
+const proposals = await page.$$eval('.qs-ai-proposal', (p) => p.map((x) => x.textContent));
+console.log('\nproposals checked:', proposals.length, proposals.join(' | '));
+const answers = await page.$$eval('.qs-ai-answer', (a) => a.map((x) => x.innerText));
+for (const a of answers) console.log('\n--- answer as shown ---\n' + a);
+const { mkdirSync } = await import('node:fs');
+mkdirSync(join(dirname(fileURLToPath(import.meta.url)), 'screenshots'), { recursive: true });
+await page.locator('.qs-ai').screenshot({ path: join(dirname(fileURLToPath(import.meta.url)), 'screenshots', 'assistant-live.png') });
+await browser.close();
+server.close();
diff --git a/wai/quantum-studio/test/studio.test.mjs b/wai/quantum-studio/test/studio.test.mjs
new file mode 100644
index 000000000..dc882fd57
--- /dev/null
+++ b/wai/quantum-studio/test/studio.test.mjs
@@ -0,0 +1,196 @@
+// 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;
+// - the assistant, against a scripted model server: a tool call run on the engine, a
+// proposal the engine cannot read sent back with its reason, the corrected one checked
+// and run before it is offered, and Apply;
+// - 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');
+// The assistant, against a scripted model server: it calls a tool, proposes a program
+// the engine cannot read, gets the engine's reason back, and proposes a corrected one,
+// which the Studio checks and runs before offering it.
+{
+ const asked = [];
+ const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
+ const model = createServer((req, res) => {
+ const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' };
+ if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; }
+ if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; }
+ let body = '';
+ req.on('data', (c) => (body += c));
+ req.on('end', () => {
+ const r = JSON.parse(body);
+ asked.push(r);
+ const msgs = r.messages;
+ const told = msgs.some((m) => m.role === 'user' && /could not read the program you proposed/.test(m.content));
+ const toolDone = msgs.some((m) => m.role === 'tool');
+ let message;
+ if (!toolDone) message = { role: 'assistant', content: '', tool_calls: [{ id: 'c1', type: 'function', function: { name: 'lint_qasm', arguments: JSON.stringify({ qasm: GOOD }) } }] };
+ else if (!told) message = { role: 'assistant', content: 'It entangles two qubits. A variant:\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nfoo q[0];\n```' };
+ else message = { role: 'assistant', content: 'Corrected:\n```qasm\n' + GOOD + '```' };
+ res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message, finish_reason: message.tool_calls ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } }));
+ });
+ });
+ await new Promise((r) => model.listen(0, '127.0.0.1', r));
+ await page.click('#qs-ai-gear');
+ await page.fill('#qs-ai-endpoint', `http://127.0.0.1:${model.address().port}/v1`);
+ await page.click('#qs-ai-connect');
+ await page.waitForFunction(() => /connected · scripted/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: LONG });
+ check(true, 'the assistant connects to a model server and reads its models');
+ await page.click('.qs-ai-quick button >> text=Explain this circuit');
+ await page.waitForSelector('.qs-ai-apply', { timeout: LONG });
+ const log = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => l.textContent));
+ check(log.some((t) => t.includes('used lint_qasm')), 'the model\'s tool call ran on the engine, and the log shows it');
+ check(log.some((t) => /could not read the proposed program \(line 3: unknown gate foo\)/.test(t)), `a proposal the engine cannot read goes back with the engine's reason (${log.find((t) => /could not read/.test(t)) ?? 'none'})`);
+ const proposal = await page.textContent('.qs-ai-proposal');
+ check(/checked: 2 qubits, 2 gates/.test(proposal) && /ran here on Stabilizer tableau/.test(proposal), `the corrected proposal is checked and run before it is offered (${proposal})`);
+ check(asked.length === 3 && asked.every((r) => r.tools?.some((t) => t.function.name === 'run_job') && r.messages[0].role === 'system' && /Never state a probability/.test(r.messages[0].content)), 'every call to the model carries the engine\'s tools and the rule that numbers come from them');
+ const toolMsg = asked[1].messages.find((m) => m.role === 'tool');
+ check(toolMsg && JSON.parse(toolMsg.content).qubits === 2, 'the tool\'s answer went back to the model');
+ await page.click('.qs-ai-apply');
+ check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === GOOD, 'Apply puts the checked program in the editor');
+ model.close();
+}
+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..e48194ff5
--- /dev/null
+++ b/wai/wai-quantum-desktop/Cargo.lock
@@ -0,0 +1,4901 @@
+# 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 = [
+ "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 2.14.2",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[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 = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[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 = "litrs"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
+
+[[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 = "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-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 = "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 = "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 = "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 0.23.1",
+ "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 0.23.1",
+ "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",
+ "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-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 = "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",
+ "ureq",
+ "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 = "webpki-roots"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[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 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[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 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[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..0cb519b4b
--- /dev/null
+++ b/wai/wai-quantum-desktop/Cargo.toml
@@ -0,0 +1,31 @@
+[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"] }
+# The assistant's model server, called by the app (a local one by default).
+ureq = { version = "3", features = ["json"] }
+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..b9934da25
--- /dev/null
+++ b/wai/wai-quantum-desktop/src/main.rs
@@ -0,0 +1,315 @@
+//! 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