mutineer-mongoid 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1194 -0
  3. data/LICENSE +21 -0
  4. data/README.md +659 -0
  5. data/bin/mutineer +6 -0
  6. data/lib/mutineer/baseline.rb +185 -0
  7. data/lib/mutineer/changed_lines.rb +136 -0
  8. data/lib/mutineer/child_stdout.rb +35 -0
  9. data/lib/mutineer/cli.rb +681 -0
  10. data/lib/mutineer/config.rb +372 -0
  11. data/lib/mutineer/coverage_map.rb +1366 -0
  12. data/lib/mutineer/daemon_backend.rb +351 -0
  13. data/lib/mutineer/daemon_client.rb +407 -0
  14. data/lib/mutineer/daemon_server.rb +351 -0
  15. data/lib/mutineer/external_backend.rb +288 -0
  16. data/lib/mutineer/file_swap.rb +194 -0
  17. data/lib/mutineer/isolation.rb +321 -0
  18. data/lib/mutineer/job_plan.rb +352 -0
  19. data/lib/mutineer/kill_channel.rb +284 -0
  20. data/lib/mutineer/kill_matrix.rb +88 -0
  21. data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
  22. data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
  23. data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
  24. data/lib/mutineer/minitest_integration.rb +137 -0
  25. data/lib/mutineer/mongoid_failures.rb +82 -0
  26. data/lib/mutineer/mongoid_worker_db.rb +97 -0
  27. data/lib/mutineer/mutant_id.rb +126 -0
  28. data/lib/mutineer/mutation.rb +27 -0
  29. data/lib/mutineer/mutator_registry.rb +109 -0
  30. data/lib/mutineer/mutators/arithmetic.rb +35 -0
  31. data/lib/mutineer/mutators/array_literal.rb +44 -0
  32. data/lib/mutineer/mutators/base.rb +105 -0
  33. data/lib/mutineer/mutators/boolean_connector.rb +52 -0
  34. data/lib/mutineer/mutators/boolean_literal.rb +56 -0
  35. data/lib/mutineer/mutators/chain_link.rb +132 -0
  36. data/lib/mutineer/mutators/collection_method.rb +44 -0
  37. data/lib/mutineer/mutators/comparison.rb +35 -0
  38. data/lib/mutineer/mutators/condition_forcing.rb +134 -0
  39. data/lib/mutineer/mutators/condition_negation.rb +49 -0
  40. data/lib/mutineer/mutators/literal_mutation.rb +51 -0
  41. data/lib/mutineer/mutators/negation_removal.rb +44 -0
  42. data/lib/mutineer/mutators/operand_removal.rb +58 -0
  43. data/lib/mutineer/mutators/operator_assignment.rb +113 -0
  44. data/lib/mutineer/mutators/range_literal.rb +54 -0
  45. data/lib/mutineer/mutators/regex_literal.rb +72 -0
  46. data/lib/mutineer/mutators/return_nil.rb +72 -0
  47. data/lib/mutineer/mutators/safe_navigation.rb +76 -0
  48. data/lib/mutineer/mutators/statement_removal.rb +33 -0
  49. data/lib/mutineer/mutators/string_literal.rb +34 -0
  50. data/lib/mutineer/orphan_guard.rb +28 -0
  51. data/lib/mutineer/pairing.rb +298 -0
  52. data/lib/mutineer/parser.rb +45 -0
  53. data/lib/mutineer/progress.rb +46 -0
  54. data/lib/mutineer/project.rb +506 -0
  55. data/lib/mutineer/project_path.rb +57 -0
  56. data/lib/mutineer/rails_worker_db.rb +212 -0
  57. data/lib/mutineer/reporter.rb +926 -0
  58. data/lib/mutineer/result.rb +240 -0
  59. data/lib/mutineer/runner.rb +418 -0
  60. data/lib/mutineer/statement_lines.rb +183 -0
  61. data/lib/mutineer/subject.rb +38 -0
  62. data/lib/mutineer/test_runners/minitest.rb +22 -0
  63. data/lib/mutineer/test_runners/rspec.rb +330 -0
  64. data/lib/mutineer/test_runners.rb +30 -0
  65. data/lib/mutineer/version.rb +6 -0
  66. data/lib/mutineer/worker_pool.rb +147 -0
  67. data/lib/mutineer.rb +48 -0
  68. metadata +143 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,1194 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [2.0.0] - 2026-10-15
