# quantum-bridge-mcp V1 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a zero-dependency MCP server (single Rust binary) that exposes three tools — `list_backends`, `validate_circuit`, `run_circuit` — for local OpenQASM 3.0 quantum circuit simulation. **Architecture:** An rmcp stdio server wires three tool handlers; each handler delegates to either `CircuitValidator` (wrapping `oq3_semantics`) or `LocalSimulator` (wrapping `spinoza`). The tools depend on `Backend` traits, never on concrete types, to allow IBM backend addition in V1.5 without touching tool code. **Tech Stack:** Rust 2021, `rmcp` (git, MCP SDK), `oq3_semantics 0.7` (OpenQASM3 parser), `spinoza 0.5` (statevector simulator), `tokio`, `serde`/`schemars`, `thiserror`, `tracing`. --- ## File Map ``` Cargo.toml src/ main.rs — rmcp stdio server, wires QuantumBridgeServer error.rs — BridgeError (thiserror) types.rs — QubitIndex, ShotCount, CircuitSource, ValidationResult, SimulationResult executor.rs — Backend traits + LocalSimulator (spinoza) validator.rs — CircuitValidator (oq3_semantics) tools/ mod.rs — QuantumBridgeServer struct + tool_router impl list_backends.rs — list_backends handler validate_circuit.rs — validate_circuit handler run_circuit.rs — run_circuit handler tests/ integration/ mcp_protocol.rs — JSON-RPC roundtrip, schema conformance proptest/ invariants.rs — unitarity, normalisation reference/ bell.qasm — valid Bell circuit bell_counts.json — expected count distribution ghz3.qasm — 3-qubit GHZ circuit ghz3_counts.json invalid/ unsupported_gate.qasm + unsupported_gate.error.txt undeclared_qubit.qasm + undeclared_qubit.error.txt syntax_error.qasm + syntax_error.error.txt qubit_limit.qasm + qubit_limit.error.txt benches/ simulation.rs — Criterion: Bell < 5 ms, 20 qubits < 500 ms scripts/ gen_reference.py — generates tests/reference/ via Qiskit Aer .github/workflows/ ci.yml release.yml ``` --- ## Task 1: Bootstrap — Cargo project + dependencies **Files:** - Create: `Cargo.toml` - Modify: `src/main.rs` (created by `cargo new`) - [ ] **Step 1: Initialise the cargo project** ```bash cd /home/vincent/src/misc/quantum-bridge-mcp cargo new --name quantum-bridge-mcp . ``` Expected: `src/main.rs` and `Cargo.toml` created. Git will show them as untracked. - [ ] **Step 2: Write Cargo.toml** > **Note:** `spinoza` publishes as `0.5.x` on crates.io (not `2.x`). Run `cargo search spinoza` and `cargo search oq3_semantics` to confirm latest patch versions before pinning. ```toml [package] name = "quantum-bridge-mcp" version = "0.1.0" edition = "2021" description = "Zero-dependency MCP server for local OpenQASM 3.0 quantum circuit simulation" license = "MIT" [dependencies] rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features = ["server", "transport-io", "macros"] } oq3_semantics = "0.7" spinoza = "0.5" tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" schemars = "0.8" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1" [dev-dependencies] proptest = "1" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "simulation" harness = false ``` - [ ] **Step 3: Write minimal src/main.rs** ```rust fn main() { println!("quantum-bridge-mcp"); } ``` - [ ] **Step 4: Verify all dependencies resolve and compile** ```bash cargo build ``` Expected: compiles successfully (first run downloads ~200 MB of deps). If `spinoza` or `oq3_semantics` version constraint fails, run `cargo search ` and update the version in Cargo.toml. - [ ] **Step 5: Commit** ```bash git add Cargo.toml Cargo.lock src/main.rs git commit -m "chore: bootstrap project with all V1 dependencies" ``` --- ## Task 2: Foundation — Error types and domain newtypes **Files:** - Create: `src/error.rs` - Create: `src/types.rs` - Modify: `src/main.rs` - [ ] **Step 1: Write src/error.rs** ```rust use thiserror::Error; #[derive(Debug, Error)] pub enum BridgeError { #[error("parse error at line {line}, col {col}: {message}")] Parse { line: usize, col: usize, message: String }, #[error("gate '{gate}' is not supported at line {line} — supported: {supported}")] UnsupportedGate { gate: String, line: usize, supported: String }, #[error("qubit index {index} is out of range (circuit declares {declared} qubits)")] QubitOutOfRange { index: usize, declared: usize }, #[error("circuit requires {requested} qubits, exceeds the local simulator limit of {limit}")] QubitLimitExceeded { requested: usize, limit: usize }, #[error("simulation failed: {0}")] Simulation(String), #[error("measure at line {line} maps to an undeclared classical bit")] MeasurementMapping { line: usize }, } ``` - [ ] **Step 2: Write src/types.rs** ```rust use std::collections::HashMap; /// Newtype for qubit indices — prevents mixing with plain usizes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct QubitIndex(pub usize); /// Newtype for shot count — enforces range and intent at type level. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ShotCount(pub u32); impl ShotCount { pub const DEFAULT: ShotCount = ShotCount(1024); pub const MAX: ShotCount = ShotCount(100_000); } /// Wraps an OpenQASM 3.0 source string. #[derive(Debug, Clone)] pub struct CircuitSource(pub String); #[derive(Debug, Clone)] pub enum DiagnosticSeverity { Error, Warning, } #[derive(Debug, Clone)] pub struct ValidationDiagnostic { pub line: usize, pub column: usize, pub message: String, pub severity: DiagnosticSeverity, } #[derive(Debug, Clone)] pub struct ValidationResult { pub is_valid: bool, pub diagnostics: Vec, pub num_qubits: Option, pub num_gates: Option, } #[derive(Debug, Clone)] pub struct SimulationResult { /// Bitstring → count, e.g. `{"00": 512, "11": 512}`. pub counts: HashMap, pub shots: u32, pub execution_time_ms: f64, /// Optional full statevector as (real, imag) pairs per basis state. pub statevector: Option>, } ``` - [ ] **Step 3: Declare modules in src/main.rs** ```rust mod error; mod types; fn main() { println!("quantum-bridge-mcp"); } ``` - [ ] **Step 4: Verify compilation** ```bash cargo build ``` Expected: compiles without warnings. - [ ] **Step 5: Commit** ```bash git add src/error.rs src/types.rs src/main.rs git commit -m "feat: add error types and domain newtypes" ``` --- ## Task 3: Backend traits **Files:** - Create: `src/executor.rs` (traits only — implementation comes in Task 5) - [ ] **Step 1: Write the failing compilation test** Add to the bottom of `src/executor.rs` (create the file): ```rust use crate::error::BridgeError; use crate::types::{CircuitSource, ShotCount, SimulationResult, ValidationResult}; pub const MAX_LOCAL_QUBITS: usize = 28; pub const SUPPORTED_GATES: &[&str] = &[ "h", "x", "y", "z", "s", "t", "sdg", "tdg", "rx", "ry", "rz", "cx", "cz", "swap", "ccx", "measure", ]; pub trait CanIntrospect { fn name(&self) -> &str; fn max_qubits(&self) -> usize; fn supported_gates(&self) -> &[&str]; } pub trait CanValidate { fn validate(&self, circuit: &CircuitSource) -> Result; } pub trait CanExecute { fn run( &self, circuit: &CircuitSource, shots: ShotCount, return_statevector: bool, ) -> Result; } /// Marker trait combining all three capabilities. V1.5 IBM backend will impl this too. pub trait Backend: CanIntrospect + CanValidate + CanExecute + Send + Sync {} #[cfg(test)] mod tests { use super::*; // Compile-time test: a mock struct can satisfy the trait bounds. struct _MockBackend; impl CanIntrospect for _MockBackend { fn name(&self) -> &str { "mock" } fn max_qubits(&self) -> usize { 4 } fn supported_gates(&self) -> &[&str] { SUPPORTED_GATES } } impl CanValidate for _MockBackend { fn validate(&self, _: &CircuitSource) -> Result { Ok(ValidationResult { is_valid: true, diagnostics: vec![], num_qubits: None, num_gates: None }) } } impl CanExecute for _MockBackend { fn run(&self, _: &CircuitSource, _: ShotCount, _: bool) -> Result { Ok(SimulationResult { counts: Default::default(), shots: 0, execution_time_ms: 0.0, statevector: None }) } } impl Backend for _MockBackend {} } ``` - [ ] **Step 2: Declare executor module and run tests** Add to `src/main.rs`: ```rust mod executor; ``` ```bash cargo test ``` Expected: 0 tests run, 0 failures — the compile-time test above just confirms the trait bounds are satisfiable. - [ ] **Step 3: Commit** ```bash git add src/executor.rs src/main.rs git commit -m "feat: define Backend trait hierarchy (CanIntrospect/CanValidate/CanExecute)" ``` --- ## Task 4: CircuitValidator — oq3_semantics integration **Files:** - Create: `src/validator.rs` ### 4a — AST exploration (required before implementing) The `oq3_semantics` AST is lightly documented. This step maps the real types so the implementation is correct. - [ ] **Step 1: Write an AST exploration test in src/validator.rs** Create `src/validator.rs` with this content: ```rust use oq3_semantics::syntax_to_semantics::parse_source_string; #[cfg(test)] mod exploration { use super::*; #[test] fn print_bell_circuit_ast() { let qasm = 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 result = parse_source_string(qasm, Some("bell.qasm"), None::<&[&str]>); println!("any_errors: {}", result.any_errors()); println!("program: {:#?}", result.program()); println!("---"); for err in result.semantic_errors().iter() { println!("error: {:?}", err); } } #[test] fn print_unsupported_gate_ast() { let qasm = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; u3(0.1, 0.2, 0.3) q[0];"#; let result = parse_source_string(qasm, Some("bad.qasm"), None::<&[&str]>); println!("any_errors: {}", result.any_errors()); println!("program: {:#?}", result.program()); } } ``` - [ ] **Step 2: Run exploration and read the output** ```bash cargo test exploration -- --nocapture 2>&1 | head -200 ``` **Read the output carefully.** You need to identify: - How `Program` exposes statements (e.g. `program.stmts()`, `program.statements()`, or iterator impl) - The name of the `Stmt` enum variant for gate calls (e.g. `Stmt::GateCall(...)`) - The name of the variant for qubit declarations (e.g. `Stmt::QubitDecl(...)`) - How to get the gate name string from a `GateCall` (check if `name()` returns `&str` or needs symbol table resolution) - How to get qubit register size from a qubit declaration Write down the variant names — you will use them in the next steps. ### 4b — Implementation - [ ] **Step 3: Write the failing validator tests** Replace `src/validator.rs` with: ```rust use oq3_semantics::syntax_to_semantics::parse_source_string; use crate::error::BridgeError; use crate::executor::SUPPORTED_GATES; use crate::types::{CircuitSource, DiagnosticSeverity, ValidationDiagnostic, ValidationResult}; pub struct CircuitValidator { max_qubits: usize, } impl CircuitValidator { pub fn new(max_qubits: usize) -> Self { Self { max_qubits } } pub fn validate(&self, source: &CircuitSource) -> Result { todo!("implement after AST exploration") } } /// Converts a byte offset in `source` to (1-based line, 1-based column). fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { let safe_offset = offset.min(source.len()); let prefix = &source[..safe_offset]; let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1; let col = prefix.rfind('\n').map(|p| safe_offset - p - 1).unwrap_or(safe_offset) + 1; (line, col) } #[cfg(test)] mod tests { use super::*; use crate::executor::MAX_LOCAL_QUBITS; fn validator() -> CircuitValidator { CircuitValidator::new(MAX_LOCAL_QUBITS) } const BELL_CIRCUIT: &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;"#; #[test] fn valid_bell_circuit_is_accepted() { let result = validator() .validate(&CircuitSource(BELL_CIRCUIT.to_string())) .unwrap(); assert!(result.is_valid, "diagnostics: {:?}", result.diagnostics); assert!(result.diagnostics.is_empty()); assert_eq!(result.num_qubits, Some(2)); } #[test] fn unsupported_gate_produces_error_diagnostic() { // u3 is a valid QASM3 standard gate but not in our supported set let circuit = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; u3(0.1, 0.2, 0.3) q[0];"#; let result = validator() .validate(&CircuitSource(circuit.to_string())) .unwrap(); assert!(!result.is_valid); assert!(!result.diagnostics.is_empty()); let msg = &result.diagnostics[0].message; assert!( msg.to_lowercase().contains("u3"), "expected 'u3' in message, got: {msg}" ); } #[test] fn too_many_qubits_produces_error_diagnostic() { let n = MAX_LOCAL_QUBITS + 1; let circuit = format!("OPENQASM 3.0;\nqubit[{n}] q;\n"); let result = validator() .validate(&CircuitSource(circuit)) .unwrap(); assert!(!result.is_valid); assert!(!result.diagnostics.is_empty()); let msg = &result.diagnostics[0].message; assert!( msg.contains(&n.to_string()), "expected qubit count in message, got: {msg}" ); } #[test] fn invalid_qasm_syntax_produces_error_diagnostic() { let circuit = "OPENQASM 3.0;\nthis is not valid qasm;\n"; let result = validator() .validate(&CircuitSource(circuit.to_string())) .unwrap(); assert!(!result.is_valid); assert!(!result.diagnostics.is_empty()); } #[test] fn undeclared_qubit_produces_error_diagnostic() { let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nh q[5];\n"; let result = validator() .validate(&CircuitSource(circuit.to_string())) .unwrap(); assert!(!result.is_valid); assert!(!result.diagnostics.is_empty()); } } ``` - [ ] **Step 4: Run tests to confirm they all fail at the `todo!()`** ```bash cargo test validator::tests 2>&1 | tail -20 ``` Expected: all 5 tests panic at `todo!()`. - [ ] **Step 5: Implement CircuitValidator::validate** Replace the `todo!()` body with the real implementation. Use the variant names you identified in Step 2 above. ```rust pub fn validate(&self, source: &CircuitSource) -> Result { let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); let mut diagnostics: Vec = Vec::new(); // Collect errors from oq3_semantics (syntax + undeclared qubits, etc.) for error in parse_result.semantic_errors().iter() { let range = error.range(); // TextRange::start() returns a rowan::TextSize; .into() gives u32, cast to usize let offset: usize = u32::from(range.start()) as usize; let (line, col) = byte_offset_to_line_col(&source.0, offset); diagnostics.push(ValidationDiagnostic { line, column: col, message: error.message(), severity: DiagnosticSeverity::Error, }); } // Stop early on parse/semantic errors — AST may be incomplete if parse_result.any_errors() { return Ok(ValidationResult { is_valid: false, diagnostics, num_qubits: None, num_gates: None, }); } let program = parse_result.program(); // Walk statements to: // (a) count total declared qubits // (b) collect all gate calls and check against SUPPORTED_GATES // // NOTE: replace the Stmt variant names below with what you observed in the // AST exploration step (Step 2). Common patterns: // Stmt::GateCall(gc) — a gate application // Stmt::QubitDeclaration(qd) — qubit register declaration // Use `dbg!(stmt)` if unsure. let mut total_qubits: usize = 0; let mut gate_count: usize = 0; for stmt in program.stmts() { // --- Qubit declarations --- // Adjust the pattern arm to match the real variant name from your exploration. if let Some(num) = extract_qubit_count(stmt) { total_qubits += num; } // --- Gate calls --- if let Some(gate_name) = extract_gate_name(stmt) { if gate_name == "measure" { continue; // measure is always allowed } gate_count += 1; if !SUPPORTED_GATES.contains(&gate_name.as_str()) { let line = extract_stmt_line(stmt, &source.0); let supported = SUPPORTED_GATES.join(", "); diagnostics.push(ValidationDiagnostic { line, column: 1, message: format!( "gate '{gate_name}' is not supported by this simulator (supported: {supported})" ), severity: DiagnosticSeverity::Error, }); } } } // Qubit limit check if total_qubits > self.max_qubits { diagnostics.push(ValidationDiagnostic { line: 1, column: 1, message: format!( "circuit requires {total_qubits} qubits, exceeds local simulator limit of {}", self.max_qubits ), severity: DiagnosticSeverity::Error, }); } Ok(ValidationResult { is_valid: diagnostics.is_empty(), diagnostics, num_qubits: if total_qubits > 0 { Some(total_qubits) } else { None }, num_gates: if gate_count > 0 { Some(gate_count) } else { None }, }) } ``` Then add the helper functions below `validate`. Adjust variant names to match your exploration output: ```rust use oq3_semantics::asg::Stmt; /// Returns the qubit register size if this statement is a qubit declaration. /// Adjust the pattern to match the real Stmt variant name. fn extract_qubit_count(stmt: &Stmt) -> Option { // Example pattern — replace with actual variant from exploration: // if let Stmt::QubitDeclaration(qd) = stmt { // return Some(qd.width().unwrap_or(1)); // } // Fallback: if you can't find the variant, use the approach below // and fill in after running the exploration test. let _ = stmt; None // Replace this with the real implementation } /// Returns the lowercase gate name if this statement is a gate call. fn extract_gate_name(stmt: &Stmt) -> Option { // Example pattern — replace with actual variant from exploration: // if let Stmt::GateCall(gc) = stmt { // return Some(gc.name().to_string().to_lowercase()); // } let _ = stmt; None // Replace this with the real implementation } /// Returns the 1-based line number for a statement using its source span. fn extract_stmt_line(stmt: &Stmt, source: &str) -> usize { // Example: // let offset: usize = u32::from(stmt.span().start()) as usize; // byte_offset_to_line_col(source, offset).0 let _ = (stmt, source); 1 // Replace this with the real implementation } ``` > **Important:** The three helper functions above contain placeholder `None`/`1` returns. After running Step 2's exploration test, replace each with the real match arms using the AST variant names you observed. This is intentional — AST introspection requires seeing live debug output first. The test suite (Step 6) will fail until these are real. - [ ] **Step 6: Run the tests and fix until all pass** ```bash cargo test validator::tests 2>&1 ``` Iterate on the helper functions until all 5 tests pass. If `program.stmts()` doesn't exist, check what methods `Program` exposes via `cargo doc --open` or `rust-analyzer` hover. - [ ] **Step 7: Declare the module and verify no regressions** Add to `src/main.rs`: ```rust mod validator; ``` ```bash cargo test ``` Expected: all tests pass. - [ ] **Step 8: Commit** ```bash git add src/validator.rs src/main.rs git commit -m "feat: implement CircuitValidator with oq3_semantics" ``` --- ## Task 5: LocalSimulator — spinoza executor **Files:** - Modify: `src/executor.rs` (add `LocalSimulator` below the traits) - [ ] **Step 1: Write failing executor tests** Append to `src/executor.rs` (inside the `#[cfg(test)]` block, replacing the mock test): ```rust #[cfg(test)] mod tests { use super::*; use crate::types::{CircuitSource, ShotCount}; const BELL_CIRCUIT: &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;"#; const X_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; x q[0]; c = measure q;"#; const H_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; h q[0]; c = measure q;"#; fn sim() -> LocalSimulator { LocalSimulator::new() } #[test] fn bell_circuit_only_produces_00_and_11() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); assert_eq!(result.shots, 10_000); let total: u64 = result.counts.values().sum(); assert_eq!(total, 10_000); for key in result.counts.keys() { assert!(key == "00" || key == "11", "unexpected bitstring: {key}"); } } #[test] fn bell_circuit_is_roughly_balanced() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); let count_00 = result.counts.get("00").copied().unwrap_or(0) as f64; let ratio = count_00 / 10_000.0; assert!( (0.45..=0.55).contains(&ratio), "ratio_00={ratio:.3} not in [0.45, 0.55]" ); } #[test] fn x_gate_always_produces_one() { let result = sim() .run(&CircuitSource(X_CIRCUIT.to_string()), ShotCount(1_000), false) .unwrap(); assert_eq!(result.counts.get("1").copied().unwrap_or(0), 1_000); } #[test] fn h_gate_produces_roughly_balanced_single_qubit() { let result = sim() .run(&CircuitSource(H_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); let count_0 = result.counts.get("0").copied().unwrap_or(0) as f64; let ratio = count_0 / 10_000.0; assert!( (0.45..=0.55).contains(&ratio), "ratio_0={ratio:.3} not in [0.45, 0.55]" ); } #[test] fn run_returns_statevector_when_requested() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(100), true) .unwrap(); let sv = result.statevector.expect("statevector should be present"); assert_eq!(sv.len(), 4); // 2^2 = 4 amplitudes for 2 qubits // Bell state: amplitudes 0 and 3 are 1/√2, amplitudes 1 and 2 are 0 let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum(); assert!((norm - 1.0).abs() < 1e-9, "norm={norm}"); } #[test] fn local_simulator_name_is_local_simulator() { assert_eq!(sim().name(), "local_simulator"); } #[test] fn local_simulator_max_qubits_is_28() { assert_eq!(sim().max_qubits(), MAX_LOCAL_QUBITS); } } ``` - [ ] **Step 2: Run tests to confirm they all fail (LocalSimulator not defined yet)** ```bash cargo test executor::tests 2>&1 | tail -5 ``` Expected: compile error `cannot find struct LocalSimulator`. - [ ] **Step 3: Add LocalSimulator to src/executor.rs** Add after the trait definitions (before `#[cfg(test)]`): ```rust use std::collections::HashMap; use std::f64::consts::PI; use std::time::Instant; use oq3_semantics::syntax_to_semantics::parse_source_string; use spinoza::core::{apply, c_apply, cc_apply, reservoir_sampling, State}; use spinoza::gates::Gate; pub struct LocalSimulator; impl LocalSimulator { pub fn new() -> Self { Self } } impl CanIntrospect for LocalSimulator { fn name(&self) -> &str { "local_simulator" } fn max_qubits(&self) -> usize { MAX_LOCAL_QUBITS } fn supported_gates(&self) -> &[&str] { SUPPORTED_GATES } } impl CanValidate for LocalSimulator { fn validate(&self, circuit: &CircuitSource) -> Result { use crate::validator::CircuitValidator; CircuitValidator::new(MAX_LOCAL_QUBITS).validate(circuit) } } impl CanExecute for LocalSimulator { fn run( &self, circuit: &CircuitSource, shots: ShotCount, return_statevector: bool, ) -> Result { let start = Instant::now(); // Parse circuit — validation is assumed to have passed already. let parse_result = parse_source_string(&circuit.0, Some("circuit.qasm"), None::<&[&str]>); let program = parse_result.program(); // First pass: count qubits and identify measured qubits. let num_qubits = count_qubits(program); if num_qubits == 0 { return Err(BridgeError::Simulation("circuit declares no qubits".to_string())); } if num_qubits > MAX_LOCAL_QUBITS { return Err(BridgeError::QubitLimitExceeded { requested: num_qubits, limit: MAX_LOCAL_QUBITS, }); } // Initialise statevector |0...0⟩ let mut state = State::new(num_qubits); // Second pass: apply non-measurement gates in order. for (gate_name, params, qubits) in extract_gate_ops(program) { apply_gate(&gate_name, ¶ms, &qubits, &mut state)?; } // Collect statevector before sampling (if requested). let statevector = if return_statevector { let sv: Vec<(f64, f64)> = state .reals .iter() .zip(state.imags.iter()) .map(|(&r, &i)| (r as f64, i as f64)) .collect(); Some(sv) } else { None }; // Sample N shots from the final statevector. let num_states = 1usize << num_qubits; let mut reservoir = reservoir_sampling(&state, num_states, shots.0 as usize); let raw_counts = reservoir.get_outcome_count(); // Convert usize indices to bitstrings. let counts: HashMap = raw_counts .into_iter() .map(|(idx, cnt)| { let bitstring = format!("{:0>width$b}", idx, width = num_qubits); (bitstring, cnt as u64) }) .collect(); let execution_time_ms = start.elapsed().as_secs_f64() * 1000.0; Ok(SimulationResult { counts, shots: shots.0, execution_time_ms, statevector, }) } } impl Backend for LocalSimulator {} /// Applies a single gate to the state. Returns an error for unsupported gates. fn apply_gate( gate_name: &str, params: &[f64], qubits: &[usize], state: &mut State, ) -> Result<(), BridgeError> { let supported = SUPPORTED_GATES.join(", "); match (gate_name, qubits) { ("h", &[t]) => apply(Gate::H, state, t), ("x", &[t]) => apply(Gate::X, state, t), ("y", &[t]) => apply(Gate::Y, state, t), ("z", &[t]) => apply(Gate::Z, state, t), ("s", &[t]) => apply(Gate::P(PI / 2.0), state, t), ("t", &[t]) => apply(Gate::P(PI / 4.0), state, t), ("sdg", &[t]) => apply(Gate::P(-PI / 2.0), state, t), ("tdg", &[t]) => apply(Gate::P(-PI / 4.0), state, t), ("rx", &[t]) if params.len() == 1 => apply(Gate::RX(params[0]), state, t), ("ry", &[t]) if params.len() == 1 => apply(Gate::RY(params[0]), state, t), ("rz", &[t]) if params.len() == 1 => apply(Gate::RZ(params[0]), state, t), ("cx", &[ctrl, tgt]) => c_apply(Gate::X, state, ctrl, tgt), ("cz", &[ctrl, tgt]) => c_apply(Gate::Z, state, ctrl, tgt), ("swap", &[a, b]) => apply(Gate::SWAP(a, b), state, a), ("ccx", &[c0, c1, tgt]) => cc_apply(Gate::X, state, c0, c1, tgt), ("measure", _) => {} // handled via statevector sampling — skip inline _ => { return Err(BridgeError::UnsupportedGate { gate: gate_name.to_string(), line: 0, supported, }) } } Ok(()) } /// Walks the AST and returns gate operations as (name, params, qubit_indices). /// Fill in AST variant names after running the exploration test in Task 4. fn extract_gate_ops(program: &oq3_semantics::asg::Program) -> Vec<(String, Vec, Vec)> { let mut ops = Vec::new(); for stmt in program.stmts() { // TODO after exploration: match Stmt::GateCall(gc) and extract // gate name, parameter float values, and qubit indices. // Example skeleton: // if let Stmt::GateCall(gc) = stmt { // let name = resolve_gate_name(gc).to_lowercase(); // let params = extract_float_params(gc); // let qubits = extract_qubit_indices(gc); // ops.push((name, params, qubits)); // } let _ = stmt; } ops } /// Counts total declared qubits in the program. /// Fill in AST variant names after running the exploration test in Task 4. fn count_qubits(program: &oq3_semantics::asg::Program) -> usize { let mut total = 0; for stmt in program.stmts() { // TODO after exploration: match Stmt::QubitDeclaration(qd) and sum sizes. let _ = stmt; } total } ``` > **Note:** `extract_gate_ops` and `count_qubits` have the same `TODO` pattern as Task 4's helper functions — fill them in using the AST variant names from the exploration step. The test suite below will guide you. - [ ] **Step 4: Run tests and fix until all pass** ```bash cargo test executor::tests 2>&1 ``` The tests will fail until `extract_gate_ops` and `count_qubits` correctly walk the AST. Use the exploration output from Task 4 Step 2 to fill in the match arms. Pay attention to: - **Endianness**: if `bell_circuit_only_produces_00_and_11` sees `"01"` or `"10"`, spinoza's qubit ordering is reversed vs. QASM3. Fix by reversing the qubit index mapping: `let t = num_qubits - 1 - raw_qubit_idx`. - **Norm**: if the statevector test fails, check that `state.reals` and `state.imags` are indexed correctly. - [ ] **Step 5: Run full test suite** ```bash cargo test 2>&1 ``` Expected: all tests pass. - [ ] **Step 6: Commit** ```bash git add src/executor.rs git commit -m "feat: implement LocalSimulator with spinoza statevector engine" ``` --- ## Task 6: Tool — list_backends **Files:** - Create: `src/tools/mod.rs` - Create: `src/tools/list_backends.rs` - [ ] **Step 1: Write the failing test** Create `src/tools/list_backends.rs`: ```rust use serde_json::json; use crate::executor::{LocalSimulator, SUPPORTED_GATES, MAX_LOCAL_QUBITS}; pub fn list_backends_response() -> serde_json::Value { todo!("implement list_backends_response") } #[cfg(test)] mod tests { use super::*; #[test] fn response_contains_local_simulator() { let resp = list_backends_response(); let backends = resp["backends"].as_array().unwrap(); assert_eq!(backends.len(), 1); assert_eq!(backends[0]["name"], "local_simulator"); } #[test] fn response_contains_max_qubits() { let resp = list_backends_response(); let backend = &resp["backends"][0]; assert_eq!( backend["max_qubits"].as_u64().unwrap(), MAX_LOCAL_QUBITS as u64 ); } #[test] fn response_lists_supported_gates() { let resp = list_backends_response(); let gates = resp["backends"][0]["supported_gates"] .as_array() .unwrap(); assert!(!gates.is_empty()); assert!(gates.iter().any(|g| g == "h")); assert!(gates.iter().any(|g| g == "cx")); } } ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash cargo test tools::list_backends 2>&1 | tail -5 ``` Expected: compile error (module not declared yet) or panic at `todo!()`. - [ ] **Step 3: Implement list_backends_response** Replace the `todo!()`: ```rust pub fn list_backends_response() -> serde_json::Value { let sim = LocalSimulator::new(); json!({ "backends": [{ "name": sim.name(), "description": "Local statevector simulator (Spinoza engine). No network, no account required.", "max_qubits": sim.max_qubits(), "supported_gates": sim.supported_gates(), "simulation_type": "statevector", "notes": "Use run_circuit shots parameter (default 1024, max 100000). Statevector memory grows as 2^n × 16 bytes." }] }) } ``` - [ ] **Step 4: Create src/tools/mod.rs** ```rust pub mod list_backends; pub mod run_circuit; pub mod validate_circuit; ``` - [ ] **Step 5: Declare the tools module in src/main.rs** ```rust mod tools; ``` - [ ] **Step 6: Run tests** ```bash cargo test tools::list_backends 2>&1 ``` Expected: all 3 tests pass. - [ ] **Step 7: Commit** ```bash git add src/tools/mod.rs src/tools/list_backends.rs src/main.rs git commit -m "feat: implement list_backends tool handler" ``` --- ## Task 7: Tool — validate_circuit **Files:** - Create: `src/tools/validate_circuit.rs` - [ ] **Step 1: Write the failing test** Create `src/tools/validate_circuit.rs`: ```rust use serde_json::{json, Value}; use crate::executor::{LocalSimulator, MAX_LOCAL_QUBITS}; use crate::types::CircuitSource; pub fn validate_circuit_response(circuit: &str) -> Value { todo!("implement validate_circuit_response") } #[cfg(test)] mod tests { use super::*; const BELL_CIRCUIT: &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;"#; #[test] fn valid_circuit_returns_is_valid_true() { let resp = validate_circuit_response(BELL_CIRCUIT); assert_eq!(resp["is_valid"], true); assert_eq!(resp["diagnostics"].as_array().unwrap().len(), 0); } #[test] fn valid_circuit_includes_qubit_count() { let resp = validate_circuit_response(BELL_CIRCUIT); assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); } #[test] fn invalid_circuit_returns_is_valid_false_with_message() { let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;"); assert_eq!(resp["is_valid"], false); let diags = resp["diagnostics"].as_array().unwrap(); assert!(!diags.is_empty()); assert!(diags[0]["message"].as_str().unwrap().len() > 0); } #[test] fn unsupported_gate_returns_diagnostic_with_gate_name() { let circuit = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; u3(0.1, 0.2, 0.3) q[0];"#; let resp = validate_circuit_response(circuit); assert_eq!(resp["is_valid"], false); let diags = resp["diagnostics"].as_array().unwrap(); let msg = diags[0]["message"].as_str().unwrap(); assert!(msg.to_lowercase().contains("u3"), "msg: {msg}"); } #[test] fn diagnostic_includes_line_and_column() { let circuit = "OPENQASM 3.0;\nnot_valid_qasm;\n"; let resp = validate_circuit_response(circuit); let diag = &resp["diagnostics"][0]; assert!(diag["line"].as_u64().unwrap() >= 1); assert!(diag["column"].as_u64().unwrap() >= 1); } } ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash cargo test tools::validate_circuit 2>&1 | tail -5 ``` Expected: panic at `todo!()`. - [ ] **Step 3: Implement validate_circuit_response** Replace the `todo!()`: ```rust pub fn validate_circuit_response(circuit: &str) -> Value { use crate::validator::CircuitValidator; let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); match validator.validate(&CircuitSource(circuit.to_string())) { Err(e) => json!({ "is_valid": false, "diagnostics": [{"line": 1, "column": 1, "message": e.to_string(), "severity": "error"}], "num_qubits": null, "num_gates": null, }), Ok(result) => { let diagnostics: Vec = result.diagnostics.iter().map(|d| { json!({ "line": d.line, "column": d.column, "message": d.message, "severity": match d.severity { crate::types::DiagnosticSeverity::Error => "error", crate::types::DiagnosticSeverity::Warning => "warning", } }) }).collect(); json!({ "is_valid": result.is_valid, "diagnostics": diagnostics, "num_qubits": result.num_qubits, "num_gates": result.num_gates, }) } } } ``` - [ ] **Step 4: Run tests** ```bash cargo test tools::validate_circuit 2>&1 ``` Expected: all 5 tests pass. - [ ] **Step 5: Commit** ```bash git add src/tools/validate_circuit.rs git commit -m "feat: implement validate_circuit tool handler" ``` --- ## Task 8: Tool — run_circuit **Files:** - Create: `src/tools/run_circuit.rs` - [ ] **Step 1: Write the failing test** Create `src/tools/run_circuit.rs`: ```rust use serde_json::{json, Value}; use crate::types::{CircuitSource, ShotCount}; pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { todo!("implement run_circuit_response") } #[cfg(test)] mod tests { use super::*; const BELL_CIRCUIT: &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;"#; const X_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; x q[0]; c = measure q;"#; #[test] fn bell_circuit_returns_counts_with_total_matching_shots() { let resp = run_circuit_response(BELL_CIRCUIT, 1_000, false); let counts = resp["counts"].as_object().unwrap(); let total: u64 = counts.values().map(|v| v.as_u64().unwrap()).sum(); assert_eq!(total, 1_000); } #[test] fn bell_circuit_only_produces_00_and_11_outcomes() { let resp = run_circuit_response(BELL_CIRCUIT, 1_000, false); let counts = resp["counts"].as_object().unwrap(); for key in counts.keys() { assert!(key == "00" || key == "11", "unexpected key: {key}"); } } #[test] fn x_gate_produces_only_one_outcome() { let resp = run_circuit_response(X_CIRCUIT, 100, false); let counts = resp["counts"].as_object().unwrap(); assert_eq!(counts.get("1").and_then(|v| v.as_u64()), Some(100)); } #[test] fn response_includes_shots_and_execution_time() { let resp = run_circuit_response(BELL_CIRCUIT, 512, false); assert_eq!(resp["shots"].as_u64().unwrap(), 512); assert!(resp["execution_time_ms"].as_f64().unwrap() >= 0.0); } #[test] fn statevector_is_absent_when_not_requested() { let resp = run_circuit_response(BELL_CIRCUIT, 100, false); assert!(resp["statevector"].is_null()); } #[test] fn statevector_has_correct_length_when_requested() { let resp = run_circuit_response(BELL_CIRCUIT, 100, true); let sv = resp["statevector"].as_array().unwrap(); assert_eq!(sv.len(), 4); // 2^2 basis states } #[test] fn invalid_circuit_returns_error_field() { let resp = run_circuit_response("OPENQASM 3.0;\nnot_valid;", 100, false); assert!(resp.get("error").is_some(), "expected 'error' field, got: {resp}"); } #[test] fn shots_clamped_to_max() { // ShotCount::MAX is 100_000 — requesting more should clamp or error let resp = run_circuit_response(BELL_CIRCUIT, ShotCount::MAX.0 + 1, false); // Expect either clamping (shots == MAX) or an error field let has_error = resp.get("error").is_some(); let shots = resp["shots"].as_u64().unwrap_or(0); assert!(has_error || shots == ShotCount::MAX.0 as u64); } } ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash cargo test tools::run_circuit 2>&1 | tail -5 ``` Expected: panic at `todo!()`. - [ ] **Step 3: Implement run_circuit_response** Replace the `todo!()`: ```rust pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { use crate::executor::{CanExecute, CanValidate, LocalSimulator}; use crate::validator::CircuitValidator; use crate::executor::MAX_LOCAL_QUBITS; // Clamp shots to max let shot_count = ShotCount(shots.min(ShotCount::MAX.0)); // Validate first — return structured error if invalid let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); let validation = match validator.validate(&CircuitSource(circuit.to_string())) { Err(e) => return json!({"error": e.to_string()}), Ok(v) => v, }; if !validation.is_valid { let messages: Vec<&str> = validation.diagnostics.iter().map(|d| d.message.as_str()).collect(); return json!({ "error": format!("circuit validation failed: {}", messages.join("; ")) }); } let sim = LocalSimulator::new(); match sim.run(&CircuitSource(circuit.to_string()), shot_count, return_statevector) { Err(e) => json!({"error": e.to_string()}), Ok(result) => { let statevector: Value = match result.statevector { None => Value::Null, Some(sv) => sv.iter() .map(|(r, i)| json!([r, i])) .collect::>() .into(), }; json!({ "counts": result.counts, "shots": result.shots, "execution_time_ms": result.execution_time_ms, "statevector": statevector, "backend": "local_simulator", }) } } } ``` - [ ] **Step 4: Run tests** ```bash cargo test tools::run_circuit 2>&1 ``` Expected: all 8 tests pass. - [ ] **Step 5: Commit** ```bash git add src/tools/run_circuit.rs git commit -m "feat: implement run_circuit tool handler" ``` --- ## Task 9: MCP Server — main.rs with rmcp **Files:** - Modify: `src/main.rs` (replace stub) - Modify: `src/tools/mod.rs` (add QuantumBridgeServer) - [ ] **Step 1: Write the failing compilation test** First, write a compile-only test in `src/tools/mod.rs` to verify the struct can be created: ```rust pub mod list_backends; pub mod run_circuit; pub mod validate_circuit; use rmcp::{ ErrorData as McpError, ServerHandler, handler::server::{router::tool::ToolRouter, wrapper::Parameters}, model::*, tool, tool_handler, tool_router, }; use schemars::JsonSchema; use serde::Deserialize; use crate::tools::list_backends::list_backends_response; use crate::tools::run_circuit::run_circuit_response; use crate::tools::validate_circuit::validate_circuit_response; #[derive(Debug, Deserialize, JsonSchema)] pub struct ValidateCircuitParams { /// OpenQASM 3.0 source string to validate. pub circuit: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct RunCircuitParams { /// OpenQASM 3.0 source string to execute. pub circuit: String, /// Number of measurement shots (default 1024, max 100 000). #[serde(default = "default_shots")] pub shots: u32, /// Whether to include the full statevector in the response. #[serde(default)] pub return_statevector: bool, } fn default_shots() -> u32 { 1024 } #[derive(Clone)] pub struct QuantumBridgeServer { tool_router: ToolRouter, } #[tool_router] impl QuantumBridgeServer { pub fn new() -> Self { Self { tool_router: Self::tool_router() } } #[tool(description = "List available quantum simulation backends and their capabilities.")] async fn list_backends(&self) -> Result { let json = list_backends_response(); Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } #[tool(description = "Parse and validate an OpenQASM 3.0 circuit. Returns structured diagnostics with line/column for each error.")] async fn validate_circuit( &self, Parameters(ValidateCircuitParams { circuit }): Parameters, ) -> Result { let json = validate_circuit_response(&circuit); Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } #[tool(description = "Execute an OpenQASM 3.0 circuit on the local statevector simulator. Returns measurement counts, execution time, and optionally the full statevector.")] async fn run_circuit( &self, Parameters(RunCircuitParams { circuit, shots, return_statevector }): Parameters, ) -> Result { let json = run_circuit_response(&circuit, shots, return_statevector); if json.get("error").is_some() { return Err(McpError::invalid_params( json["error"].as_str().unwrap_or("unknown error"), None, )); } Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } } #[tool_handler] impl ServerHandler for QuantumBridgeServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder() .enable_tools() .build(), ) .with_server_info(Implementation::from_build_env()) .with_protocol_version(ProtocolVersion::V_2024_11_05) .with_instructions( "Quantum circuit simulator MCP server. Accepts OpenQASM 3.0 circuits. \ Use validate_circuit before run_circuit to get actionable error messages.".to_string(), ) } } #[cfg(test)] mod tests { use super::*; #[test] fn quantum_bridge_server_can_be_constructed() { let _server = QuantumBridgeServer::new(); } } ``` - [ ] **Step 2: Run compile test** ```bash cargo test tools::tests 2>&1 ``` Expected: compiles and the construction test passes. If `#[tool_router]` / `#[tool_handler]` macro names are wrong, check `cargo doc --open` for the rmcp crate or look at `~/.cargo/git/checkouts/rust-sdk-*/examples/`. - [ ] **Step 3: Write src/main.rs** ```rust mod error; mod executor; mod tools; mod types; mod validator; use anyhow::Result; use rmcp::{ServiceExt, transport::stdio}; use tools::QuantumBridgeServer; #[tokio::main] async fn main() -> Result<()> { // All logs go to stderr — stdout is the MCP JSON-RPC channel. 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 binary compiles** ```bash cargo build 2>&1 ``` Expected: compiles without errors. Warnings about unused imports are acceptable at this stage. - [ ] **Step 5: Smoke test — send a JSON-RPC initialize message** ```bash echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}' | cargo run 2>/dev/null ``` Expected: a JSON response containing `"result"` with `"serverInfo"` including `"name":"quantum-bridge-mcp"`. The server will then wait for more input — press Ctrl+C. - [ ] **Step 6: Commit** ```bash git add src/main.rs src/tools/mod.rs git commit -m "feat: wire rmcp stdio server with list_backends/validate_circuit/run_circuit tools" ``` --- ## Task 10: Integration Tests — MCP protocol conformance **Files:** - Create: `tests/integration/mcp_protocol.rs` - Create: `tests/integration/mod.rs` - [ ] **Step 1: Create test infrastructure** ```bash mkdir -p tests/integration ``` Create `tests/integration/mod.rs`: ```rust // Integration test module — empty re-export for cargo test discovery. ``` - [ ] **Step 2: Write the integration tests** Create `tests/integration/mcp_protocol.rs`: ```rust //! Roundtrip tests: spawn the binary, send JSON-RPC messages 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 quantum-bridge-mcp 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"}}}"#); let line = proc.recv_line(); serde_json::from_str(&line).expect("initialize response is 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 = r#"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_with_x_gate_returns_count_of_1_for_all_shots() { let x_circ = r#"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:** The integration tests use `env!("CARGO_BIN_EXE_quantum-bridge-mcp")` which cargo sets to the compiled binary path. Run with `cargo test --test integration` — not with the plain `cargo test` that only runs unit tests. - [ ] **Step 3: Run integration tests (requires binary build)** ```bash cargo test --test integration 2>&1 ``` Expected: all 5 tests pass. If the MCP framing uses length-prefixed lines rather than newlines, adjust `recv_line` accordingly (check rmcp's stdio transport output format). - [ ] **Step 4: 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 **Files:** - Create: `tests/proptest/invariants.rs` - Create: `tests/proptest/mod.rs` - [ ] **Step 1: Create proptest directory** ```bash mkdir -p tests/proptest ``` Create `tests/proptest/mod.rs`: ```rust // Property-based test module. ``` - [ ] **Step 2: Write the property tests** Create `tests/proptest/invariants.rs`: ```rust //! Quantum mechanics invariants: any valid circuit must preserve unitarity and normalisation. use proptest::prelude::*; use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; // Make crate types visible — add `pub` to modules in lib.rs (see Step 3). proptest! { /// Normalisation: 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 within floating-point tolerance. #[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 correct length (= number of qubits). #[test] fn count_bitstrings_have_correct_length( n_qubits in 1usize..=5usize, shots in 10u32..=100u32 ) { let qubit_decls: 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{qubit_decls}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, "bitstring '{key}' has wrong length for {n_qubits}-qubit circuit" ); } } } ``` - [ ] **Step 3: Expose crate internals for tests** Proptest files live in `tests/` and need `use quantum_bridge_mcp::...`. Add a `src/lib.rs` so the crate has a library target: Create `src/lib.rs`: ```rust pub mod error; pub mod executor; pub mod types; pub mod validator; pub(crate) mod tools; ``` Update `src/main.rs` — remove the `mod` declarations that are now in `lib.rs`: ```rust // Remove: mod error; mod executor; mod types; mod validator; mod tools; // Keep only: use quantum_bridge_mcp::tools::QuantumBridgeServer; // or use crate path use anyhow::Result; 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(()) } ``` Add to `Cargo.toml`: ```toml [lib] name = "quantum_bridge_mcp" path = "src/lib.rs" ``` - [ ] **Step 4: Run proptest** ```bash cargo test --test proptest 2>&1 ``` Expected: all 3 property tests pass (each runs 256 cases by default). - [ ] **Step 5: Commit** ```bash git add tests/proptest/ src/lib.rs src/main.rs Cargo.toml 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 the bench file** ```bash mkdir -p benches ``` Create `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_CIRCUIT: &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_qubits: usize, n_shots: u32) -> (CircuitSource, ShotCount) { 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" ); (CircuitSource(circuit), ShotCount(n_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_CIRCUIT.to_string()), ShotCount(1024), false, ) .unwrap() }) }); } fn bench_10_qubits_10k_shots(c: &mut Criterion) { let sim = LocalSimulator::new(); let (circuit, shots) = make_h_circuit(10, 10_000); c.bench_function("10_qubits_10k_shots", |b| { b.iter(|| sim.run(&circuit, shots, false).unwrap()) }); } fn bench_20_qubits_10k_shots(c: &mut Criterion) { let sim = LocalSimulator::new(); let (circuit, shots) = make_h_circuit(20, 10_000); c.bench_function("20_qubits_10k_shots", |b| { b.iter(|| sim.run(&circuit, 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 to establish baseline** ```bash cargo bench 2>&1 | tail -20 ``` Expected output includes: ``` bell_1024_shots time: [X ms ...] 10_qubits_10k_shots time: [X ms ...] 20_qubits_10k_shots time: [X ms ...] ``` 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 almost certainly in `extract_gate_ops` (O(n) loop with AST allocation). Profile with `cargo bench -- --profile-time 5` and check if the AST is being cloned unnecessarily. - [ ] **Step 3: 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 + gen_reference.py **Files:** - Create: `tests/reference/bell.qasm` - Create: `tests/reference/bell_counts.json` - Create: `tests/reference/ghz3.qasm` - Create: `tests/reference/ghz3_counts.json` - Create: `tests/reference/invalid/unsupported_gate.qasm` - Create: `tests/reference/invalid/unsupported_gate.error.txt` - Create: `tests/reference/invalid/undeclared_qubit.qasm` - Create: `tests/reference/invalid/undeclared_qubit.error.txt` - Create: `tests/reference/invalid/syntax_error.qasm` - Create: `tests/reference/invalid/syntax_error.error.txt` - Create: `tests/reference/invalid/qubit_limit.qasm` - Create: `tests/reference/invalid/qubit_limit.error.txt` - Create: `scripts/gen_reference.py` - Create: `tests/reference/cross_validate.rs` (Rust test that reads the golden files) - [ ] **Step 1: Create reference 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 gen_reference.py** Create `scripts/gen_reference.py`: ```python #!/usr/bin/env python3 """ Generate reference counts and statevectors via Qiskit Aer. Run in CI to populate tests/reference/*.json. Requires: pip install qiskit qiskit-aer """ import json import 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") job = sim.run(qc, shots=SHOTS, seed_simulator=SEED) result = job.result() counts = result.get_counts() # Statevector from noiseless zero-shot run qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path)) qc_sv.save_statevector() sv_job = sim.run(qc_sv, shots=0) sv = list(sv_job.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], } def validate_mode() -> bool: """--validate: check that Spinoza counts match Qiskit within chi-squared tolerance.""" import subprocess, scipy.stats as stats all_pass = True for qasm in REF_DIR.glob("*.qasm"): ref_file = qasm.with_suffix(".json") if not ref_file.exists(): print(f"MISSING {ref_file}") all_pass = False continue ref = json.loads(ref_file.read_text()) # Run against the local binary result = subprocess.run( ["cargo", "run", "--", "--validate-only", str(qasm)], capture_output=True, text=True ) # Parse Spinoza counts from stdout and chi-squared compare with ref["counts"] # ... (implementation depends on CLI output format — add --validate-only flag in V1.5) print(f"SKIP {qasm.name} (--validate-only CLI flag not implemented in V1)") return all_pass if __name__ == "__main__": if "--validate" in sys.argv: sys.exit(0 if validate_mode() else 1) 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 cross-validation Rust test** Create `tests/reference/cross_validate.rs`: ```rust //! Reads the golden .json files and verifies that LocalSimulator produces //! statistically compatible counts (chi-squared, α=0.01, N=10k shots, seed=42). use std::collections::HashMap; use std::path::Path; use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; fn load_circuit(name: &str) -> CircuitSource { let path = Path::new("tests/reference").join(name); CircuitSource(std::fs::read_to_string(path).unwrap()) } fn load_expected_counts(name: &str) -> HashMap { let path = Path::new("tests/reference").join(name); if !path.exists() { // JSON not generated yet (Qiskit not installed) — skip return HashMap::new(); } let text = std::fs::read_to_string(path).unwrap(); let data: serde_json::Value = serde_json::from_str(&text).unwrap(); data["counts"] .as_object() .unwrap() .iter() .map(|(k, v)| (k.clone(), v.as_u64().unwrap())) .collect() } fn chi_squared_passes(observed: &HashMap, expected: &HashMap, total: u64) -> bool { if expected.is_empty() { return true; } // skip if no golden file let alpha = 0.01; // Degrees of freedom = number of outcomes - 1 let chi_sq: f64 = expected.iter().map(|(k, &exp_count)| { let obs_count = *observed.get(k).unwrap_or(&0) as f64; let exp_f = exp_count as f64; if exp_f == 0.0 { 0.0 } else { (obs_count - exp_f).powi(2) / exp_f } }).sum(); let df = (expected.len() - 1) as f64; // Rough critical value for chi-squared at α=0.01 — exact table lookup would need a stats crate // For df=1 (Bell): critical ≈ 6.63; for df=7 (3-qubit GHZ): ≈ 18.5 let critical = df * 3.0 + 6.63; // conservative upper bound chi_sq < critical } #[test] fn bell_counts_match_qiskit_golden() { let sim = LocalSimulator::new(); let circuit = load_circuit("bell.qasm"); let expected = load_expected_counts("bell_counts.json"); if expected.is_empty() { return; } // Qiskit not available in this environment let result = sim.run(&circuit, ShotCount(10_000), false).unwrap(); assert!( chi_squared_passes(&result.counts, &expected, 10_000), "Bell counts diverge from Qiskit golden file.\n\ Spinoza: {:?}\nQiskit: {:?}", result.counts, expected ); } #[test] fn ghz3_counts_match_qiskit_golden() { let sim = LocalSimulator::new(); let circuit = load_circuit("ghz3.qasm"); let expected = load_expected_counts("ghz3_counts.json"); if expected.is_empty() { return; } let result = sim.run(&circuit, ShotCount(10_000), false).unwrap(); assert!( chi_squared_passes(&result.counts, &expected, 10_000), "GHZ-3 counts diverge from Qiskit golden file.\n\ Spinoza: {:?}\nQiskit: {:?}", result.counts, expected ); } #[test] fn invalid_circuits_produce_validation_errors() { use quantum_bridge_mcp::validator::CircuitValidator; use quantum_bridge_mcp::executor::MAX_LOCAL_QUBITS; use std::fs; let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); let invalid_dir = Path::new("tests/reference/invalid"); for entry in fs::read_dir(invalid_dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.extension().map(|e| e == "qasm").unwrap_or(false) { let error_file = path.with_extension("error.txt"); let expected_fragment = fs::read_to_string(&error_file) .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 all_messages: String = result.diagnostics .iter() .map(|d| d.message.to_lowercase()) .collect::>() .join(" "); assert!( all_messages.contains(&expected_fragment), "expected '{}' in messages for {}\nGot: {}", expected_fragment, path.display(), all_messages ); } } } ``` - [ ] **Step 4: Run the 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 (golden JSON not generated yet); `invalid_circuits_produce_validation_errors` passes. To generate the golden files (needs Python + Qiskit): ```bash python3 scripts/gen_reference.py cargo test --test cross_validate 2>&1 ``` - [ ] **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: CI — GitHub Actions **Files:** - Create: `.github/workflows/ci.yml` - Create: `.github/workflows/release.yml` - [ ] **Step 1: Write ci.yml** Create `.github/workflows/ci.yml`: ```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 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - name: Check formatting run: cargo fmt --check - name: Clippy (deny warnings) run: cargo clippy -- -D warnings - name: Run unit and integration tests run: cargo test - name: Run benchmarks (smoke — compile + one iteration) run: cargo bench -- --test cross-validate: name: Cross-validate with Qiskit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: "3.11" - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-qiskit restore-keys: ${{ runner.os }}-pip- - name: Install Qiskit Aer run: pip install qiskit qiskit-aer - name: Generate reference golden files run: python3 scripts/gen_reference.py - name: Run cross-validation tests run: cargo test --test cross_validate ``` - [ ] **Step 2: Write release.yml** Create `.github/workflows/release.yml`: ```yaml name: Release on: push: tags: - "v*" jobs: dist: name: Build release binaries 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 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Install cross (Linux cross-compilation) if: matrix.os == 'ubuntu-latest' && matrix.target == 'aarch64-unknown-linux-gnu' run: cargo install cross - name: Build run: | if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then cross build --release --target ${{ matrix.target }} else cargo build --release --target ${{ matrix.target }} fi shell: bash - 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' 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 ../../../ shell: pwsh - name: Upload release artifact uses: actions/upload-artifact@v4 with: name: quantum-bridge-mcp-${{ matrix.target }} path: quantum-bridge-mcp-${{ matrix.target }}.* github-release: name: Create GitHub Release needs: dist runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@v4 with: merge-multiple: true - name: Create release uses: softprops/action-gh-release@v2 with: files: "quantum-bridge-mcp-*" generate_release_notes: true ``` - [ ] **Step 3: Verify CI config is valid YAML** ```bash python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo OK python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK ``` Expected: both print `OK`. - [ ] **Step 4: Run the full verification suite locally** ```bash cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -20 ``` Expected: all green. - [ ] **Step 5: Final commit** ```bash git add .github/ git commit -m "ci: add GitHub Actions for Rust checks, cross-validation, and multi-platform release" ``` --- ## Spec Coverage Checklist Self-review against `docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md`: | Spec requirement | Covered by task | |---|---| | `list_backends` tool | Task 6 | | `validate_circuit` tool (line/col diagnostics) | Tasks 4, 7 | | `run_circuit` tool (counts + optional statevector) | Tasks 5, 8 | | Gates: H, X, Y, Z, S, T, Sdg, Tdg, RX, RY, RZ, CX, CZ, SWAP, CCX | Task 5 | | MAX\_LOCAL\_QUBITS = 28 | Tasks 2, 4, 5 | | Unsupported gate → explicit error with gate name + supported list | Tasks 4, 7 | | rmcp stdio server | Task 9 | | Trait `Backend` (OCP — IBM extension point) | Task 3 | | DIP: tools depend on `&dyn Backend` | Task 3 (traits), Tasks 6–8 | | Unit tests for gates | Task 5 | | Statistical count tests (chi-squared / ratio) | Tasks 5, 11 | | Cross-validation with Qiskit | Task 13 | | Property-based tests (proptest) | Task 11 | | Golden files for invalid circuits | Task 13 | | MCP protocol roundtrip tests | Task 10 | | Criterion benchmarks (Bell < 5 ms, 20q < 500 ms) | Task 12 | | CI green (lint + tests + cross-val) | Task 14 | | Multi-platform release binaries | Task 14 | | No `unwrap`/`panic!` in production code | Enforced throughout | | `--f32` mode flag | **Not covered** — add as `clap` CLI arg in a follow-up | | Homebrew tap | **Not covered** — post-V1 per spec | **One gap identified:** the `--f32` mode (spec §6, §7) for 30% speedup is not implemented. This is a small addition: add `clap` as a dependency, parse a `--f32` flag in `main.rs`, and pass it as a `use_f32: bool` field on `LocalSimulator`. Add to a follow-up task or V1 polish.