Files
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

803 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub-plan 5 — Quality & CI (Tasks 1014)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
**Goal:** Integration tests (JSON-RPC roundtrip), property-based tests, Criterion benchmarks, Qiskit cross-validation golden files, and GitHub Actions CI + release workflows.
**Starting state (after sub-plan 4):**
- Full working MCP server binary
- All tool handlers implemented and unit-tested
- `cargo test` passes, `cargo build` succeeds
**Deliverable:** `cargo test` passes all test suites. `cargo bench` compiles. CI YAML is valid. Five commits added.
**Important — lib target:** Tasks 1112 need `use quantum_bridge_mcp::...` from `tests/`. This requires adding `src/lib.rs` and a `[lib]` entry in `Cargo.toml`. Task 11 includes these steps.
**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 1014.
---
## Task 10: Integration Tests — MCP protocol roundtrip
**Files:**
- Create: `tests/integration/mod.rs`
- Create: `tests/integration/mcp_protocol.rs`
- [ ] **Step 1: Create test directory**
```bash
mkdir -p tests/integration
```
- [ ] **Step 2: Create tests/integration/mod.rs**
```rust
// Integration test module — empty file for cargo test discovery.
```
- [ ] **Step 3: Create tests/integration/mcp_protocol.rs**
```rust
//! Roundtrip: spawn binary, send JSON-RPC on stdin, assert stdout.
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
struct McpProcess { child: Child }
impl McpProcess {
fn spawn() -> Self {
let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn binary");
Self { child }
}
fn send(&mut self, msg: &str) {
let stdin = self.child.stdin.as_mut().unwrap();
writeln!(stdin, "{}", msg).unwrap();
stdin.flush().unwrap();
}
fn recv_line(&mut self) -> String {
let stdout = self.child.stdout.as_mut().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
line.trim().to_string()
}
}
impl Drop for McpProcess {
fn drop(&mut self) { let _ = self.child.kill(); }
}
fn initialize(proc: &mut McpProcess) -> serde_json::Value {
proc.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#);
serde_json::from_str(&proc.recv_line()).expect("valid JSON")
}
#[test]
fn initialize_returns_server_info() {
let mut proc = McpProcess::spawn();
let resp = initialize(&mut proc);
assert!(resp["result"]["serverInfo"]["name"].as_str().is_some());
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
}
#[test]
fn tools_list_contains_three_tools() {
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let tools = resp["result"]["tools"].as_array().unwrap();
assert_eq!(tools.len(), 3);
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
assert!(names.contains(&"list_backends"));
assert!(names.contains(&"validate_circuit"));
assert!(names.contains(&"run_circuit"));
}
#[test]
fn call_list_backends_returns_local_simulator() {
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["backends"][0]["name"], "local_simulator");
}
#[test]
fn call_validate_circuit_with_valid_bell_returns_is_valid_true() {
let bell = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;";
let call = format!(
r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{{"name":"validate_circuit","arguments":{{"circuit":"{}"}}}}}}"#,
bell
);
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(&call);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["is_valid"], true);
}
#[test]
fn call_run_circuit_x_gate_returns_100_ones() {
let x_circ = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[1] q;\\nbit[1] c;\\nx q[0];\\nc = measure q;";
let call = format!(
r#"{{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{{"name":"run_circuit","arguments":{{"circuit":"{}","shots":100}}}}}}"#,
x_circ
);
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(&call);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["counts"]["1"].as_u64().unwrap(), 100);
}
```
> **Note:** Uses `env!("CARGO_BIN_EXE_quantum-bridge-mcp")` — cargo sets this automatically. Run with `cargo test --test integration`. If rmcp uses length-prefixed framing instead of newlines, adjust `recv_line` to skip the header bytes.
- [ ] **Step 4: Run integration tests**
```bash
cargo test --test integration 2>&1
```
Expected: all 5 pass.
- [ ] **Step 5: Commit**
```bash
git add tests/integration/
git commit -m "test: add MCP protocol integration tests (initialize, tools/list, tools/call)"
```
---
## Task 11: Property-Based Tests + lib target
**Files:**
- Create: `src/lib.rs`
- Modify: `Cargo.toml` (add `[lib]` section)
- Modify: `src/main.rs` (remove mod declarations now in lib.rs)
- Create: `tests/proptest/mod.rs`
- Create: `tests/proptest/invariants.rs`
- [ ] **Step 1: Create src/lib.rs**
```rust
pub mod error;
pub mod executor;
pub mod types;
pub mod validator;
pub(crate) mod tools;
```
- [ ] **Step 2: Add [lib] to Cargo.toml**
```toml
[lib]
name = "quantum_bridge_mcp"
path = "src/lib.rs"
```
- [ ] **Step 3: Update src/main.rs — remove mod declarations, use crate name**
```rust
use anyhow::Result;
use quantum_bridge_mcp::tools::QuantumBridgeServer;
use rmcp::{ServiceExt, transport::stdio};
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(false)
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
tracing::info!("quantum-bridge-mcp starting");
let service = QuantumBridgeServer::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
```
- [ ] **Step 4: Verify cargo test still passes**
```bash
cargo test 2>&1
```
Expected: all existing tests still pass.
- [ ] **Step 5: Create proptest files**
```bash
mkdir -p tests/proptest
```
`tests/proptest/mod.rs`:
```rust
// Property-based test module.
```
`tests/proptest/invariants.rs`:
```rust
//! Quantum mechanics invariants that must hold for any valid circuit.
use proptest::prelude::*;
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
proptest! {
/// Shot counts always sum to exactly the requested number of shots.
#[test]
fn shot_counts_always_sum_to_requested_shots(shots in 1u32..=1_000u32) {
let circuit = r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;"#;
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit.to_string()), ShotCount(shots), false).unwrap();
let total: u64 = result.counts.values().sum();
prop_assert_eq!(total, shots as u64);
}
/// Statevector norm is always 1.0 (±1e-9).
#[test]
fn statevector_norm_is_one(angle in -std::f64::consts::PI..=std::f64::consts::PI) {
let circuit = format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;"
);
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit), ShotCount(1), true).unwrap();
let sv = result.statevector.unwrap();
let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum();
prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}");
}
/// Bitstrings in counts have length equal to the number of qubits.
#[test]
fn count_bitstrings_have_correct_length(
n_qubits in 1usize..=5usize,
shots in 10u32..=100u32,
) {
let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect();
let circuit = format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n"
);
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit), ShotCount(shots), false).unwrap();
for key in result.counts.keys() {
prop_assert_eq!(key.len(), n_qubits, "wrong length for key '{key}'");
}
}
}
```
- [ ] **Step 6: Run proptest**
```bash
cargo test --test proptest 2>&1
```
Expected: 3 property tests pass (each runs 256 cases).
- [ ] **Step 7: Commit**
```bash
git add src/lib.rs src/main.rs Cargo.toml tests/proptest/
git commit -m "test: add property-based tests for shot normalisation, statevector norm, and bitstring length"
```
---
## Task 12: Benchmarks
**Files:**
- Create: `benches/simulation.rs`
- [ ] **Step 1: Create bench file**
```bash
mkdir -p benches
```
`benches/simulation.rs`:
```rust
use criterion::{criterion_group, criterion_main, Criterion};
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
const BELL: &str = r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;"#;
fn make_h_circuit(n: usize, shots: u32) -> (CircuitSource, ShotCount) {
let gates: String = (0..n).map(|i| format!("h q[{i}];\n")).collect();
(
CircuitSource(format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n{gates}c = measure q;\n"
)),
ShotCount(shots),
)
}
fn bench_bell_1024_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
c.bench_function("bell_1024_shots", |b| {
b.iter(|| sim.run(&CircuitSource(BELL.to_string()), ShotCount(1024), false).unwrap())
});
}
fn bench_10_qubits_10k_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
let (circ, shots) = make_h_circuit(10, 10_000);
c.bench_function("10_qubits_10k_shots", |b| {
b.iter(|| sim.run(&circ, shots, false).unwrap())
});
}
fn bench_20_qubits_10k_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
let (circ, shots) = make_h_circuit(20, 10_000);
c.bench_function("20_qubits_10k_shots", |b| {
b.iter(|| sim.run(&circ, shots, false).unwrap())
});
}
criterion_group!(benches, bench_bell_1024_shots, bench_10_qubits_10k_shots, bench_20_qubits_10k_shots);
criterion_main!(benches);
```
- [ ] **Step 2: Run benchmarks (smoke pass — verifies they compile and run)**
```bash
cargo bench -- --test 2>&1
```
Expected: all three benchmark functions report a timing without panicking.
- [ ] **Step 3: Run full benchmarks to check SLAs**
```bash
cargo bench 2>&1 | grep -E "bell|qubits"
```
SLA targets (spec §6):
- `bell_1024_shots` < **5 ms**
- `10_qubits_10k_shots` < **50 ms**
- `20_qubits_10k_shots` < **500 ms**
If any target is missed, the bottleneck is likely `extract_gate_ops` allocating on every call. Check if the AST is being re-parsed unnecessarily — consider caching or pre-converting on first call.
- [ ] **Step 4: Commit**
```bash
git add benches/simulation.rs
git commit -m "bench: add Criterion benchmarks for Bell, 10-qubit, and 20-qubit circuits"
```
---
## Task 13: Reference Golden Files + Cross-Validation
**Files:**
- Create: `tests/reference/bell.qasm`
- Create: `tests/reference/ghz3.qasm`
- Create: `tests/reference/invalid/` (4 × .qasm + .error.txt pairs)
- Create: `scripts/gen_reference.py`
- Create: `tests/reference/cross_validate.rs`
- [ ] **Step 1: Create directory structure and circuit files**
```bash
mkdir -p tests/reference/invalid scripts
```
`tests/reference/bell.qasm`:
```
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;
```
`tests/reference/ghz3.qasm`:
```
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;
```
`tests/reference/invalid/unsupported_gate.qasm`:
```
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
u3(0.1, 0.2, 0.3) q[0];
```
`tests/reference/invalid/unsupported_gate.error.txt`:
```
gate 'u3' is not supported
```
`tests/reference/invalid/undeclared_qubit.qasm`:
```
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
h q[5];
```
`tests/reference/invalid/undeclared_qubit.error.txt`:
```
out of range
```
`tests/reference/invalid/syntax_error.qasm`:
```
OPENQASM 3.0;
this is not valid;
```
`tests/reference/invalid/syntax_error.error.txt`:
```
error
```
`tests/reference/invalid/qubit_limit.qasm`:
```
OPENQASM 3.0;
qubit[29] q;
```
`tests/reference/invalid/qubit_limit.error.txt`:
```
exceeds
```
- [ ] **Step 2: Write scripts/gen_reference.py**
```python
#!/usr/bin/env python3
"""Generate reference counts/statevectors via Qiskit Aer. Requires: pip install qiskit qiskit-aer"""
import json, sys
from pathlib import Path
try:
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
except ImportError:
print("ERROR: pip install qiskit qiskit-aer", file=sys.stderr); sys.exit(1)
SHOTS = 10_000
SEED = 42
REF_DIR = Path(__file__).parent.parent / "tests" / "reference"
def run_circuit(qasm_path: Path) -> dict:
qc = QuantumCircuit.from_qasm_file(str(qasm_path))
sim = AerSimulator(method="statevector")
counts = sim.run(qc, shots=SHOTS, seed_simulator=SEED).result().get_counts()
qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path))
qc_sv.save_statevector()
sv = list(sim.run(qc_sv, shots=0).result().get_statevector())
return {
"shots": SHOTS, "seed": SEED,
"counts": {k: int(v) for k, v in counts.items()},
"statevector": [[float(a.real), float(a.imag)] for a in sv],
}
if __name__ == "__main__":
for qasm in REF_DIR.glob("*.qasm"):
out = REF_DIR / qasm.with_suffix(".json").name
data = run_circuit(qasm)
out.write_text(json.dumps(data, indent=2))
print(f"wrote {out}")
```
- [ ] **Step 3: Write tests/reference/cross_validate.rs**
```rust
//! Cross-validates LocalSimulator counts against Qiskit golden files (chi-squared, α=0.01).
use std::collections::HashMap;
use std::path::Path;
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
use quantum_bridge_mcp::validator::CircuitValidator;
fn load_circuit(name: &str) -> CircuitSource {
CircuitSource(std::fs::read_to_string(Path::new("tests/reference").join(name)).unwrap())
}
fn load_expected_counts(name: &str) -> HashMap<String, u64> {
let path = Path::new("tests/reference").join(name);
if !path.exists() { return HashMap::new(); }
let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
data["counts"].as_object().unwrap().iter()
.map(|(k, v)| (k.clone(), v.as_u64().unwrap()))
.collect()
}
fn chi_squared_passes(observed: &HashMap<String, u64>, expected: &HashMap<String, u64>) -> bool {
if expected.is_empty() { return true; }
let chi_sq: f64 = expected.iter().map(|(k, &exp)| {
let obs = *observed.get(k).unwrap_or(&0) as f64;
let e = exp as f64;
if e == 0.0 { 0.0 } else { (obs - e).powi(2) / e }
}).sum();
let critical = (expected.len() - 1) as f64 * 3.0 + 6.63; // conservative
chi_sq < critical
}
#[test]
fn bell_counts_match_qiskit_golden() {
let expected = load_expected_counts("bell_counts.json");
if expected.is_empty() { return; }
let result = LocalSimulator::new().run(&load_circuit("bell.qasm"), ShotCount(10_000), false).unwrap();
assert!(chi_squared_passes(&result.counts, &expected),
"Bell diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
}
#[test]
fn ghz3_counts_match_qiskit_golden() {
let expected = load_expected_counts("ghz3_counts.json");
if expected.is_empty() { return; }
let result = LocalSimulator::new().run(&load_circuit("ghz3.qasm"), ShotCount(10_000), false).unwrap();
assert!(chi_squared_passes(&result.counts, &expected),
"GHZ-3 diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
}
#[test]
fn invalid_circuits_produce_validation_errors() {
use std::fs;
let validator = CircuitValidator::new(MAX_LOCAL_QUBITS);
for entry in fs::read_dir("tests/reference/invalid").unwrap() {
let path = entry.unwrap().path();
if path.extension().map(|e| e == "qasm").unwrap_or(false) {
let expected_frag = fs::read_to_string(path.with_extension("error.txt"))
.unwrap().trim().to_lowercase();
let source = fs::read_to_string(&path).unwrap();
let result = validator.validate(&quantum_bridge_mcp::types::CircuitSource(source)).unwrap();
assert!(!result.is_valid, "{} should be invalid", path.display());
let msgs: String = result.diagnostics.iter().map(|d| d.message.to_lowercase()).collect::<Vec<_>>().join(" ");
assert!(msgs.contains(&expected_frag),
"expected '{}' in messages for {}\nGot: {}", expected_frag, path.display(), msgs);
}
}
}
```
- [ ] **Step 4: Run cross-validation tests**
```bash
cargo test --test cross_validate 2>&1
```
Expected: `bell_counts_match_qiskit_golden` and `ghz3_counts_match_qiskit_golden` are **skipped** (no JSON yet); `invalid_circuits_produce_validation_errors` **passes**.
Optional — generate golden files (needs Python + Qiskit):
```bash
python3 scripts/gen_reference.py && cargo test --test cross_validate
```
- [ ] **Step 5: Commit**
```bash
git add tests/reference/ scripts/gen_reference.py
git commit -m "test: add cross-validation golden files and gen_reference.py script"
```
---
## Task 14: GitHub Actions CI + Release
**Files:**
- Create: `.github/workflows/ci.yml`
- Create: `.github/workflows/release.yml`
- [ ] **Step 1: Create .github/workflows/ci.yml**
```bash
mkdir -p .github/workflows
```
```yaml
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
rust:
name: Rust checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test
- run: cargo bench -- --test
cross-validate:
name: Cross-validate with Qiskit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-qiskit
- run: pip install qiskit qiskit-aer
- run: python3 scripts/gen_reference.py
- run: cargo test --test cross_validate
```
- [ ] **Step 2: Create .github/workflows/release.yml**
```yaml
name: Release
on:
push:
tags:
- "v*"
jobs:
dist:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: ubuntu-latest, target: aarch64-unknown-linux-gnu }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross (aarch64 Linux)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: cargo install cross
- name: Build
shell: bash
run: |
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
tar czf quantum-bridge-mcp-${{ matrix.target }}.tar.gz quantum-bridge-mcp
mv quantum-bridge-mcp-${{ matrix.target }}.tar.gz ../../../
- name: Package (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
cd target/${{ matrix.target }}/release
Compress-Archive quantum-bridge-mcp.exe quantum-bridge-mcp-${{ matrix.target }}.zip
mv quantum-bridge-mcp-${{ matrix.target }}.zip ../../../
- uses: actions/upload-artifact@v4
with:
name: quantum-bridge-mcp-${{ matrix.target }}
path: quantum-bridge-mcp-${{ matrix.target }}.*
github-release:
needs: dist
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
files: "quantum-bridge-mcp-*"
generate_release_notes: true
```
- [ ] **Step 3: Validate YAML**
```bash
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo "ci.yml OK"
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo "release.yml OK"
```
- [ ] **Step 4: Full local verification pass**
```bash
cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10
```
Expected: all green.
- [ ] **Step 5: Commit**
```bash
git add .github/
git commit -m "ci: add GitHub Actions for Rust checks, Qiskit cross-validation, and multi-platform release"
```
---
## Final verification — all sub-plans complete
```bash
cargo fmt --check \
&& cargo clippy -- -D warnings \
&& cargo test \
&& cargo test --test integration \
&& cargo test --test proptest \
&& cargo test --test cross_validate \
&& cargo bench -- --test \
&& echo "ALL CHECKS PASSED"
```
Expected: `ALL CHECKS PASSED`. V1 implementation complete.