8
+
9
+ - Rename the gem, repository, and action to `mutineer-mongoid`.
10
+ - Document inherited Minitest/SQLite support alongside Mongoid/RSpec support.
11
+ - Preserve the `mutineer` command and the v1.7.0 release for existing users.
12
+
13
+ ## [1.7.0] - 2026-10-15
14
+
15
+ - Add Mongoid 9 / RSpec daemon execution with a disposable database per child.
16
+ - Preserve boot-time documents and indexes, and clean databases after timeouts.
17
+ - Package the fork as `mutineer-gaia`, retaining the existing executable.
18
+ - Ship a composite GitHub action with a daemon input and a bundled-source installer.
19
+ - Attach the gem and checksum to GitHub releases.
20
+
21
+ ## [Unreleased]
22
+
23
+ ## [1.6.0] - 2026-10-08
24
+
25
+ ### Added
26
+
27
+ - **An `unplaceable` mutant status.** Under `--strategy redefine`, a mutant in
28
+ a method whose class or module has no constant name is not run. It is left
29
+ out of the score and, unlike `uncapturable`, out of `no_verdict`, so it does
30
+ not fail `--threshold`. The human report gives it an `Unplaceable:` row, the
31
+ HTML report a count, and the JSON report `summary.unplaceable` and an
32
+ `unplaceable[]` list; `schema_version` is `1.7`.
33
+ - **The agent skill installs with `gh skill` and `npx skills` (#125).** It
34
+ moved from `docs/skill.md` to `skills/mutineer/SKILL.md`, the layout of the
35
+ Agent Skills specification, so `gh skill install davidteren/mutineer
36
+ mutineer` finds it and `npx skills add davidteren/mutineer --skill mutineer`
37
+ installs one `SKILL.md` instead of the whole `docs/` site. The site build
38
+ copies that file to `skill.md`, so its URL still works. The skill's
39
+ description now says when to use it, and the README shows how to install it.
40
+ - **A `ran_at_load` mutant status** (#187). A mutant on a method-body line
41
+ that ran while the app booted or its class loaded gets this status when it
42
+ survives, or when only the load ran the line. A kill stays a kill. It is
43
+ left out of the score and out of `no_verdict`, so it does not fail
44
+ `--threshold`. The human report gives it a `Ran at load:` row that points
45
+ to `--test-command` to verify these mutants, the HTML report a count, and
46
+ the JSON report `summary.ran_at_load` and a `ran_at_load[]` list, within
47
+ schema `1.7`.
48
+
49
+ ### Changed
50
+
51
+ - **Mutants no longer run under Coverage with `--boot` or `--rails` (#228).** Coverage
52
+ starts before the app boots so the coverage map can be built, and it used to
53
+ keep running in every mutant fork after that, though nothing read it. It is
54
+ now suspended once the map is built, from a fresh capture or from the cache.
55
+ Coverage that a host process started before the run is left running.
56
+ On a small app whose test calls tiny methods in a loop, the CPU time of a
57
+ whole in-process `--boot` run fell from 0.76s to 0.57s (median of five). On
58
+ the Rails fixture app the run is mostly boot, and the change is within noise
59
+ (1.61s before, 1.60s after). `--daemon` runs its mutants in worker daemons
60
+ that never start Coverage, so its time does not change (3.48s before, 3.44s
61
+ after); the map-building daemon now suspends Coverage after the map too.
62
+ - **A mutant runs its paired test files first, then the cheapest (#203).** The
63
+ covering test files used to run in the order the coverage map stored them,
64
+ so a mutant that one fast file kills could still score `timeout` when slow
65
+ files ran first. Now the files that pairing finds for the source run first,
66
+ then the rest from fastest to slowest by the time each file took during
67
+ coverage capture, less the fastest file's time (the startup cost each
68
+ capture pays). The order is fixed for a given cache. Costs compare in
69
+ doubling buckets, so files of close cost keep their path order when a
70
+ rebuilt cache measures them again; a file near a bucket edge can still move,
71
+ so keep `.mutineer/` between CI runs for the most stable `--baseline`. The
72
+ order applies to Minitest (after the seeded class shuffle, with
73
+ `parallelize_me!` classes still last) and to RSpec. `--daemon` runs every
74
+ covering test with no stop, so the order does not change its verdicts. The
75
+ coverage cache now saves these timings, so a cache from an earlier version
76
+ rebuilds once. A complete run gives the same verdicts and `--matrix` rows,
77
+ but the new order can change which mutants time out, so a `--baseline` gate
78
+ can see a one-time change on upgrade.
79
+ - **Job selection moved from `Mutineer::Runner` to a new `Mutineer::JobPlan`
80
+ module (#75).** The in-process runner, the `--daemon` backend and
81
+ `--dry-run` share it, so each file now requires what it uses and no
82
+ `require_relative` cycle exists in `lib/mutineer`. The moved methods are
83
+ `collect_jobs`, `filter_since`, `coverage_selection`, `ran_at_load?`,
84
+ `load_verdict`, `abort_if_unclean!`, `test_load_roots`, `source_dirs`,
85
+ `sweep_orphans`, `suppress_map`, `suppressed?` and `result_keys`. These were
86
+ internal methods, so `Runner` keeps no copies. Runs, reports and caches do
87
+ not change.
88
+
89
+ ### Fixed
90
+
91
+ - **An endless method that only the tests call is no longer `no_coverage`
92
+ under `--rails`, `--boot` or `--daemon` (#209).** Ruby counts no line when
93
+ an endless method such as `def half(x) = x / 2` runs, only its `def` line
94
+ when it is defined. The boot defines it before any test runs, so the forked
95
+ coverage capture never saw that line run. Coverage now also
96
+ counts methods (`Coverage.start(lines: true, methods: true)`), and a test
97
+ that calls such a method covers its `def` line. Coverage caches from earlier
98
+ versions rebuild once.
99
+ - **`module_function :name` in a reopened module inside `class << self`
100
+ promotes the method from the earlier opening (#216).** Ruby treats both
101
+ openings as one module, but Mutineer matched them only within one body, so
102
+ it named the method `#<Class:App>::M#c` instead of `#<Class:App>::M.c`.
103
+ The two openings now match by the module's full name. A module whose name
104
+ Mutineer cannot be sure of still matches only within its own body: a path
105
+ such as `self::X` or `Foo::X` written directly inside `class << self`, a
106
+ name under an anonymous class, or a module in a `class << self` inside a
107
+ method or a `class_eval` block. A `module self::X` in a block, or a
108
+ `self::X = ...` in a `class_eval` block, matches only openings in the same
109
+ block. A `self::X` like these now also stays apart from a
110
+ module of the same name outside `class << self`, where a `module_function`
111
+ in that module used to promote its methods.
112
+ - **A `module self::X` or `class self::X` in a block is `unplaceable` under
113
+ `--strategy redefine` (#229).** In a block, `self` is not always the
114
+ enclosing class: in `Other.class_eval do` it is `Other`, and in
115
+ `Foo = Class.new do` it is the new class. Mutineer gave these modules a
116
+ known owner on the enclosing namespace, so redefine loaded the mutant onto
117
+ a module the tests never use, and the mutant could falsely survive or
118
+ score `error`. Such a module, everything nested in it, and a
119
+ `self::X = ...` in a block that does not build a class now have
120
+ their owner unknown: redefine reports their mutants as `unplaceable`, and
121
+ `--strategy reload` still runs them. In a `Data.define`, `Struct.new`,
122
+ `Class.new` or `Module.new` block, the module is now named under the
123
+ builder's constant (`Outer::Foo::X#c`, was `Outer::X#c`), so the subject
124
+ names and mutant ids of those methods change. `self::X` directly in a class
125
+ or module body is unchanged.
126
+
127
+ - **`--daemon` worker databases start as a copy of the test database
128
+ (#222).** Each worker slot's SQLite database used to start from
129
+ `db/schema.rb` alone. Rows the daemon wrote while it booted, in
130
+ initializers or `--require` files, were only in the base test database, so
131
+ a test that read them passed in-process but failed under `--daemon`. On its
132
+ first use, a worker database is now a copy of the base test database (schema
133
+ and rows, made with the SQLite online backup API). The worker still loads
134
+ `db/schema.rb` when the copy's schema differs from it (another schema
135
+ version, or another or no `schema_sha1` checksum in `ar_internal_metadata`),
136
+ for example when the test database is empty, out of date, or set up with a
137
+ plain `load "db/schema.rb"`. That load drops the
138
+ copied rows of the tables it defines.
139
+ - **Standalone coverage capture loads the `--require` files (#217).** A run
140
+ without `--boot` requires the sources and then each `--require` (or
141
+ `require:`) file before it forks the mutants, but the coverage capture
142
+ loaded only the sources. A source method that a `--require` file called
143
+ while it loaded was then not seen as run at load, so its mutant was a false
144
+ `survived` instead of `ran_at_load`. The capture and the clean-suite check
145
+ now load the `--require` files after the sources. With or without
146
+ `--boot`, the coverage cache rebuilds when one of them changes, is added,
147
+ is removed, or moves in the load order.
148
+ - **`--daemon` loads the `--require` files (#220).** The daemon booted the app
149
+ but never loaded the `--require` (or `require:`) files, so a test that
150
+ needs one failed under `--daemon` and passed in-process. A source method
151
+ that such a file called while it loaded was also not seen as run at load,
152
+ so its mutant could be a false `survived` instead of `ran_at_load`. The
153
+ daemon now loads the files after the app boots, as an in-process run does,
154
+ and the daemon's coverage cache rebuilds when one of them changes.
155
+ - **The release workflow publishes only stable tags** (#161). Its trigger,
156
+ `v*.*.*`, also matches a prerelease tag such as `v1.3.1-rc1`, and the only
157
+ guard checked that the tag matched `Mutineer::VERSION`. A prerelease tag
158
+ could then publish the gem and move the floating major tag (`v1`) that
159
+ Action users pin. The workflow now stops with a clear error before any
160
+ publish step unless the tag is a stable `vMAJOR.MINOR.PATCH`.
161
+ - **Coverage capture keeps to `capture_timeout` from start to finish.** Under
162
+ `--boot` and `--daemon`, a hung test file ignored the timeout and blocked
163
+ the run; it is now skipped as "timed out", like a standalone capture. A
164
+ process that a test started and left running could keep the result pipe
165
+ open and make a boot-mode capture wait for it to exit. Each capture now runs
166
+ in its own process group, and a timeout stops the whole group, not only the
167
+ test process. A capture whose test finished just before the deadline could
168
+ also lose its valid result and report "invalid coverage output"; it now
169
+ gets a short grace period to read the result. (#101, #129)
170
+ - **`--daemon` no longer waits forever on a daemon that stops answering**
171
+ (#101). A mutant whose verdict has not arrived 30 seconds after its own
172
+ timeout is scored `error`, and the daemon is killed and respawned, as after
173
+ a crash. A boot that has not finished after 600 seconds ends the run with a
174
+ message that says so (it is not retried by a second daemon), and shutdown no longer waits on a stuck daemon.
175
+
176
+ - **A method in a `Data.define`, `Struct.new`, `Class.new` or `Module.new`
177
+ block belongs to the class or module the block builds.** In
178
+ `class App; Argo = Data.define(:url) do def self.load = ...; end; end` the
179
+ subject is now `App::Argo.load`, not `App.load`; at the top level it was
180
+ `.load`, with no owner. Under `--strategy redefine` (the `--rails` default)
181
+ the mutated method was loaded into the wrong class, so the real method never
182
+ changed and every such mutant falsely survived. The class or module is
183
+ named after the constant it is assigned to with `=`, `||=` or `&&=`, through
184
+ parentheses or `begin`, resolved as Ruby resolves the assignment: `X` in the
185
+ current namespace, `::X` and any path at the top level from `Object`,
186
+ `self::X` under the current class. When it cannot be named statically (no
187
+ constant, a relative path such as `User::Permission` inside a namespace, or
188
+ a constant assigned in `class << self`), redefine reports its mutants with
189
+ a new status, `unplaceable`, rather than load them onto the wrong class;
190
+ reload still runs them. `module_function :name` in a `Module.new` block
191
+ promotes that module's method. Mutant ids for these subjects change:
192
+ regenerate `ignore:` entries and baselines that name them.
193
+ - **The automated release PR gets its required CI checks without a token
194
+ secret (#85).** `release-pr.yml` pushes the release branch with the default
195
+ `GITHUB_TOKEN`, which starts no other workflow, so the four required checks
196
+ never ran and the PR stayed blocked until a person closed and reopened it.
197
+ The job now dispatches `ci.yml` on the release branch (CI gained a
198
+ `workflow_dispatch` trigger), and those runs report the required checks on
199
+ the PR's head commit. With a `RELEASE_PR_TOKEN` secret set, the push
200
+ triggers CI as before and no dispatch is made.
201
+ - **Code that runs while the app boots or a class loads no longer gives a false
202
+ `survived` or `no_coverage`** (#187). A class body such as
203
+ `ALL = [price(3)].freeze` runs `price` before any mutant is applied. Under
204
+ `--strategy redefine` (the `--rails` default) the load never runs again, so
205
+ a test that checks `ALL` saw the original value and the mutant falsely
206
+ survived. Under `--rails` or `--boot` no test was credited with lines that
207
+ ran during boot, so those mutants were falsely `no_coverage`. Mutineer now
208
+ reads the lines that ran at load (from the boot, or from requiring the
209
+ sources in standalone capture) and reports these mutants as `ran_at_load`,
210
+ under both strategies and with `--daemon`: reload runs the file's class body
211
+ again but not an initializer or another file's code, so a survivor there is
212
+ not trusted either. Under `--boot` with redefine, a
213
+ lazily loaded class (Zeitwerk, `autoload`) is loaded before the boot lines
214
+ are read, so its class body counts as load too. A baseline survivor that is
215
+ now `ran_at_load` is listed as fixed, not as a regression. Standalone
216
+ coverage caches from earlier versions rebuild once. A one-line or endless
217
+ `def` keeps its body on the `def` line, which Ruby counts when the method is
218
+ defined, so Mutineer reads the method's call count at load for it instead
219
+ (#209).
220
+ - **A boot file that prints to stdout no longer breaks the daemon handshake
221
+ (#102).** Output from `puts`, `STDOUT`, `$stdout`, a direct fd 1 write, or a
222
+ subprocess reached the JSON channel before the ready message, so the daemon
223
+ failed with "daemon exited before the handshake". The daemon now keeps a
224
+ private copy of its original stdout for the protocol and points the app's
225
+ stdout at stderr, where that output stays visible.
226
+ - **A class or module opened inside `class << self` belongs to the singleton
227
+ class (#208).** In `class App; class << self; class Q; def q1 ...` the
228
+ subject was `App::Q.q1`, a class method of a constant Ruby never defines:
229
+ `Q` is `App.singleton_class::Q`, and `q1` is an instance method. It is now
230
+ `#<Class:App>::Q#q1` with no known owner, as is a class or module opened in
231
+ a builder block there, and anything nested in either. Inside a builder
232
+ block, `class << self` opens the built class's singleton class, so a class
233
+ or constant there is named under it, for example `#<Class:App::P>::W`. Under
234
+ `--strategy redefine` loading the mutant raised `NameError` and scored
235
+ `error`; redefine now reports these mutants as `unplaceable`, and reload
236
+ still runs them. A compact `class Foo::X` and a top-level `class ::X` or
237
+ `::X = Class.new do` there are named as written (`::X` as `X`, now with
238
+ instance methods) and are owner-unknown too: redefine reopens them without
239
+ the singleton class, so constants their bodies look up through it would not
240
+ resolve. `module_function :name` in a body whose owner is unknown now
241
+ promotes that body's own methods, wherever it is: a `Foo::X = Module.new`
242
+ block's `c` is `Foo::X.c`, not `Foo::X#c`, and a def in an anonymous
243
+ `Class.new` block is no longer promoted by a `module_function` of the module
244
+ around it. Mutant ids for these subjects change: regenerate `ignore:`
245
+ entries and baselines that name them.
246
+
247
+ ## [1.5.0] - 2026-10-06
248
+
249
+ ### Added
250
+ - **`--timeout SECONDS` and `--capture-timeout SECONDS`**, also `timeout:` and
251
+ `capture_timeout:` in `.mutineer.yml`. They set the in-process per-mutant
252
+ time limit (default 10s) and the coverage-capture time limit (default 120s),
253
+ which were fixed. On a large Rails suite the clean run of the unmutated
254
+ tests took longer than 120s, so every run that offered the whole suite
255
+ stopped as not green, and a mutant on a line many tests cover took longer
256
+ than 10s and was scored `timeout`. `--daemon` keeps its own per-mutant
257
+ limit but uses `--capture-timeout` for its coverage capture;
258
+ `--test-command` uses neither. Either backend warns about a limit it was
259
+ given and does not use.
260
+ - **`--cache-dir DIR`**, also `cache_dir:` in `.mutineer.yml`, sets where the
261
+ coverage cache is written (default `.mutineer`, as before). Two runs from the
262
+ same project root shared `.mutineer/coverage.json` and could overwrite each
263
+ other's map; each can now have its own directory. `--daemon` uses it too.
264
+ A blank value (an unset CI variable) exits 2 rather than writing the cache
265
+ into the project root.
266
+ - **The README states how timeouts affect the score** (#62): a timed-out
267
+ mutant is left out of the score, and counts as a mutant with no verdict
268
+ under `--threshold`.
269
+ - **`--matrix` reports which tests kill each mutant** (`matrix: true` in
270
+ `.mutineer.yml`). Each mutant runs every test in its covering files instead
271
+ of stopping at the first failure, and the child sends each test's outcome to
272
+ the parent as it is recorded. The report names blind tests, which ran in a
273
+ complete row and killed no mutant, and redundant tests, whose every kill
274
+ another test also makes. Redundancy is judged one test at a time, so delete
275
+ redundant tests one at a time. The human report lists up to 20 of each, the
276
+ HTML report all of them, and the JSON report moves to schema `1.5` with a
277
+ `matrix` block that appears only with the flag. A test is its file and its
278
+ id (the example id under RSpec), so examples that share a description stay
279
+ apart and an example whose generated description changes with the mutant
280
+ stays one test. Verdicts, the score and the exit code do not change: each
281
+ mutant gets the verdict a run without the flag gives. After a serial test
282
+ fails, an exit, a crash or the timeout in a later test, class or group
283
+ leaves it `killed`; an end in the code that run still reaches (the failing
284
+ test's class wrapper or group hooks, the suite's own cleanup) and a failure
285
+ in a `parallelize_me!` class keep the exit status. A row is complete only
286
+ when the child's stream arrived in order and every test reported (under
287
+ Minitest the recorder saw every test, under RSpec every planned example),
288
+ so a caught `Interrupt` leaves it incomplete. The human report names up
289
+ to 20 incomplete rows and the HTML and JSON reports all of them; the human
290
+ and HTML reports warn not to delete a blind test until its rows are
291
+ complete.
292
+ Minitest and RSpec 3.3+ are supported, on the in-process backend only;
293
+ `--daemon`, `--test-command`, `--fail-fast` and `--dry-run` exit 2 with it.
294
+ `--no-matrix` and `--no-fail-fast` beat the matching `.mutineer.yml` key,
295
+ so a file that sets one can still run with a flag that conflicts with it.
296
+ - **`no_coverage[]` and `uncapturable[]` name each mutant.** Every entry in
297
+ the JSON report now has `operator`, `token` and `id`, as `ignored[]` has, so
298
+ mutants on one line can be told apart and an `id` can go in `ignore:`. The
299
+ entries are sorted by `(file, line, operator, id)`. `schema_version` is `1.6`.
300
+
301
+ - **The site and README explain "clean-room" and compare Mutineer with
302
+ Mutant**, and the landing page hero has a one-minute explainer video. It
303
+ plays at 55% volume while in view (not with reduced motion; muted until
304
+ the first click or key where the browser requires it), pauses when it scrolls
305
+ away or the tab is hidden, and has an Enlarge button.
306
+
307
+ ### Changed
308
+ - **The human report has a `Timeout:` row**, and the score line lists timeouts
309
+ apart from errored mutants. Before, the `Errored:` row and the score line
310
+ added the two together, while the JSON report kept them apart. The HTML
311
+ summary now shows a `timeout` count apart from `errored` too.
312
+ - **A test file given as a source after `--test` exits 2.** `--test` takes one
313
+ file, so in `mutineer run app/x.rb --test spec/a_spec.rb spec/b_spec.rb` the
314
+ second spec became a source to mutate, and the run tested with one spec file
315
+ without a word. Now a source named `*_test.rb` or `*_spec.rb` inside a
316
+ `test/` or `spec/` directory, given with `--test`, is a usage error that says
317
+ to repeat `--test`. The usage line now reads `--test <test> [--test <test>...]`.
318
+
319
+ ### Fixed
320
+ - **The RSpec runner keeps what gems configured before the run.** It called
321
+ `RSpec.reset`, which drops the whole configuration. Under `rails: true`, a
322
+ `--boot` file or `require:`, the app's gems load first, so a setting such as
323
+ rspec-retry's `verbose_retry` was gone, and a support file that set it
324
+ raised `NoMethodError`. The run stopped with "the unmutated suite is not
325
+ green". The runner now calls `RSpec.clear_examples`, which keeps the
326
+ configuration. Hooks that gems added now apply too: an example with
327
+ rspec-retry's `retry: 3` runs up to 3 times per mutant, as under `rspec`.
328
+ - **`--since` no longer scores a file that Git ignores** (#168). A file
329
+ outside the index is scored in full, so a new file is not missed. That
330
+ check also caught files that Git ignores on purpose (`.gitignore`,
331
+ `.git/info/exclude` or the global excludes file), such as generated code
332
+ inside a directory run. Those files now get no mutants under `--since`,
333
+ and a warning names each one. Other untracked files are still scored in
334
+ full, and so is a file when Git cannot tell, such as one beyond a symlink or
335
+ inside a submodule.
336
+ - **The human report escapes control characters from the source** (#163).
337
+ A surviving line that held a terminal control byte (for example ESC) was
338
+ printed as that byte, so it could change what the terminal showed. The
339
+ token, the replacement, the diff lines, method names, file paths and the
340
+ kill matrix's test names now print each control character except tab as
341
+ its Ruby escape (`\e`), and a byte that is not valid UTF-8 (a Latin-1
342
+ source) as `\xNN`. The JSON and HTML
343
+ reports already escaped text and do not change.
344
+ - **One edit on a line is scored once** (#159). Some opt-in operators emitted
345
+ two mutants that give the same source: `literal_mutation` on `0` (the
346
+ "change to 1" and "add 1" rules), `negation_removal` on `!!x` (either `!`
347
+ removed), and `chain_link` or `operand_removal` on a repeated part such as
348
+ `a.b.b.c` or `x || x`. Each copy had its own id, so one surviving edit
349
+ counted twice. Now only the first copy on a line runs. Copies that start
350
+ on different lines (a multi-line chain) both stay, so `--since` never
351
+ loses the edit. Ids are assigned before the copy is dropped, so every kept
352
+ mutant keeps its id, and the dropped copy's id simply stops appearing.
353
+ Default runs do not change. If you use these operators with `--baseline`,
354
+ regenerate the baseline after upgrading: the score can move with no code
355
+ change, and in a full (unscoped) comparison a dropped copy that survived
356
+ is listed as fixed. An ignore entry for one copy's id still ignores only
357
+ that copy; the other copy still runs.
358
+ - **`# mutineer:disable-line` inside a string no longer silences a line**
359
+ (#158). The marker was found with a text search, so a string such as
360
+ `"# mutineer:disable-line"` ignored every mutant on its line and left
361
+ them out of the score. Only a real `#` comment counts now. A heredoc or
362
+ regex that holds the text is ignored too.
363
+ - **`./lib/x.rb` and an absolute path pair with a test** (#104). Auto-pairing
364
+ found the test for `lib/calc.rb` but not for `./lib/calc.rb` or the
365
+ absolute path to the same file. It reported "no test found by convention"
366
+ and exited 2. A file argument inside the project is now made relative to
367
+ the project root first, so equivalent spellings (`./`, `..`, absolute)
368
+ pair with the same test and run once. A symlink keeps its own name, so it
369
+ stays a separate source from its target. Reports show that root-relative
370
+ path. A path outside the project stays as typed.
371
+ - **A mutant on another line of a multi-line statement runs its tests**
372
+ instead of being reported as `no_coverage`. Ruby counts one line of a
373
+ statement only. When the mutant's own line has no count, the runner now uses
374
+ the tests that ran the statement that holds the mutation. A heredoc body
375
+ belongs to its statement. This fallback skips a first or last line that also
376
+ holds other code, such as the `def` line, and the body of `x while c` and
377
+ `x until c`. It also gives no tests to a mutant in code of the statement
378
+ that runs only sometimes: a `when` or `in` condition, a `rescue` class
379
+ list, a parameter default, the rescue side of `x rescue y`, the value of
380
+ `||=` or `&&=`, the operand of `defined?`, the right side of `a ||` or
381
+ `a &&`, a branch of `x if c`, `x unless c`, a ternary or `else`, the
382
+ pattern of `v in p` or `v => p`, an
383
+ argument or block of `x&.m(...)`, and the value of `x&.m += v`. Those
384
+ mutants stay `no_coverage`, as before.
385
+ - **A survivor diff for a multi-line mutant applies with `git apply`** (#106).
386
+ The JSON `diff` header always read `@@ -N +N @@`, even when the mutant
387
+ removed two lines and added one, so `git apply` rejected the patch as
388
+ corrupt. The header now carries each side's line count (`@@ -3,2 +3 @@`).
389
+ A one-line diff keeps its old header. A mutant that empties a line now
390
+ shows that empty line as a `+` line. A side whose last line has no final
391
+ newline gets ``. Lines of a CRLF file now keep
392
+ their `\r`, so the patch applies to that file.
393
+ - **Split test files pair with their source** (#87). A source such as
394
+ `app/foo/bar.rb` now also uses `test/foo/bar_upsert_test.rb` and
395
+ `test/foo/bar_guards_test.rb`, together with `test/foo/bar_test.rb` when
396
+ that file exists. The same names under `test/lib/` pair with a `lib/`
397
+ source. Before, only the exact `bar_test.rb` name was tried, so a suite
398
+ split across `bar_*_test.rb` files was skipped and a gate could pass with
399
+ no mutants run. A spec the old rules already find is unchanged. The
400
+ split name is left for `user_session.rb` when that file exists, so
401
+ `user.rb` does not run `user_session_test.rb`. The same rule holds outside
402
+ `app/` and `lib/`: `src/user.rb` does not take `test/src/user_session_test.rb`
403
+ when `src/user_session.rb` exists. A longer source claims a split file
404
+ only when that source itself searches the directory the file is in.
405
+ A failed capture of `test/foo/bar_upsert_test.rb` can mark `app/foo/bar.rb`
406
+ uncapturable only when no successful capture covered that source and
407
+ `bar_upsert.rb` does not exist. A test outside that directory does not,
408
+ and neither does a source that only shares the test basename.
409
+ `user_session__test.rb` stays with `user.rb` when `user_session.rb`
410
+ exists, because that name is not the exact test or a split test for
411
+ `user_session`. A failed exact spec in the mirrored `spec/` or `spec/lib/` directory
412
+ marks an `app/` or `lib/` source the same way. A failed spec in another
413
+ directory does not. A project rooted at `/` still finds split tests
414
+ inside that root. A stored failure keeps the pairing from the moment it
415
+ was checked. Adding or deleting a longer source rebuilds the coverage
416
+ map, so that failure is not moved onto the shorter source.
417
+ - **A blank `operators` key no longer runs zero mutants and exits 0** (#167).
418
+ `operators:` with no value became an empty list. The run made no mutants
419
+ and the gate passed. A blank operator list is now an error, and so is a
420
+ list of only unknown operator names. An empty `require` or `ignore` list
421
+ is still valid. `--operators` on the command line still replaces the
422
+ file list, including a blank or unknown file list.
423
+ - **The daemon no longer runs the tool's Ruby settings inside the app** (#100).
424
+ Open3 keeps a variable when the spawn hash omits it, so a parent `RUBYOPT`
425
+ ran inside the app. The child now gets the environment Bundler saved before
426
+ it activated, passed with `unsetenv_others`. The app keeps its gem home,
427
+ excluded groups, Bundler config, and chruby path. An `RBENV_VERSION` or
428
+ `ASDF_RUBY_VERSION` already in the environment stays. That pin is often
429
+ the Ruby that started the tool. `.ruby-version` applies only when no pin
430
+ is set. The tool's `RUBYOPT` and `RUBYLIB` do not reach the child, including
431
+ a value Bundler had saved. A gem home, `BUNDLE_PATH`, or `BUNDLE_WITHOUT`
432
+ reaches the child only when Bundler saved the app's value. The same
433
+ rule covers `GEM_PATH` and `BUNDLER_VERSION`. An rbenv version
434
+ bin under `RBENV_ROOT`, or under `~/.rbenv` when that variable is unset,
435
+ leaves `PATH` only when that root has a shim directory. An asdf version
436
+ bin leaves only when an asdf shim directory exists, including
437
+ `ASDF_DATA_DIR/shims`. Without that shim, the bin stays and `bundle` can
438
+ still be found. The app Gemfile, an explicit Ruby pin, and Rails env are
439
+ applied after that.
440
+ - **The Action refuses to run when `output` and `baseline` are the same file** (#160).
441
+ That setup used to work, and a failing run copied its report onto the
442
+ baseline. The next run then treated the failed report as the baseline, so
443
+ the gate could pass. The Action now stops before the run and exits 2.
444
+ The baseline may come from the `baseline` input, from `--baseline` in
445
+ `extra-args`, or from `.mutineer.yml`. A numeric path such as `123` is
446
+ read the same way as the CLI. Write the report to a separate path. Copy
447
+ that file onto the baseline in a later step, and only when this step
448
+ exits 0. You can also refresh the baseline with the CLI, and keep the
449
+ file only when the run exits 0. The next release moves the `v1` tag, so
450
+ workflows on `@v1` get this refusal then.
451
+ - **A mutation no longer leaves a heredoc body behind as Ruby** (#134).
452
+ Replacing a node deletes the heredoc opener and not the body. The body
453
+ then parses as code, raises, and counts as a kill. A mutation that drops
454
+ the opener is skipped. A replacement that still contains the opener stays.
455
+ - **A bare `public`, `private` or `protected` ends `module_function` mode**,
456
+ so later `def`s in the module body are named as instance methods, not
457
+ singleton methods (#144).
458
+ - **Turning on reduced motion mid-visit finishes the landing page count-ups**,
459
+ so no number keeps changing after the visitor asks for less motion (#176).
460
+ - **`rake site:build` can no longer delete a directory such as `lib/` or
461
+ `.git`** (#162). The task removed the destination it was given, and only
462
+ refused the checkout, its parents and `docs/`. It now always builds into
463
+ `_site/` at the checkout root, and `rake "site:build[DEST]"` and
464
+ `SITE_BUILD_DEST` are gone.
465
+ - **Documentation matches the 1.4 behavior.** Config examples use supported keys,
466
+ agent loops check the exit code and a real score, and report totals explain
467
+ partial `--fail-fast` runs. The README lists every config key and its validation
468
+ rules. API README links work outside GitHub, repeated changelog headings have
469
+ unique anchors, and the website checks validate built links and anchors.
470
+ Mutant-id wording and the gem's Minitest/RSpec description are also corrected.
471
+ - **A mutant in a nested method counts once** (#157). A `def` inside
472
+ another method is a subject of its own, but most operators also mutated
473
+ its code on the outer method. The same edit then counted twice in the
474
+ score, with two ids. Every operator now skips a nested `def` when it
475
+ mutates the outer method. A `def` inside `class << obj`, where `obj` is
476
+ not `self`, is not a subject, so the outer method still mutates its code.
477
+ `array_literal`, `chain_link`, `condition_true`, `condition_false`,
478
+ `operand_removal`, `operator_assignment` and `return_nil` skipped that
479
+ code before. Their new mutants there can show as new survivors against a
480
+ baseline.
481
+
482
+ A mutant id includes an ordinal among the mutants with the same operator
483
+ and token in a subject. When the duplicates leave the outer method, the
484
+ ordinals of its own mutants change. An old duplicate id can then name a
485
+ different mutant on the outer method, and an `ignore:` entry or a
486
+ baseline entry with that id applies to that mutant with no warning. If
487
+ your code has nested methods, regenerate the `ignore:` entries and the
488
+ baseline for the outer methods.
489
+
490
+ Under `--strategy redefine` (the `--rails` default), a mutant in a nested
491
+ method can survive or error even when a test would catch it. `redefine`
492
+ loads only the inner method, and a call to the outer method defines the
493
+ original inner method again. Before, the copy of the edit on the outer
494
+ method was killed. Now each edit has one verdict, from the inner method.
495
+
496
+ ## [1.4.0] - 2026-09-30
497
+
498
+ ### Added
499
+ - **Condition-forcing operators** (Tier-2, opt-in via `--operators`):
500
+ `condition_true` and `condition_false` replace an `if`/`elsif`/`unless`,
501
+ ternary, modifier or `case`/`in` guard condition with `(true)` or `(false)`, so
502
+ its branch always runs or never runs. A surviving mutant means no selected
503
+ test detected the forced condition. A literal condition, also in
504
+ parentheses, makes no mutant: forcing it changes nothing or repeats the
505
+ `boolean_literal` flip. A condition that holds a heredoc makes no mutant.
506
+ A condition that assigns a variable keeps its code, and only its value
507
+ is forced (`(m = x; true)`), so later code still sees the variable.
508
+ The never-runs side of an else-less conditional can
509
+ be the same program as the `nil` that `statement_removal` or `return_nil`
510
+ puts in its place; it is still made, so these mutants and their ids do not
511
+ depend on which other operators run.
512
+ - **Operator-assignment operator** (Tier-2, opt-in via `--operators`):
513
+ `operator_assignment` swaps the operator of a compound assignment:
514
+ `+=` <-> `-=`, `*=` <-> `/=`, and `%=` and `**=` -> `*=`. It covers local,
515
+ instance, class and global variables, constants, calls (`a.b += 1`) and
516
+ index calls (`a[i] += 1`). The `arithmetic` operator never sees these
517
+ forms, because Prism does not parse them as calls. The operator does not
518
+ change `||=`, `&&=`, or the bitwise and shift forms (`|=`, `<<=`).
519
+
520
+ ### Fixed
521
+ - **A typed `--rails`, `--verbose` or `--debug` beats `rails: false` and
522
+ `verbose: false` in `.mutineer.yml`** (#103). The flag was dropped, so the
523
+ run went on without Rails boot or verbose output. The config layers now keep
524
+ only the keys the user wrote, and "did the user write this" comes from those
525
+ keys, not from a hand-kept list.
526
+ - **`framework: rspec` in `.mutineer.yml` survives test pairing** (#103).
527
+ Pairing re-detected the framework from the test file names, so an RSpec
528
+ suite in `test/calc_test.rb` ran under Minitest.
529
+ - **A bad number is an error, not a rounded value** (#105). `--jobs 1.9` and
530
+ `--baseline-epsilon abc` no longer become `1` and `0.0`. One option schema
531
+ parses each value once, where it enters, for both the command line and
532
+ `.mutineer.yml`.
533
+ - **`--since` keeps a source file that git does not track yet** (#156). Before,
534
+ such a file counted as unchanged. The run scored no mutants, and a positive
535
+ `--threshold` still exited 0. The file is now new in full, so all its
536
+ mutants run.
537
+
538
+ ### Changed
539
+ - **These now exit 2 with a message that names the option**: `jobs: 1.9` or
540
+ `jobs: true` in `.mutineer.yml` (before: `1`, or a crash); a bad, negative
541
+ or non-finite `--baseline-epsilon` (before: `0.0`); a boolean key in
542
+ `.mutineer.yml` that is not `true` or `false`, such as the string `"yes"`
543
+ (before: `false`).
544
+ - **`--jobs` takes plain digits only.** `0x2`, `+2`, `" 2"` and `1_0` exit 2.
545
+ Before, `Integer()` read them as 2, 2, 2 and 10. `--threshold` and
546
+ `--baseline-epsilon` take a plain decimal, such as `2` or `2.5`, from a
547
+ string. `0x10`, `+2`, `1_0` and `1e2` exit 2. Before, `Float()` read them
548
+ as 16.0, 2.0, 10.0 and 100.0.
549
+ - **A string option that gets `true`, `false` or no value exits 2**, such as
550
+ `only: false` or `baseline:` in `.mutineer.yml`. A blank `since` also exits 2,
551
+ in the file and on the command line, so an unset shell variable does not
552
+ turn scoping off. `since: false` still means no scoping.
553
+ - **Config file errors start with the file and key**, for example
554
+ `.mutineer.yml: threshold must be a number between 0 and 100`, where they
555
+ started with `--threshold`.
556
+ - **The docs site is built in CI** — a Pages workflow runs `rake site:build`
557
+ and deploys the result, so the YARD HTML under `/api/`, `llms-full.txt`,
558
+ `json-schema.html` and `sitemap.xml` are no longer committed. CI checks
559
+ that the site builds, in place of `rake yard:pages:check`. The site keeps
560
+ its URLs (#153).
561
+
562
+ ## [1.3.0] - 2026-09-29
563
+
564
+ ### Changed
565
+ - **Mutant ids now include the project-relative file path** (#126). Before, two
566
+ mutants with the same method name, operator and token in different files got
567
+ the same id. That happened with a top-level `def` or block in two files, and
568
+ with a class reopened in another file. One `ignore:` entry then suppressed
569
+ both mutants, and `--baseline` could hide a new survivor behind an old one.
570
+ - Every survivor id changes once in this release. This affects any external
571
+ tool that tracks survivors by id.
572
+ - Moving or renaming a file now changes its ids.
573
+ - Ids follow the project root: the directory mutineer runs from, or the
574
+ Action's `working-directory`. A run from a different root gives different ids.
575
+ mutineer finds `.mutineer.yml` by walking up, so a run from a subdirectory
576
+ prints one `[mutineer]` warning that the loaded ignore ids will not match.
577
+ - A source outside the project root uses its absolute path, so its ids differ
578
+ between machines.
579
+ - Two methods with the same qualified name in one file (for example two
580
+ top-level `def index` in two DSL blocks) now get different ids. The second
581
+ and later ones hash their position among those methods; the first keeps its
582
+ id.
583
+ - **The JSON report marks its id format** (schema `1.4`, additive).
584
+ `summary.id_format` is `2` for ids that include the file path.
585
+ `summary.legacy_id_matches` counts old-format `ignore:` entries (`ignore`) and
586
+ survivors matched only through an old-format baseline id (`baseline`). The
587
+ GitHub Action shows one warning annotation when either count is not zero.
588
+
589
+ ### Deprecated
590
+ - **Old-format ids in `ignore:` and in baselines.** Matching on them is removed
591
+ in 2.0. Replace each old `ignore:` entry with the new ids from the warning
592
+ (only the intended ones when it over-matched several mutants).
593
+ Regenerate a baseline (`--format json`) only after every gate that reads it
594
+ runs this version or later (the Action's `version:` pin, your CI
595
+ `Gemfile.lock`). An older version treats every new-format survivor as new.
596
+
597
+ ### Fixed
598
+ - **`module_function :name` and `module_function def name` promote only
599
+ their own module's methods** — a class or module in the same file with a
600
+ method of the same name kept an instance method in Ruby, but mutineer
601
+ named it as a class method. `--strategy redefine` then mutated a method the
602
+ tests never call, so a killable mutant falsely survived, and
603
+ `--only Class#name` selected nothing (#98). The affected methods now get
604
+ their correct names, so their mutant ids change: regenerate any ignore
605
+ entries or baseline survivors that pointed at them.
606
+ - **A root-anchored reopening names the top-level constant** — a
607
+ `module ::Root` or `class ::Solo` written inside another module now gives
608
+ `Root` and `Solo`, not `Outer::Root` and `Outer::Solo`. This applies to every
609
+ method in such a body, with or without `module_function`, so their mutant
610
+ ids change too. `--strategy redefine` rebuilds such a body's scope as
611
+ written (`module Outer` then `class ::Solo`), so a constant from `Outer`
612
+ still resolves in the mutated method, as it does under `reload`. Before,
613
+ the method raised NameError in the test, which counted as a false kill
614
+ (#145).
615
+ - **Old-format ids keep working, with a warning** (#126). An old-format
616
+ `ignore:` entry still suppresses the mutants it matched before. The run prints
617
+ one `[mutineer]` warning per entry with each new id, its file and its method.
618
+ When the entry matched several mutants (in different files, or same-named
619
+ methods in one file), the warning says it
620
+ over-matched and to keep only the ids for the mutant you meant to ignore. A
621
+ `--baseline` file without `summary.id_format` matches on new ids, or on old
622
+ ids from the same file, so no survivor reads as new or fixed only because its
623
+ id changed. A stored file outside the project root (a baseline written on
624
+ another machine) matches on the old id alone. The run prints one
625
+ `[mutineer]` warning to regenerate the baseline.
626
+
627
+ ## [1.2.0] - 2026-09-28
628
+
629
+ ### Added
630
+ - **Operand-removal operator** (Tier-2, opt-in via `--operators`):
631
+ `operand_removal` replaces `a && b` with `(a)` and with `(b)`, and does the
632
+ same for `||`, `and` and `or`. The mutant survives when no test needs the
633
+ operand that the mutant removes. The operator never keeps a jump operand
634
+ (`return`, `break`, `next`, `redo`, `retry`) alone, because a jump does not
635
+ parse in a value context. It never removes an operand that holds a heredoc,
636
+ because the heredoc body stays behind as code. It skips nested method
637
+ definitions, because mutineer mutates each one as its own method.
638
+ - **Array-literal operator** (Tier-2, opt-in via `--operators`):
639
+ `array_literal` replaces a non-empty array literal, such as `[a, b]` or
640
+ `%i[a b]`, with `[]`. The mutant survives when no test checks the contents
641
+ of the array. The operator skips an implicit array (`x = 1, 2`), an array
642
+ that holds a heredoc, and nested method definitions.
643
+ - **Sources also pair with Minitest's `test/**/test_*.rb` files** — after the
644
+ `_test.rb` forms, so existing projects pair as before. `lib/helper.rb` does
645
+ not pair with the `test/test_helper.rb` support file. A failed capture of a
646
+ `test_<name>.rb` file now marks `<name>.rb` uncapturable, as `<name>_test.rb`
647
+ does (#120). Without `framework:` set, a source with `spec/<name>_spec.rb`
648
+ and `test/test_<name>.rb` but no `<name>_test.rb` now pairs with the
649
+ Minitest file, as the "Minitest first" order says.
650
+
651
+ ### Fixed
652
+ - **`reload` loads the mutant by an absolute path** — a relative source path
653
+ gave the mutant relative backtrace paths, so code that checks its own frames
654
+ by absolute path failed for every mutant, a false kill (#123).
655
+ - **`require "test_helper"` works without `RUBYOPT`** — a standalone run puts
656
+ `lib`, then each test file's `test_helper.rb` directory, on the load path,
657
+ as boot mode and `rake test` do. A run where no test records coverage because
658
+ captures failed now exits 1 instead of reporting N/A (#119).
659
+ - **A disable-line marker warns about an operator it does not know** — a
660
+ reason written without `--` became part of the operator name, so the marker
661
+ suppressed nothing and said nothing (#124). A marker followed only by spaces
662
+ or commas, such as `disable-line -- why`, now disables the whole line.
663
+ - **Coverage capture and the clean check run each source once** — they read
664
+ sources with `load`, so a test's own `require` ran them again: a `Struct`
665
+ superclass raised `superclass mismatch`, and load-time code ran twice (#122).
666
+ A mutant of such a class still errors under `--strategy reload`, which loads
667
+ the mutated file again; `--strategy redefine` runs it.
668
+ Code that guards itself to run once (`unless defined?(X)`) can now show as
669
+ covered, so its mutants run where they were `no_coverage` before.
670
+ - **A red unmutated suite now shows why it failed** — in a standalone run, the
671
+ Minitest summary or RSpec output of the failing test, with its failure
672
+ message, goes to stderr before the "not green" error. A passing run prints
673
+ nothing extra. Boot mode (`--rails`, `--boot`) is unchanged (#121).
674
+
675
+ ## [1.1.0] - 2026-09-28
676
+
677
+ ### Added
678
+ - **Safe-navigation operator** (Tier-2, opt-in via `--operators`):
679
+ `safe_navigation` replaces `&.` with `.`. The mutant survives when no test
680
+ passes `nil` to the call.
681
+ - **Range operator** (Tier-2, opt-in via `--operators`): `range` replaces
682
+ `..` with `...` and `...` with `..`. The `..` -> `...` mutant survives when
683
+ no test checks the last element of the range. Endless ranges (`1..`) are
684
+ skipped, because `(1..)` and `(1...)` give the same result for slicing,
685
+ `include?`, `===` and pattern matching.
686
+ - **Negation-removal operator** (Tier-2, opt-in via `--operators`):
687
+ `negation_removal` removes the `!` from `!x` and the `not` from `not x`.
688
+ The mutant survives when no test depends on the negated value. The
689
+ explicit form `x.!` is skipped, because `x.` does not parse.
690
+
691
+ ### Changed
692
+ - **Stderr of tests and specs is visible** in the in-process and `--daemon`
693
+ runs. Mutineer silences stdout once per child process and no longer hides
694
+ stderr, so its own child diagnostics always reach you. `--test-command`
695
+ runs still capture stderr with stdout and show it under `--verbose`.
696
+ - **A mutant's test run stops at the first failing test** — one failure
697
+ already kills the mutant, so the forked child does not run the tests that
698
+ remain. Killed mutants cost less time, and survived mutants cost the same.
699
+ Under Minitest, when the outer reporter of the run records a failure or an
700
+ error (a skip does not count), each remaining test and each remaining test
701
+ class returns before it starts. A skipped class does not start its
702
+ class-level hooks. The run does not unwind: a class that is running
703
+ finishes normally, so its `after_all` hooks and a class-level
704
+ `transaction { super; raise ActiveRecord::Rollback }` still run. RSpec runs
705
+ with `--fail-fast`. This applies to the in-process backend only: coverage
706
+ capture and the clean checks still run every test, and the `--daemon` and
707
+ `--test-command` backends do not change. The CLI `--fail-fast` flag keeps
708
+ its meaning. On rack's `lib/rack/utils.rb` (`--jobs 1`), a full run takes
709
+ about 35–41 s instead of about 86–89 s. With the same coverage map, the
710
+ verdicts are the same.
711
+ - **The mutant run uses a fixed Minitest seed** — with the stop, the test
712
+ order can decide the verdict, so the child runs Minitest with seed `1`
713
+ unless the environment sets `SEED`. The same code then gives the same
714
+ verdict on each run. Coverage capture and the clean checks keep the random
715
+ seed, so the clean check runs the tests in a random order while each mutant
716
+ run uses the fixed order. RSpec keeps its configured order: a suite configured with
717
+ `config.order = :random` can still get a different verdict on each run for
718
+ the case below. In an order-dependent Minitest suite, the fixed seed makes
719
+ a false `killed` happen on every run or on no run, not on some runs.
720
+ - **A mutant whose failing test runs before a hanging test is now `killed`,
721
+ not `timeout`** — the run stops at the failure, before the hang. The tests
722
+ did detect the mutation, so `killed` is the correct verdict. If the hanging
723
+ test runs first in the fixed order, the verdict stays `timeout`. Compared
724
+ with a baseline from an earlier version, the score usually goes up. In an
725
+ order-dependent suite it can also go down: a mutant that a random order
726
+ killed on some runs can survive on every run in the fixed order. A change
727
+ to the tests can change the fixed order, so a later run can move such a
728
+ mutant from `killed` to `timeout`, and a `--baseline` gate then reports a
729
+ score drop.
730
+ - **Some runs still run most tests** — Minitest `parallelize_me!`, and Rails
731
+ `parallelize` above its threshold (by default more than 50 tests in the
732
+ child, or at any test count when `PARALLEL_WORKERS` is 2 or more in the
733
+ environment), queue their tests before the first result comes back, so the
734
+ queued tests still run. The verdict is the same as before. Below the
735
+ Rails threshold, the tests run one after the other in the child, and the
736
+ stop works.
737
+
738
+ ### Fixed
739
+ - **Tests that reopen `$stdout`** (Minitest's `capture_subprocess_io`,
740
+ RSpec's `to_stdout_from_any_process`) no longer make a green suite
741
+ "not green" or count as false kills.
742
+ - **Test or source files that print while they load** no longer make coverage
743
+ capture fail with `invalid coverage output`. The capture subprocess now
744
+ sends its result over a separate pipe, not over stdout.
745
+ - **Chain-link operator** (Tier-2, opt-in via `--operators`):
746
+ `chain_link` drops one call from a chain, with its arguments and block
747
+ (`user.account.name` -> `user.name`). The mutant survives when no test tells
748
+ the chain apart from the same chain without that step. Conversions and copies
749
+ (`to_s`, `to_a`, `dup`, `freeze`, ...) and `new` are never dropped.
750
+
751
+ ## [1.0.2] - 2026-09-21
752
+
753
+ ### Added
754
+ - **AI-readable docs wiring**: HTML pages with Markdown twins now advertise
755
+ `rel="alternate" type="text/markdown"`, `index.md` is the landing/CLI
756
+ essentials twin, `skill.md` is listed under Optional in `llms.txt`, and
757
+ `sitemap.xml` is generated from the same catalog as `llms.txt` (#91).
758
+ - **Single-source CLI contract**: exit codes and `--threshold` live in
759
+ `docs/fragments/contract.yml`; `rake docs:generate` writes `llms-full.txt`,
760
+ `json-schema.html`, and the marked copies so they cannot drift (#82).
761
+ - **YARD API on Pages**: the current gem's YARD HTML is published at
762
+ `/api/`, linked from the docs site, and `rake yard:pages:check` keeps it
763
+ from lagging the shipped sources. `documentation_uri` stays the Pages
764
+ root (#92).
765
+
766
+ ## [1.0.1] - 2026-09-18
767
+
768
+ ### Added
769
+ - **RubyGems `documentation_uri`**: the published gem now points at the docs
770
+ site (`https://davidteren.github.io/mutineer/`) so gem-page discovery
771
+ reaches the Pages docs (#90).
772
+
773
+ ### Fixed
774
+ - **A red unmutated suite can no longer pass a mutation gate** — coverage
775
+ capture now keeps the original Minitest/RSpec result, and a failing clean run
776
+ aborts with the existing smoke-check error (exit 1) instead of scoring those
777
+ assertion failures as killed mutants. A warm coverage cache re-checks the
778
+ current suite and cannot bypass this (#96).
779
+ - **Concurrent external runs no longer restore each other's source files** —
780
+ swap and orphan recovery share one exclusive OS lock per source, acquired
781
+ before reading or healing. A live owner's mutant and backup stay intact; a
782
+ dead owner's backup still restores the original bytes (#99).
783
+ - **Coverage cache now invalidates when a required test helper changes** —
784
+ a successful map records fingerprints of project-local loaded Ruby files,
785
+ old cache entries without that data rebuild, and a helper-only edit no
786
+ longer hides a new survivor behind a stale 100% score (#97).
787
+ - **Release version calculation ignores floating major tags** — `release-pr.yml`
788
+ selects the newest complete `vMAJOR.MINOR.PATCH` ancestor and validates the
789
+ next version before writing files, so a later `v1` tag can no longer produce
790
+ `v1..1` (#95).
791
+
792
+ ## [1.0.0] - 2026-09-08
793
+
794
+ The GitHub Action's PR default changes in this release, which is why it is a
795
+ new major: workflows pinned to `davidteren/mutineer@v0` keep the old full-scan
796
+ behavior; upgrading to `@v1` opts into diff-scoped PR runs (details under
797
+ Changed).
798
+
799
+ ### Added
800
+ - **The Action reports where CI readers look**: with the default JSON format it
801
+ writes a score/pass-fail table (plus the baseline delta, when `baseline` is
802
+ set) to the job step summary, and emits one `file=…,line=…` annotation per
803
+ surviving mutant (up to 50; the summary lists the first 20, the full set
804
+ stays in the JSON report) so results land on the PR diff instead of only in a
805
+ collapsed log group — `error` level when the gate failed, `warning` when it
806
+ passed. When `output` is unset the JSON report is routed to a temp file and
807
+ still printed to the log, and the `report` output now exposes the report
808
+ path in both cases so a later step can consume the JSON without scraping
809
+ the log (#86).
810
+ - **Progress during the run**: every backend prints `[mutineer] N/M mutants
811
+ (P%)` to stderr at each 10% step, so a long run is never silent between
812
+ config resolution and the report. Stdout stays byte-exact for `--format json`
813
+ and `--output`. `WorkerPool#run` gains an optional `on_result:` callback for
814
+ this (called in the parent per reaped result) (#86).
815
+
816
+ ### Changed
817
+ - **PR runs scope themselves in the GitHub Action**: on `pull_request` events
818
+ the `since` input now defaults to the PR base, so the action grades just the
819
+ diff out of the box. **Migration note (the reason for the major bump)**: a
820
+ PR gate that previously full-scanned now scores only the PR's changed lines,
821
+ so `threshold` applies to fewer mutants. Stay on `@v0` to keep the old
822
+ default, or pass `since: none` on `@v1` for full scans. Workflows that
823
+ already pass a non-empty `since` are unchanged (an explicit empty string is
824
+ indistinguishable from unset and picks up the new default). A PR with no
825
+ changed source lines (docs- or test-only) scores zero mutants and passes the
826
+ scoped gate vacuously; keep a full-scan baseline refresh on main as the
827
+ backstop. With `use-bundler: true` the caller's Gemfile picks the gem, and
828
+ the new default needs mutineer >= 1.0.0 (the action enforces the floor with
829
+ a clear error). The action scopes to the PR's exact base commit from the
830
+ event payload (immune to the
831
+ base branch advancing mid-job), falling back to a fresh fetch of the base
832
+ branch tip; when neither can be resolved it warns and runs without an
833
+ action-provided `--since` (a `.mutineer.yml` `since:` key, if any, still
834
+ applies). The default deliberately does NOT fire on `pull_request_target`:
835
+ checkout there defaults to the base branch, so auto-scoping would diff the
836
+ base against itself and green the gate on an empty run (#86).
837
+ - **`--baseline` on a diff-scoped run gates on new survivors only**: a
838
+ `--since` run's score covers only the changed-line mutants, a different
839
+ denominator from a full-run baseline, so comparing the two scores
840
+ manufactured false regressions (a 3/4-mutant PR at 75% "dropped" from a
841
+ 92% whole-repo baseline with zero new survivors). With `--since`, the
842
+ score-drop half of the baseline gate is skipped; new-survivor detection by
843
+ stable id (and the reported before/after scores) are unchanged. The JSON
844
+ report records the scope in a new additive `summary.scoped` key
845
+ (`schema_version` 1.3), and the `baseline` block records `score_comparable`
846
+ so a consumer knows when not to render the two scores as a comparison
847
+ (`fixed_survivors` is likewise empty under a scoped side: an out-of-scope
848
+ survivor was never re-tested, so absence does not mean fixed). The reverse
849
+ direction is a hard guard: a scoped report is refused as a baseline (exit 2
850
+ with a regenerate hint), because survivors outside its diff would all read
851
+ as new regressions. A new `--no-since` flag disables diff scoping explicitly: a
852
+ typed no beats a `.mutineer.yml` `since:` key, and the action's
853
+ `since: none` passes it through (#86).
854
+
855
+ ## [0.11.4] - 2026-07-29
856
+
857
+ ### Fixed
858
+ - **`--threshold` now gates on the run being complete, not just its score** — a
859
+ score is `killed / (killed + survived)`, so mutants that error, time out or come
860
+ back uncapturable are excluded from the denominator: a broken harness *raised*
861
+ the score instead of lowering it. Ninety errored mutants and ten that ran (nine
862
+ killed) reported 90% and exited 0, so CI could not tell a complete run from a
863
+ mostly-broken one. Past 10% of attempted mutants producing no verdict, a positive
864
+ `--threshold` now exits 1 whatever the score, and the report says which states
865
+ broke, on every `--format`. A single bad mutant never trips it, however small the
866
+ run, so a `--since` PR with a handful of mutants keeps its flake tolerance (#78).
867
+
868
+ ### Added
869
+ - **`no_verdict[]` in the JSON report** (`schema_version` 1.2) — every attempted
870
+ mutant that produced no verdict, with its `status` and, where there is one, the
871
+ `details` explaining the cause. `Result#details` was built and rendered in no
872
+ format at all, so a daemon crash reached the user as nothing but a larger errored
873
+ count. `summary` gains `attempted` and `no_verdict`, the two figures the
874
+ completeness gate is computed from (#78).
875
+
876
+ ## [0.11.3] - 2026-07-29
877
+
878
+ ### Fixed
879
+ - **Nothing to mutate now costs nothing** — `--since` matching no changed line (a
880
+ docs-only PR, which README documents `--since origin/<base>` for) still did the
881
+ expensive part before discovering there was no work: `--daemon` booted the app
882
+ once for the coverage map and again for every `--jobs` worker, and
883
+ `--test-command` ran the whole suite for a smoke check that calibrates a timeout
884
+ no mutant would use. Both backends now return as soon as the job list is empty.
885
+ The in-process path is unchanged: it boots before collecting jobs, so it cannot
886
+ know the list is empty yet. One consequence worth stating: a zero-job `--daemon`
887
+ run no longer boots the app, so an app that fails to boot now exits 0 (nothing
888
+ to do) instead of exit 1 (#76).
889
+ - **A dead daemon ends the run instead of scoring the rest against it** — a
890
+ daemon that dies while running one mutant was already handled: `DaemonClient`
891
+ respawns and answers `error` for that mutant. But a daemon that could not come
892
+ back was not. `restart!` closes the pipes before respawning, so if the respawn
893
+ failed at the OS level (`EMFILE` or `ENOMEM` under `--jobs N`, `ENOENT` when
894
+ `bundle` does not resolve) the client was left permanently dead while raising
895
+ errors that read as ordinary per-mutant failures. Every remaining mutant scored
896
+ `error` against nothing, and Mutineer printed a mutation score built on the
897
+ fraction of mutants that ran before the daemon died. `DaemonClient` now raises
898
+ `DaemonBootError` whenever it is gone for good — a refused spawn, a failed boot
899
+ handshake on respawn, closed pipes, or `MAX_RESTARTS` crashes — and the CLI
900
+ reports it as a message and exit 1 rather than a backtrace. Under `--jobs N`
901
+ the remaining queue is dropped so sibling workers stop too. Errored mutants
902
+ still cannot fail the `--threshold` gate once any mutant is scored (#78).
903
+
904
+ ### Changed
905
+ - **Daemon orchestration split out of `Runner`** — the persistent-daemon backend
906
+ (job fan-out, worker DBs, coverage-map build, boot config, verdict mapping) now
907
+ lives in `Mutineer::DaemonBackend`. `Runner` keeps job collection, coverage
908
+ selection and the single in-process mutant run, and drops from 662 to 439 lines.
909
+ The external backend's orchestration stays on `Runner` for now, so the two
910
+ backends are not yet symmetrical. No CLI or behaviour change (#58).
911
+ Internal-only removals: `Runner.execute_daemon`, `Runner.daemon_coverage_map`
912
+ and `Runner::DAEMON_TIMEOUT` no longer exist. They were never documented —
913
+ the supported programmatic contract is the JSON report, the stable mutant ids
914
+ and the exit codes — so only code calling `Runner` internals directly is
915
+ affected.
916
+ - **Comment diet on orchestration files** — present-tense YARD contracts; drop
917
+ ticket/phase/KTD history tags from runner, CLI, daemon pair, coverage map,
918
+ reporter, result, and related modules. Safety and score invariants kept (#57).
919
+
920
+ ## [0.11.2] - 2026-07-24
921
+
922
+ ### Fixed
923
+ - **`--test-command` under version managers** — scrub Mutineer's rbenv/asdf
924
+ version bins and bundler/gem env from the child so the suite can resolve the
925
+ app's Ruby via shims / `.ruby-version`. Smoke check prints a targeted hint on
926
+ `Bundler::RubyVersionMismatch` instead of only blaming DB/migrations. Docs
927
+ cover a wrapper recipe for stubborn setups (#32).
928
+
929
+ ## [0.11.1] - 2026-07-23
930
+
931
+ ### Fixed
932
+ - **`--daemon` score disclosure** — stop claiming “no coverage narrowing / lower
933
+ bound” on every run (narrowing already ships). Warn only when the coverage map
934
+ is unavailable and the run falls back to the full `--test` set (#48).
935
+ - **`--threshold` CI fidelity** — exit 1 when a positive threshold is set but the
936
+ run produced only errors/timeouts/uncapturable (nil score with a broken harness)
937
+ instead of silently green; pure no_coverage / all-ignored still skips the gate
938
+ (#49). Non-numeric `--threshold` / YAML `threshold` is a usage error (exit 2),
939
+ not a silent gate-off (#51).
940
+ - **`--daemon` contracts** — reject `--framework rspec` (exit 2); force
941
+ `--strategy reload` with a clear warning when redefine was requested (daemon
942
+ whole-file only) (#50). **In-process `--rails` always serial** unless
943
+ `--daemon` (only daemon has per-worker DB isolation) (#55).
944
+ - **Isolation timeout** — kill the child process group and honor a clean exit
945
+ that races the deadline (not always timeout) (#52). **External backend** —
946
+ signal death is `error`, not `killed` (#53).
947
+ - **`--fail-fast` is serial** on the in-process path (matches daemon) so the
948
+ survivor set is deterministic under any `--jobs` (#54).
949
+ - **Dry-run uses `collect_jobs`** so candidates match a real run (#59).
950
+ - **Daemon schema load once per worker** (not every mutant fork) (#56).
951
+ - **Nested `statement_removal`** visits inner statement lists (#62).
952
+ - Dead seams: remove unused `RailsWorkerDb.provision`, simplify parallel daemon
953
+ fail-fast branch, drop unused `validate_daemon!` arg (#60).
954
+ - Archive historical implementation spec under `docs/archive/` (#61).
955
+
956
+ ## [0.11.0] - 2026-07-02
957
+
958
+ ### Added
959
+ - **`--daemon` backend — fast, parallel-safe Rails mutation testing** (#26/#27
960
+ Phase 2). Boots the app **once** in a persistent daemon and forks per mutant
961
+ (restoring shared-boot speed), and gives **each parallel worker its own
962
+ database** so `--jobs N` is safe under Rails for the first time — parallel
963
+ verdicts are proven identical to serial (no fixture cross-talk). Coverage
964
+ narrowing is restored on this path (each mutant runs only its covering tests;
965
+ a mutant on an uncovered line is `no_coverage`), so the daemon score is
966
+ comparable to the in-process `--rails` score. Opt in with `--rails --daemon`
967
+ (also `daemon: true` in `.mutineer.yml`); `--daemon` can't be combined with
968
+ `--test-command`. **SQLite** today (hermetic, CI-proven); **Postgres**
969
+ per-worker provisioning is in progress (#34/#35). The gem core stays Prism +
970
+ stdlib, zero runtime dependencies — worker-DB routing uses the app's own
971
+ ActiveRecord.
972
+
973
+ ## [0.10.0] - 2026-07-02
974
+
975
+ ### Added
976
+ - **`--test-command` external backend** (#27) — mutation-test apps pinned to Ruby
977
+ < 3.4. Mutineer stays on ≥ 3.4 but runs your suite as a subprocess in the app's
978
+ own runtime via `--test-command "bundle exec rails test %{files}"` (`%{files}`
979
+ expands to the `--test` paths; env is inherited). The mutant is applied on disk
980
+ with crash-safe backup/restore (self-heals a hard-killed run on next startup); a
981
+ smoke check aborts before scoring if the unmutated suite isn't green. This path
982
+ is reload-only, serial (`--jobs` forced to 1), and does no coverage narrowing —
983
+ so its score is an upper bound, not comparable to an in-process `--rails` score
984
+ (Mutineer prints this caveat). Also settable as `test_command:` in `.mutineer.yml`.
985
+ Safe parallelism for this path is tracked in #26.
986
+
987
+ ## [0.9.1] - 2026-07-01
988
+
989
+ ### Fixed
990
+ - **Per-method uncapturable granularity** (#25) — the `:uncapturable` taint was
991
+ whole-file, so a method reachable only by a *failed* capture in an otherwise-
992
+ covered file was mislabeled `no_coverage`. It's now attributed per method (by
993
+ the method's body coverage), so only the affected method is tainted. Fully-
994
+ failed files are unchanged.
995
+
996
+ ## [0.9.0] - 2026-06-30
997
+
998
+ ### Added
999
+ - **`--fail-fast`** (#21) — stop at the first surviving mutant; in-flight workers
1000
+ drain, the rest are skipped. Fast red signal for PR gates.
1001
+ - **`--format html`** (#23) — a single self-contained HTML report (inline CSS, no
1002
+ external assets) with the score, per-source table, and a card per survivor
1003
+ (subject, file:line, operator, stable id, diff).
1004
+ - **String, regex, and collection-method operators** (#24, Tier-2, opt-in via
1005
+ `--operators`): `string_literal`, `regex`, `collection_method`
1006
+ (`map`↔`each`, `all?`↔`any?`, `first`↔`last`, `min`↔`max`, `select`↔`reject`).
1007
+
1008
+ ### Changed
1009
+ - **`--dry-run` now honors suppression** (#22) — inline `# mutineer:disable-line`
1010
+ and `.mutineer.yml` `ignore:` entries are omitted from the preview and counted
1011
+ as "ignored (suppressed)", matching a real run.
1012
+
1013
+ ## [0.8.0] - 2026-06-30
1014
+
1015
+ ### Fixed
1016
+ - **Singleton methods are now mutated** (#20) — `class << self` and
1017
+ `module_function` methods were discovered but applied to the instance scope, so
1018
+ the mutant never ran on the singleton the caller dispatches to; every such
1019
+ mutant falsely survived and the file read a false 0%. `module_function` methods
1020
+ are now discovered as singletons, and the redefine strategy re-opens
1021
+ `class << self` so the mutation lands on the called method. (Scores for
1022
+ singleton-heavy files will rise to their true values.)
1023
+ - **Write-heavy Rails tests are capturable again** (#19) — capture/worker pipes
1024
+ are `binmode`: a binary Marshal payload over a text-mode pipe could raise an
1025
+ encoding error the child then swallowed → empty pipe → false `:uncapturable`.
1026
+ That was the root cause of the residual write-heavy failures too. A child that
1027
+ dies without writing now also reports how it died (exit status / signal), and
1028
+ `--verbose` always surfaces a real reason. Verified on a real Rails app: all 6
1029
+ previously-uncapturable interactors (incl. caxlsx + Google-client) now capture
1030
+ with real scores, 0 uncapturable.
1031
+
1032
+ ## [0.7.1] - 2026-06-30
1033
+
1034
+ ### Added
1035
+ - **GitHub Action** (`action.yml`, composite) wrapping the CLI for CI — gate a PR
1036
+ on new survivors / score drop with `sources`, `since`, `baseline`, `threshold`,
1037
+ etc. Inputs are passed via `env` (no `${{ }}` interpolation into the run script)
1038
+ for command-injection safety.
1039
+ - Docs site (GitHub Pages) with Open Graph / Twitter Card share image; YARD doc
1040
+ comments across the library.
1041
+
1042
+ ## [0.7.0] - 2026-06-30
1043
+
1044
+ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
1045
+
1046
+ ### Added
1047
+ - **Equivalent-mutant suppression** (#10) — inline `# mutineer:disable-line [ops]`
1048
+ and a `.mutineer.yml` `ignore:` list keyed on a stable, offset-free mutant id;
1049
+ suppressed mutants are excluded from the score (100% reachable). The stable id
1050
+ (and readable token) is emitted per survivor in JSON.
1051
+ - **Source→test auto-pairing + multi-source runs** (#11) — pass a directory or
1052
+ several sources with `--test` omitted; tests are inferred by convention
1053
+ (`app/`,`lib/` → `test/…_test.rb` / `spec/…_spec.rb`) and run under one boot,
1054
+ with per-source results (human + JSON `per_source`).
1055
+ - **`--baseline <file.json>` CI gating** (#13) — diff against a prior run by stable
1056
+ id; exit 1 on new survivors or a score drop (with `--baseline-epsilon`), naming
1057
+ what regressed. Combines with `--threshold` via max exit code.
1058
+ - **`--verbose`/`--debug`** (#8) — surface the real error when a fork capture fails.
1059
+ - **`:uncapturable` status** (#9) — distinct from `no_coverage`; reported separately
1060
+ ("tests failed to run" vs "genuinely uncovered"). Both excluded from the score.
1061
+
1062
+ ### Fixed
1063
+ - **Fork capture no longer drops fixture transactions** (#8) — `reconnect` skips
1064
+ `clear_all_connections!` when a fixture transaction is open, and stops swallowing
1065
+ the child error; write-heavy Rails tests are mutation-testable again.
1066
+
1067
+ ### Changed
1068
+ - JSON `schema_version` → `1.1` (additive: survivor `id`/`token`, `ignored`,
1069
+ `uncapturable`, `per_source`).
1070
+
1071
+ ## [0.6.2] - 2026-06-29
1072
+
1073
+ ### Fixed
1074
+ - **`--rails` defaults `RAILS_ENV` to `test`** (#7) — an unset `RAILS_ENV` booted
1075
+ development, where the suite isn't loaded, so every mutant was falsely reported
1076
+ `no_coverage` (score N/A, exit 0). An explicit `RAILS_ENV` is respected.
1077
+ - **`--rails` defaults to `--jobs 1`** (#12) — parallel mutant forks share one
1078
+ database and deadlock on transactional fixtures; explicit `--jobs N` opts back
1079
+ into parallelism.
1080
+
1081
+ ### Added
1082
+ - **Tier-2 operator discoverability** (#14) — the human-format run summary now
1083
+ lists the available opt-in tier-2 operators and how to enable them.
1084
+
1085
+ ## [0.6.1] - 2026-06-29
1086
+
1087
+ ### Changed
1088
+ - **Removed `eval` entirely** — the redefine strategy now `load`s the wrapped
1089
+ method snippet from a tempfile instead of evaluating a string. Behavior is
1090
+ identical (top-level load rebuilds the same `Module.nesting`), but the gem no
1091
+ longer uses dynamic string execution, clearing supply-chain scanner flags.
1092
+ Zero runtime dependencies unchanged.
1093
+
1094
+ ## [0.6.0] - 2026-06-28
1095
+
1096
+ ### Added
1097
+ - **RSpec support** (#6) — `--framework rspec` (or auto-detected when most
1098
+ `--test` files end in `_spec.rb`) runs RSpec suites instead of Minitest, via a
1099
+ pluggable test-runner abstraction. Both frameworks are loaded lazily, so
1100
+ Mutineer keeps zero runtime gem dependencies and works in an rspec-only
1101
+ project; coverage selection works for both. `.mutineer.yml` accepts `framework:`.
1102
+
1103
+ ### Fixed
1104
+ - **Redefine strategy keeps compact `class A::B` as a single nesting wrapper**
1105
+ (#5) — avoids a constant-resolution disagreement with the reload strategy.
1106
+
1107
+ ## [0.5.0] - 2026-06-28
1108
+
1109
+ ### Fixed
1110
+ - **`class << self` methods are now discovered and mutated** (#3) — previously
1111
+ they were treated as instance methods, so the redefine strategy mis-targeted
1112
+ them. `class << other_obj` blocks are skipped (not representable).
1113
+ - **Worker pool no longer deadlocks on large results** (#4) — pipes are drained
1114
+ with `IO.select` and children reaped on EOF, so a result bigger than the OS
1115
+ pipe buffer (~64KB) can't wedge the run.
1116
+
1117
+ ## [0.4.0] - 2026-06-28
1118
+
1119
+ ### Added
1120
+ - **`--since <git-ref>`** (#2) — mutate only the lines changed since a git ref
1121
+ (e.g. `--since origin/main`), so CI on a pull request mutation-tests just the
1122
+ new/changed code. Composes with coverage selection; `--dry-run --since`
1123
+ narrows the preview too. Unknown ref / not-a-git-repo exits 2.
1124
+
1125
+ ## [0.3.0] - 2026-06-28
1126
+
1127
+ ### Added
1128
+ - **Coverage-guided test selection in boot mode** (#1) — `--rails`/`--boot` now
1129
+ captures coverage by forking the booted app and runs only the test files that
1130
+ cover each mutant's line (uncovered lines report `no_coverage`), instead of
1131
+ running every `--test` file for every mutant. Cached like standalone mode.
1132
+
1133
+ ## [0.2.0] - 2026-06-28
1134
+
1135
+ ### Added
1136
+ - **Boot mode for Rails (and any app needing its environment booted)** —
1137
+ `--rails` boots `config/environment` once in the parent and forks per mutant
1138
+ (children inherit the booted app), defaults the strategy to `redefine`, and
1139
+ reconnects ActiveRecord in each fork for DB fork-safety. `--boot FILE` boots a
1140
+ custom entry point. Boot mode requires `--test` files and runs them for every
1141
+ mutant (coverage-guided selection in boot mode is future work). `.mutineer.yml`
1142
+ accepts `boot:` and `rails:`.
1143
+ - GitHub Actions CI (test suite + gem build on Ruby 3.4, ubuntu + macos).
1144
+
1145
+ ### Changed
1146
+ - `--strategy` values are now `reload` / `redefine` (canonical); `7a` / `7b`
1147
+ remain accepted as deprecated aliases.
1148
+
1149
+ ## [0.1.0] - 2026-06-28
1150
+
1151
+ ### Added
1152
+ - Initial release of Mutineer — a clean-room, Prism-based mutation-testing tool
1153
+ for Ruby with zero runtime dependencies (Ruby ≥ 3.4).
1154
+ - Mutation operators: arithmetic, comparison, boolean-connector, boolean-literal,
1155
+ statement-removal (Tier 1, default); return-nil, literal-mutation,
1156
+ condition-negation (Tier 2, opt-in via `--operators`).
1157
+ - Coverage-guided test selection with a digest-keyed, auto-invalidating cache.
1158
+ - Fork-isolated, parallel execution (`--jobs`) with per-mutant timeouts.
1159
+ - Two application strategies: `reload` (whole-file, default) and `redefine`
1160
+ (surgical), verified to agree on namespaced multi-statement methods. (`7a`/`7b`
1161
+ accepted as deprecated aliases.)
1162
+ - `run`, `--dry-run`, `--threshold`, `--only`, `--operators`, `--strategy`,
1163
+ `--format human|json`, `--output`, `--list-operators`.
1164
+ - `.mutineer.yml` configuration (CLI > config > default precedence).
1165
+ - Byte-correct source handling for multibyte (UTF-8) sources.
1166
+
1167
+ [1.6.0]: https://github.com/davidteren/mutineer/releases/tag/v1.6.0
1168
+ [1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
1169
+ [1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
1170
+ [1.3.0]: https://github.com/davidteren/mutineer/releases/tag/v1.3.0
1171
+ [1.2.0]: https://github.com/davidteren/mutineer/releases/tag/v1.2.0
1172
+ [1.1.0]: https://github.com/davidteren/mutineer/releases/tag/v1.1.0
1173
+ [1.0.2]: https://github.com/davidteren/mutineer/releases/tag/v1.0.2
1174
+ [1.0.1]: https://github.com/davidteren/mutineer/releases/tag/v1.0.1
1175
+ [1.0.0]: https://github.com/davidteren/mutineer/releases/tag/v1.0.0
1176
+ [0.11.4]: https://github.com/davidteren/mutineer/releases/tag/v0.11.4
1177
+ [0.11.3]: https://github.com/davidteren/mutineer/releases/tag/v0.11.3
1178
+ [0.11.2]: https://github.com/davidteren/mutineer/releases/tag/v0.11.2
1179
+ [0.11.1]: https://github.com/davidteren/mutineer/releases/tag/v0.11.1
1180
+ [0.11.0]: https://github.com/davidteren/mutineer/releases/tag/v0.11.0
1181
+ [0.10.0]: https://github.com/davidteren/mutineer/releases/tag/v0.10.0
1182
+ [0.9.1]: https://github.com/davidteren/mutineer/releases/tag/v0.9.1
1183
+ [0.9.0]: https://github.com/davidteren/mutineer/releases/tag/v0.9.0
1184
+ [0.8.0]: https://github.com/davidteren/mutineer/releases/tag/v0.8.0
1185
+ [0.7.1]: https://github.com/davidteren/mutineer/releases/tag/v0.7.1
1186
+ [0.7.0]: https://github.com/davidteren/mutineer/releases/tag/v0.7.0
1187
+ [0.6.2]: https://github.com/davidteren/mutineer/releases/tag/v0.6.2
1188
+ [0.6.1]: https://github.com/davidteren/mutineer/releases/tag/v0.6.1
1189
+ [0.6.0]: https://github.com/davidteren/mutineer/releases/tag/v0.6.0
1190
+ [0.5.0]: https://github.com/davidteren/mutineer/releases/tag/v0.5.0
1191
+ [0.4.0]: https://github.com/davidteren/mutineer/releases/tag/v0.4.0
1192
+ [0.3.0]: https://github.com/davidteren/mutineer/releases/tag/v0.3.0
1193
+ [0.2.0]: https://github.com/davidteren/mutineer/releases/tag/v0.2.0
1194
+ [0.1.0]: https://github.com/davidteren/mutineer/releases/tag/v0.1.0