7afb4ac5

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). The gate is a pure, unit-tested helper `reorderForMutationChild` that only reorders when `settingRandomiseExecutionOrder` is on and a baseline is present (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, that the test-id set is unchanged, and the gate (reorders when on + baseline; leaves the forest untouched when randomisation is off or no baseline). Confirmed non-vacuous by breaking the sort / the gate and watching them fail.
- `nix flake check` passes.
- End-to-end verdict-unchanged confirmed by the two mutation runs above (identical killed/survived/uncovered tallies).
- `CHANGELOG.md` entry added under `sydtest`.
</content>

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 16s 2s 2s 19s 16s
Eval 19s 24s 24s 44s 0s
Build 1h06m07s 57s 32s 1h06m40s 1h05m22s
Suite 16s 1m24s 1h06m23s 1h06m40s 1h05m40s

Timeline

0s20s30s40s1h6m1h6m1h6m1h6m