Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions docs/superpowers/plans/2026-07-26-commute-subcommand.md
Original file line number Diff line number Diff line change
@@ -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 <A> <B>`, 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: <message>` 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.
89 changes: 89 additions & 0 deletions docs/superpowers/specs/2026-07-26-commute-subcommand-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# `commute` Subcommand Design

## Context

GitHub issue
[#1](https://github.com/QuantumBFS/qsym-rs/issues/1) asks for a
`qsym commute <A> <B>` 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: <message>` 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.
14 changes: 14 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading