e54e3f42

By: Tom Sydney Kerckhove <syd@cs-syd.eu>

sydtest: control-group (no-op) mutations

Add control-group mutations: deliberate no-ops that are expected to survive.
The plugin wraps an expression as `(\ctrl -> ifMutation cmid ctrl ctrl) e` (the
same expression on both branches, so a genuine non-diff), one after every 12
real mutations. Both branches share the subtree through the lambda binder, so
the no-op does not duplicate the instrumented expression. The control rides the
existing operator field under the reserved name "Control"; no new record field.

A control changes no behaviour, so killing it is impossible unless the mutation
testing is unsound. Tests are already retried, so a killed control is a real
defect (a flaky/nondeterministic suite or a harness bug), not transient noise,
and it fails the run like a survivor: assert-score fails on it, the driver's
fail-fast exit trips on it, and isMutationFailure is True for it. A passing
control never trips anything and is excluded from the killed/survived score.

MutationRunReport is split into three parts to match: a MutationTally (the
normal killed/survived/timed-out/uncovered/skipped score), a ControlTally
(passed/failed), and the per-mutation groups; the JSON nests to match. Record
types used as formal arguments use RecordWildCards.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 3s 2s 2s 5s 3s
Eval 6s 42s 42s 49s 0s
Build - - - - 17m43s
Suite 3s 45s - - 17m47s