Files
quantum-bridge-mcp/docs/superpowers/plans/sub5-quality-ci.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

23 KiB
Raw Blame History

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

mkdir -p tests/integration
  • Step 2: Create tests/integration/mod.rs
// Integration test module — empty file for cargo test discovery.
  • Step 3: Create tests/integration/mcp_protocol.rs
//! 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
cargo test --test integration 2>&1

Expected: all 5 pass.

  • Step 5: Commit
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

pub mod error;
pub mod executor;
pub mod types;
pub mod validator;
pub(crate) mod tools;
  • Step 2: Add [lib] to Cargo.toml
[lib]
name = "quantum_bridge_mcp"
path = "src/lib.rs"
  • Step 3: Update src/main.rs — remove mod declarations, use crate name
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
cargo test 2>&1

Expected: all existing tests still pass.

  • Step 5: Create proptest files
mkdir -p tests/proptest

tests/proptest/mod.rs:

// Property-based test module.

tests/proptest/invariants.rs:

//! 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
cargo test --test proptest 2>&1

Expected: 3 property tests pass (each runs 256 cases).

  • Step 7: Commit
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

mkdir -p benches

benches/simulation.rs:

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)
cargo bench -- --test 2>&1

Expected: all three benchmark functions report a timing without panicking.

  • Step 3: Run full benchmarks to check SLAs
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
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

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
#!/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
//! 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
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):

python3 scripts/gen_reference.py && cargo test --test cross_validate
  • Step 5: Commit
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

mkdir -p .github/workflows
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
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
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
cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10

Expected: all green.

  • Step 5: Commit
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

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.