diff --git a/docs/superpowers/plans/2026-07-26-commute-subcommand.md b/docs/superpowers/plans/2026-07-26-commute-subcommand.md new file mode 100644 index 0000000..8d907da --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-commute-subcommand.md @@ -0,0 +1,181 @@ +# `commute` Subcommand 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:** Add `qsym commute `, printing exactly `commute` or `anticommute` while preserving the CLI's existing validation behavior. + +**Architecture:** Extend the existing clap `Cmd` enum and `run` dispatcher in `src/main.rs`. The new branch will reuse `parse_pair` for parsing and equal-length validation, then call the already-tested `PauliString::commutes`; black-box CLI tests will verify both parity outcomes and both error paths. + +**Tech Stack:** Rust 2024 edition, clap 4 derive API, Rust integration tests, GNU Make, Conda environment `qsym-rs`. + +## Global Constraints + +- The production-code change is limited to `src/main.rs`; persistent integration tests are added to `tests/cli.rs`. +- `commute X Z` must print exactly `anticommute`. +- `commute XX ZZ` must print exactly `commute`. +- Invalid Pauli letters and unequal operand lengths must use the existing nonzero `error: ` path. +- Reuse `parse_pair(&a, &b)?`; do not duplicate parsing or length validation. +- Do not change `src/lib.rs`, `PauliString::commutes`, existing `Mul`/`Show`/`Weight` behavior, dependencies, JSON output, or verbose output. +- Run every Rust command inside Conda environment `qsym-rs`. + +## File Map + +- Modify `tests/cli.rs`: add four black-box tests for odd parity, even parity, unequal lengths, and an invalid Pauli letter. +- Modify `src/main.rs`: add the clap subcommand variant and its `run` match arm. +- Reference only `src/lib.rs`: use the existing `PauliString::commutes(&self, other: &PauliString) -> bool`; make no edits. + +--- + +### Task 1: Add the `commute` CLI Subcommand + +**Files:** +- Modify: `tests/cli.rs:28` +- Modify: `src/main.rs:16-39` +- Modify: `src/main.rs:55-97` +- Test: `tests/cli.rs` + +**Interfaces:** +- Consumes: `parse_pair(a: &str, b: &str) -> Result<(PauliString, PauliString), String>` from `src/main.rs`. +- Consumes: `PauliString::commutes(&self, other: &PauliString) -> bool` from `src/lib.rs`. +- Produces: private clap variant `Cmd::Commute { a: String, b: String }`. +- Produces: stdout token `commute` when the method returns `true`, or `anticommute` when it returns `false`. + +- [ ] **Step 1: Write four failing CLI tests** + +Insert these tests in `tests/cli.rs` after `mul_rejects_bad_input`: + +```rust +#[test] +fn commute_reports_anticommutation_for_odd_parity() { + let (stdout, stderr, ok) = run(&["commute", "X", "Z"]); + assert!(ok, "stderr was: {stderr}"); + assert_eq!(stdout, "anticommute"); +} + +#[test] +fn commute_reports_commutation_for_even_parity() { + let (stdout, stderr, ok) = run(&["commute", "XX", "ZZ"]); + assert!(ok, "stderr was: {stderr}"); + assert_eq!(stdout, "commute"); +} + +#[test] +fn commute_rejects_length_mismatch() { + let (stdout, stderr, ok) = run(&["commute", "X", "ZZ"]); + assert!(!ok, "length mismatch must exit non-zero"); + assert!(stdout.is_empty(), "stdout was: {stdout}"); + assert!( + stderr.contains("error: length mismatch"), + "stderr was: {stderr}" + ); +} + +#[test] +fn commute_rejects_bad_pauli_letter() { + let (stdout, stderr, ok) = run(&["commute", "XQ", "ZZ"]); + assert!(!ok, "invalid Pauli letter must exit non-zero"); + assert!(stdout.is_empty(), "stdout was: {stdout}"); + assert!( + stderr.contains("error: not a Pauli letter: Q"), + "stderr was: {stderr}" + ); +} +``` + +- [ ] **Step 2: Run the focused tests and verify the red state** + +Run: + +```bash +conda run -n qsym-rs cargo test --test cli commute_ -- --nocapture +``` + +Expected: all four new tests fail because clap reports `commute` as an unrecognized subcommand. This proves the tests detect the missing feature rather than passing against existing behavior. + +- [ ] **Step 3: Add the minimal command implementation** + +In `src/main.rs`, add this variant immediately after `Cmd::Mul`: + +```rust + /// Report whether two equal-length Pauli strings commute. + Commute { a: String, b: String }, +``` + +In `run`, add this match arm immediately after the `Cmd::Mul` arm: + +```rust + Cmd::Commute { a, b } => { + let (pa, pb) = parse_pair(&a, &b)?; + println!( + "{}", + if pa.commutes(&pb) { + "commute" + } else { + "anticommute" + } + ); + Ok(()) + } +``` + +Do not add a helper, modify `parse_pair`, or edit `src/lib.rs`. + +- [ ] **Step 4: Run the focused tests and verify the green state** + +Run: + +```bash +conda run -n qsym-rs cargo test --test cli commute_ -- --nocapture +``` + +Expected: four tests pass. The odd-parity case prints `anticommute`, the even-parity case prints `commute`, and both invalid inputs return nonzero with no stdout success token. + +- [ ] **Step 5: Run the existing multiplication regression test** + +Run: + +```bash +conda run -n qsym-rs cargo test --test cli mul_subcommand_prints_product -- --exact +``` + +Expected: one test passes and continues to verify that `mul XZ ZX` prints `+YY`. + +- [ ] **Step 6: Run the full repository quality gate** + +Run: + +```bash +conda run -n qsym-rs make check +``` + +Expected: formatting check, clippy with warnings denied, all CLI tests, all engine tests, and documentation tests pass. + +- [ ] **Step 7: Audit the final scope** + +Run: + +```bash +git diff --check +git diff -- src/main.rs tests/cli.rs +git status --short +``` + +Expected: no whitespace errors; only `src/main.rs` and `tests/cli.rs` are modified; `src/lib.rs`, dependencies, and unrelated commands are unchanged. + +- [ ] **Step 8: Commit the tested feature** + +```bash +git add src/main.rs tests/cli.rs +git commit -m "feat(cli): add commute subcommand" +``` + +- [ ] **Step 9: Verify the committed state** + +Run: + +```bash +git status --short +conda run -n qsym-rs make check +``` + +Expected: the worktree is clean and the complete quality gate still passes on the committed feature. diff --git a/docs/superpowers/specs/2026-07-26-commute-subcommand-design.md b/docs/superpowers/specs/2026-07-26-commute-subcommand-design.md new file mode 100644 index 0000000..1efb6dd --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-commute-subcommand-design.md @@ -0,0 +1,89 @@ +# `commute` Subcommand Design + +## Context + +GitHub issue +[#1](https://github.com/QuantumBFS/qsym-rs/issues/1) asks for a +`qsym commute ` command. The library already implements and tests +`PauliString::commutes`; the missing piece is a CLI entry point. + +The issue says the implementation touches only `src/main.rs`. For this +test-driven training exercise, that means the production-code change is +limited to `src/main.rs`; persistent integration tests will also be added to +`tests/cli.rs`. + +## Goal + +Expose the existing commutation check through the CLI: + +- Print `anticommute` for an odd number of anticommuting sites, such as + `qsym commute X Z`. +- Print `commute` for an even number, such as `qsym commute XX ZZ`. +- Preserve the same parse and equal-length validation used by `qsym mul`. + +## Scope + +The production change consists of: + +1. A `Commute { a: String, b: String }` variant in the `Cmd` enum. +2. A `Cmd::Commute` arm in `run`. +3. Reuse of `parse_pair(&a, &b)?`. +4. A call to `pa.commutes(&pb)`, followed by one exact output token. + +The test change adds CLI integration coverage in `tests/cli.rs`. + +Out of scope: + +- No changes to `src/lib.rs` or `PauliString::commutes`. +- No changes to the existing `Mul`, `Show`, or `Weight` behavior. +- No JSON or verbose output for `commute`. +- No new dependency or generalized command-dispatch abstraction. + +## Command Flow + +```text +CLI arguments + → clap parses Cmd::Commute { a, b } + → run calls parse_pair(a, b) + → parse_pair parses both strings and checks equal length + → PauliString::commutes computes even/odd anticommutation parity + → stdout receives exactly "commute" or "anticommute" +``` + +This follows the existing two-operand `Mul` path and keeps `parse_pair` as the +single source of truth for CLI length validation. + +## Error Handling + +`parse_pair` returns `Err(String)` for: + +- An invalid Pauli letter from either operand. +- Unequal operand lengths. + +The `?` operator propagates that error from `run` to `main`. The existing +shared error path then prints `error: ` to stderr and returns a +nonzero exit code. The new command must not call `PauliString::commutes` until +validation succeeds, preventing its internal equal-length assertion from +panicking. + +## Test-Driven Verification + +Add failing CLI integration tests before production code: + +1. `commute X Z` succeeds and prints exactly `anticommute`. +2. `commute XX ZZ` succeeds and prints exactly `commute`. +3. `commute X ZZ` fails, prints no success token, and reports + `error: length mismatch` on stderr. +4. `commute XQ ZZ` fails and reports the existing bad-Pauli parse error. + +The existing `mul_subcommand_prints_product` test remains the regression check +that `mul XZ ZX` still prints `+YY`. + +The final quality gate is: + +```bash +conda run -n qsym-rs make check +``` + +It must pass formatting, clippy with warnings denied, all CLI tests, all +engine tests, and documentation tests. diff --git a/src/main.rs b/src/main.rs index 50bf23f..4f1ac71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,8 @@ enum Cmd { #[arg(long)] json: bool, }, + /// Report whether two equal-length Pauli strings commute. + Commute { a: String, b: String }, /// Inspect one Pauli string: its normalized form, length, weight, and sign, /// e.g. `qsym show XIZ` Show { @@ -68,6 +70,18 @@ fn run(cmd: Cmd) -> Result<(), String> { } Ok(()) } + Cmd::Commute { a, b } => { + let (pa, pb) = parse_pair(&a, &b)?; + println!( + "{}", + if pa.commutes(&pb) { + "commute" + } else { + "anticommute" + } + ); + Ok(()) + } Cmd::Show { a, json } => { let p = PauliString::parse(&a)?; if json { diff --git a/tests/cli.rs b/tests/cli.rs index 3c8b084..873a457 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -25,6 +25,42 @@ fn mul_rejects_bad_input() { assert!(!ok, "invalid Pauli letter must exit non-zero"); } +#[test] +fn commute_reports_anticommutation_for_odd_parity() { + let (stdout, stderr, ok) = run(&["commute", "X", "Z"]); + assert!(ok, "stderr was: {stderr}"); + assert_eq!(stdout, "anticommute"); +} + +#[test] +fn commute_reports_commutation_for_even_parity() { + let (stdout, stderr, ok) = run(&["commute", "XX", "ZZ"]); + assert!(ok, "stderr was: {stderr}"); + assert_eq!(stdout, "commute"); +} + +#[test] +fn commute_rejects_length_mismatch() { + let (stdout, stderr, ok) = run(&["commute", "X", "ZZ"]); + assert!(!ok, "length mismatch must exit non-zero"); + assert!(stdout.is_empty(), "stdout was: {stdout}"); + assert!( + stderr.contains("error: length mismatch"), + "stderr was: {stderr}" + ); +} + +#[test] +fn commute_rejects_bad_pauli_letter() { + let (stdout, stderr, ok) = run(&["commute", "XQ", "ZZ"]); + assert!(!ok, "invalid Pauli letter must exit non-zero"); + assert!(stdout.is_empty(), "stdout was: {stdout}"); + assert!( + stderr.contains("error: not a Pauli letter: Q"), + "stderr was: {stderr}" + ); +} + #[test] fn mul_json_flag_prints_json_object() { // Issue #2, verification 1.