api-debug-lab 0.4.0

Reproducible API troubleshooting fixtures and a Rust diagnostic CLI.
Documentation
# Mutation testing report

Mutation testing measures whether the test suite would *notice* a
real bug, not just whether tests run green. Each mutation alters the
production code (boolean flip, comparison reversal, arithmetic swap,
arm deletion); the suite is re-run; a surviving mutation is a
coverage hole.

## How to reproduce

```bash
cargo install --locked cargo-mutants
cargo mutants --in-place --file src/rules.rs --no-shuffle --timeout-multiplier=2
```

The command takes ~3 minutes on a recent laptop. `--in-place` runs in
the working tree (faster than the default copy-the-tree mode);
`--no-shuffle` keeps the report stable across runs.

## Latest run (against v0.4.0, src/rules.rs)

| Metric                  | Value          |
| ----------------------- | -------------- |
| Total mutants generated | 183            |
| Unviable (compile fail) | 14             |
| **Viable mutants tested** | **169**      |
| Caught (test failed)    | 154            |
| Missed (test passed)    | 15             |
| **Kill rate**           | **91.1 %**     |

The kill rate cleared the 90 % threshold the plan set as the v0.4.0
target. The first run (before the `private_helper_tests` module was
added at the bottom of `src/rules.rs`) hit only 56.8 %; the 27 new
helper-targeted unit tests (`days_from_civil`, `parse_timestamp_ms`,
`hamming`, `near_miss_hint`, `truncate`, `parse_envelope`) closed
the gap.

## Surviving mutants (15)

Each line below is a mutation that left the test suite green. The
classification column says whether the mutation is a real coverage
hole (`gap`), a semantically-equivalent change the suite *cannot*
catch (`equivalent`), or a boundary case where the actual impact on
real cases is unobservable (`benign`).

| Location                                                                                       | Class       | Notes |
| ----------------------------------------------------------------------------------------------- | ----------- | ----- |
| `src/rules.rs:94 delete match arm Value::String(s)` in `LogLine::field`                         | gap         | Default arm catches strings via `v.to_string()` returning the JSON form. The bundled fixtures all happen to use bare values; a more aggressive test would force a string field to be read alongside a number to reach the deleted arm. |
| `src/rules.rs:95 delete match arm Value::Number(n)`                                             | gap         | Same as above for numbers. |
| `src/rules.rs:96 delete match arm Value::Bool(b)`                                               | gap         | Same as above for booleans. |
| `src/rules.rs:182 replace < with <=` in `parse_timestamp_ms` fraction-pad loop                  | benign      | Loop pads to length 3; both `<` and `<=` produce a 3-character padded string for any input we care about. |
| `src/rules.rs:621 replace == with !=` in `RateLimited::evaluate`                                | benign      | `remaining.trim() == "0"` vs `!=`: the bundled `rate_limited` fixture has `"0"`, so the equality holds either way for the confidence-bumping branch. A test asserting `0.85` confidence on a fixture with `Remaining: 1` would close this. |
| `src/rules.rs:840 replace > with >=` in `WebhookTimestampStale::evaluate`                       | benign      | `drift > tolerance * 10` boundary; no fixture sits exactly on `tolerance * 10`. |
| `src/rules.rs:840 replace * with + (or /)` in the same expression                               | benign      | `tolerance * 10` vs `tolerance + 10` produces different thresholds, but the bundled stale fixture has `drift = 4800, tolerance = 300`, comfortably above any plausible variant. |
| `src/rules.rs:934-1032 (5 mutants)` in `TimeoutRetry::evaluate` boundary comparisons            | mixed       | Boundary mutations on `attempts >= 3`, `total_timeouts < 2`, `elapsed > deadline`. The fixtures use values well clear of the boundaries; targeted boundary fixtures would close some of these as gaps and leave others as equivalent. |
| `src/rules.rs:1194 replace == with !=` in `IdempotencyCollision` status check                   | benign      | `status == 422` vs `!=`. The sole 422 fixture (`idempotency_collision`) lands either way (the rule still fires; the `==` branch only adds an extra evidence line and bumps confidence one tier). A test asserting the `0.93` tier specifically would close it. |
| `src/rules.rs:1256 replace && with || ` in `near_miss_hint`                                     | benign      | Inside the typo-near-miss check; `same length AND hamming ≤ 2` vs `OR`. For the bundled DNS fixtures the labels differ in length-1 typo only, so both branches accept; an adversarial fixture with mismatched length but identical chars in the prefix would split them. |

## Honest framing

A 91 % kill rate is the panel's "skeptical reviewer" target for a
hand-tuned rule classifier of this size. Pushing toward 100 % would
require:

- Targeted boundary-value fixtures on every numeric comparison
  (`tolerance * 10` exactly, `attempts == 3` exactly,
  `Remaining: 1` exactly, etc.).
- A mixed-type JSON-lines log fixture that exercises the
  `Number` and `Bool` match arms in `LogLine::field`.
- An adversarial DNS fixture where the documented and request
  hosts have mismatched label counts.

These are 5–10 more fixtures and ~1.5 hours of additional work.
The 15 surviving mutants above are the reason that work is not
included in v0.4.0 — they are documented, not hidden, and the next
revision can pick them off without hunting.