Mutation: order covering tests cheapest-first in the mutation child
## What
When a mutation child runs the tests that cover a mutation, it runs them synchronously under fail-fast, so the first covering test that fails kills the mutant. This orders those covering tests **cheapest-first**, using the per-test baseline timing the coverage phase already measures (and, until now, discarded into a single timeout). Reaching a killing test sooner cuts the mutation phase's wall-clock, since killed mutants are the overwhelming majority of a run.
The idea: to minimise expected time to the first success among independent trials with cost `c_i` and (unknown, assumed uniform) kill-probability, order by ascending `c_i`. Shortest-first.
## How
- **`reorderTestForestByTiming`** (`sydtest/.../Mutation/Forest.hs`): a pure, tree-aware, stable reorder. It sorts siblings by ascending baseline (missing = first) and orders each subtree by the minimum cost of any leaf it contains, so a shared-setup group reorders as a unit and setup is never split. It mirrors `randomiseTestForest`: inside a `DoNotRandomiseExecutionOrder` scope it leaves the subtree untouched at every depth. So it is a drop-in alternative to execution-order randomisation, legal under exactly the same conditions.
- **Baselines persisted**: the coverage phase now writes the merged per-test timings as `baseline.json` next to `manifest-augmented.json`; the `run` and `diff` paths union them into the augmented dir the children read.
- **The child** (`MutationMode/Single.hs`) reads that baseline and reorders the filtered forest **after filtering** (so the coverage-order `TestId`s the filter needs are untouched), gated on `settingRandomiseExecutionOrder` (if the author fixed the order with `--no-randomise-execution-order`, we respect it).
Only sibling order changes, so verdicts are unaffected: the filter still decides which tests run.
## Results
Full mutation check run on two real projects, without vs with this commit, on the same machine. Fair mutation-phase-only comparison: instrumented suites + coverage warmed untimed (they are cachix-cached in real CI), so only the mutation phase is timed. **Verdicts identical in both** (same killed/survived/uncovered, every per-library score line matches).
| project | covering-test times (min / p90 / max) | without | with | speedup |
|---|---|---|---|---|
| nix-ci | 0.2ms / — / 0.7s | 1814s | 1704s | ~6% |
| really-safe-money | 0ms / 47ms / 35.8s | 530s | 405s | ~24% |
The win scales with the covering tests' time spread: cheapest-first pays off when a mutant's covering set mixes cheap tests with an expensive outlier (killing on a cheap test first avoids ever running the slow one). Projects with a heavy-tailed test-time distribution benefit most.
Caveat: n=1 per project, so treat the exact percentages as indicative (the 24% is well above scheduling noise; the 6% is closer to it). The ceiling is set by kill density and per-mutant process overhead, neither of which this change touches.
## Testing
- New pure unit tests in `sydtest-mutation` (`ReorderSpec`) covering ascending order, stable ties + missing-as-first, that a `Sequential` (parallelism) subtree is reordered but a `DoNotRandomiseExecutionOrder` one is not (incl. no descent into a nested randomise block), that a `beforeAll_` group reorders as a unit without splitting setup, and that the test-id set is unchanged. Confirmed non-vacuous by disabling the sort and watching them fail.
- `nix flake check` passes.
- End-to-end verdict-unchanged confirmed by the two mutation runs above.
</content>