Grow mutation-testing.md and scope versioning.md to CLIs
Mutation-testing now opens with a worked example showing how a weak
predicate assertion (>0) hides body-replacement mutants and how
tightening to assert_eq! catches them. Versioning gains a one-line
header noting it's CLI-specific; servers use Docker image tags.
Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
diff --git a/ai/skills/rust/mutation-testing.md b/ai/skills/rust/mutation-testing.md
index df347a35..5a77be04 100644
--- a/ai/skills/rust/mutation-testing.md
+++ b/ai/skills/rust/mutation-testing.md
@@ -1,6 +1,9 @@
-# Mutation Testing
+# Mutation testing
-`cargo mutants` modifies your source code (replacing operators, deleting calls, changing return values) and checks that at least one test fails for each mutation. Surviving mutants indicate code that tests execute but don't actually verify.
+`cargo mutants` modifies your source code (replacing operators,
+deleting calls, changing return values) and checks that at least one
+test fails for each mutation. Surviving mutants indicate code that
+tests execute but don't actually verify.
Run mutation testing separately from coverage — it's slow:
@@ -8,11 +11,85 @@ Run mutation testing separately from coverage — it's slow:
just mutants
```
-## Excluding Equivalent Mutants
+## Workflow
+
+1. Run `just mutants` after reaching 100% coverage.
+2. For each surviving mutant, tighten an existing assertion (or
+ add one) so the mutated behavior fails the test (see worked
+ example below).
+3. If a mutant is equivalent (same observable behavior) or
+ unreachable (dead code behind `unreachable!`), add it to
+ `.cargo/mutants.toml` with an explanation.
+4. CI enforces zero missed mutants — a surviving mutant fails the
+ build.
+
+## Worked example: weak assertions hide bugs
+
+A function with full line coverage and a passing test:
+
+```rust
+// src/order.rs
+pub fn total(items: &[Item]) -> u32 {
+ items.iter().map(|i| i.price).sum()
+}
+```
+
+```rust
+#[test]
+fn computes_total() {
+ let items = vec![Item::new(10), Item::new(20)];
+ assert!(total(&items) > 0);
+}
+```
+
+`just mutants` reports:
+
+```
+src/order.rs:3:5: replace total -> u32 with 1 ... NOT CAUGHT
+```
-Some mutations produce equivalent programs. For example, changing `> 1` to `>= 1` when a prior branch already handled `== 1`. These can't be caught because the mutant behaves identically to the original.
+`cargo-mutants` replaced the function body with `1`. The test still
+passes — `1 > 0` is true. Coverage saw the line execute; the
+assertion didn't notice the body was gutted.
-Document exclusions in `.cargo/mutants.toml` with regex patterns and a comment explaining why each is equivalent or unreachable:
+The mutant points at a weak assertion. Tighten it from a property
+("greater than zero") to a value ("exactly thirty"):
+
+```rust
+#[test]
+fn computes_total() {
+ let items = vec![Item::new(10), Item::new(20)];
+ assert_eq!(total(&items), 30);
+}
+```
+
+Re-run `just mutants`. The `with 1` mutant now fails (`assert_eq!(1,
+30)` panics) and is caught. Other body-replacement mutants (`with
+0`, `with u32::MAX`) are caught by the same assertion.
+
+The lesson is general: every `assert!` predicate is a hint that the
+test could be tighter. Replacing `>` with `==` (or `assert!` with
+`assert_eq!`) usually buys mutation coverage for free.
+
+## Where surviving mutants tend to live
+
+- Functions whose tests assert on a property (`> 0`, `is_some()`,
+ `len() > 1`) instead of a specific value.
+- Side-effect functions called for their effect but not verified
+ (`logger.record("x")` called, `logger.entries` not inspected).
+- Boundary conditions where the only tested inputs sit far from the
+ boundary, so `<` vs `<=` produce the same result.
+- Error-construction code where the variant is matched but the
+ payload isn't.
+
+## Excluding equivalent mutants
+
+Some mutations produce equivalent programs and can never be caught.
+For example, changing `> 1` to `>= 1` when an earlier branch already
+handled `== 1` — both versions behave identically.
+
+Document each exclusion in `.cargo/mutants.toml` with a regex
+pattern and a comment explaining why it's equivalent or unreachable:
```toml
# Equivalent mutant: the `== 1` branch above already handles len 1,
@@ -22,11 +99,5 @@ exclude_re = [
]
```
-Every exclusion needs a comment. Undocumented exclusions hide real gaps.
-
-## Workflow
-
-1. Run `just mutants` after reaching 100% coverage.
-2. Surviving mutants mean tests reach the code but don't assert on its behavior. Add targeted assertions.
-3. If a mutant is equivalent (same behavior) or unreachable (dead code behind `unreachable!`), add it to `.cargo/mutants.toml` with an explanation.
-4. CI enforces zero missed mutants — a surviving mutant fails the build.
+Every exclusion needs a comment. Undocumented exclusions hide real
+gaps.
diff --git a/ai/skills/rust/versioning.md b/ai/skills/rust/versioning.md
index ef6784ee..de57786b 100644
--- a/ai/skills/rust/versioning.md
+++ b/ai/skills/rust/versioning.md
@@ -1,5 +1,8 @@
# Versioning
+CLI binaries only. Server versions live on Docker image tags — see
+`dockerfile.md` and `server.md` (Deploy).
+
A `build.rs` sets a single `<NAME>_VERSION` env var. CI sets it to CalVer (`YYYY-MM-DD+SHORT_SHA`); local builds use the same shape but substitute the jj change ID for the git SHA and append `-dev` so they're distinguishable from release builds.
```rust