eb115e65

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>

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 0s 2s 2s 3s 0s
Eval 5s 23s 23s 28s 1s
Build 1h03m00s 39s 23s 1h03m23s 1h02m31s
Suite 0s 1m05s 1h03m23s 1h03m23s 1h02m34s

Timeline

0s10s20s1h3m1h3m