mutineer 1.3.0 → 1.5.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +317 -0
  3. data/README.md +200 -28
  4. data/lib/mutineer/changed_lines.rb +57 -2
  5. data/lib/mutineer/cli.rb +142 -92
  6. data/lib/mutineer/config.rb +215 -56
  7. data/lib/mutineer/coverage_map.rb +161 -14
  8. data/lib/mutineer/daemon_backend.rb +3 -0
  9. data/lib/mutineer/daemon_client.rb +170 -13
  10. data/lib/mutineer/daemon_server.rb +2 -1
  11. data/lib/mutineer/external_backend.rb +2 -2
  12. data/lib/mutineer/isolation.rb +91 -6
  13. data/lib/mutineer/kill_channel.rb +284 -0
  14. data/lib/mutineer/kill_matrix.rb +88 -0
  15. data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
  16. data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
  17. data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +14 -68
  18. data/lib/mutineer/minitest_integration.rb +27 -10
  19. data/lib/mutineer/mutant_id.rb +5 -4
  20. data/lib/mutineer/mutator_registry.rb +12 -3
  21. data/lib/mutineer/mutators/array_literal.rb +0 -7
  22. data/lib/mutineer/mutators/base.rb +63 -4
  23. data/lib/mutineer/mutators/chain_link.rb +0 -7
  24. data/lib/mutineer/mutators/collection_method.rb +1 -1
  25. data/lib/mutineer/mutators/condition_forcing.rb +134 -0
  26. data/lib/mutineer/mutators/operand_removal.rb +0 -7
  27. data/lib/mutineer/mutators/operator_assignment.rb +113 -0
  28. data/lib/mutineer/mutators/return_nil.rb +5 -11
  29. data/lib/mutineer/mutators/string_literal.rb +1 -1
  30. data/lib/mutineer/pairing.rb +222 -15
  31. data/lib/mutineer/parser.rb +12 -3
  32. data/lib/mutineer/project.rb +7 -0
  33. data/lib/mutineer/rails_worker_db.rb +1 -1
  34. data/lib/mutineer/reporter.rb +288 -52
  35. data/lib/mutineer/result.rb +37 -1
  36. data/lib/mutineer/runner.rb +133 -16
  37. data/lib/mutineer/statement_lines.rb +143 -0
  38. data/lib/mutineer/test_runners/minitest.rb +4 -2
  39. data/lib/mutineer/test_runners/rspec.rb +243 -3
  40. data/lib/mutineer/test_runners.rb +5 -3
  41. data/lib/mutineer/version.rb +1 -1
  42. data/lib/mutineer/worker_pool.rb +4 -2
  43. data/lib/mutineer.rb +3 -0
  44. metadata +10 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7b8a623ac7db818d33df11a7ca32e26a67db7f156061027f018debeb17ced2f
4
- data.tar.gz: 5957fb6e57cb5a5149822818f773f3e8d3da271bce7bb0cfc4d0a8640764aa40
3
+ metadata.gz: 50113a063a85737ad99476f85e78cb2ac015f267018d19ba6350ba36ff4ac6df
4
+ data.tar.gz: c12343a00d2df6d495859644c326bc650e33d013ddf009f2648c06a199aceb86
5
5
  SHA512:
6
- metadata.gz: 1de2a131774b7b587abbc8ca91a57da333688cb2fdb937defa002d661426137decf8b86fbde1a2d176a5525f8cb7d9a8b6a2bebc353960729704b3e8d2473213
7
- data.tar.gz: '094d062c4c8458fda7061a575681f13022131b2b9078e5731bcbbfa300ac67b86e53691e4d9e104cc06e50832b66bd3dd32433c1bdf420e74b75a7b7748b4916'
6
+ metadata.gz: a170b15c6883a4dcec6a2798c3b102a5f9c9ded1f46c166df98d0e5bdf5b891ca5b24892efe06245680b45a94da38638adc36ca9a48a48d2ac1c07b29126f173
7
+ data.tar.gz: a30272ff6afb48dd666d6945c6532d987ef335e0e78dc422420cf26cdbec639ee1c850118a10ba25e10a426a2644b3926e39698126eaad0646707a4f2be2e719
data/CHANGELOG.md CHANGED
@@ -6,6 +6,321 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.5.0] - 2026-10-06
10
+
11
+ ### Added
12
+ - **`--timeout SECONDS` and `--capture-timeout SECONDS`**, also `timeout:` and
13
+ `capture_timeout:` in `.mutineer.yml`. They set the in-process per-mutant
14
+ time limit (default 10s) and the coverage-capture time limit (default 120s),
15
+ which were fixed. On a large Rails suite the clean run of the unmutated
16
+ tests took longer than 120s, so every run that offered the whole suite
17
+ stopped as not green, and a mutant on a line many tests cover took longer
18
+ than 10s and was scored `timeout`. `--daemon` keeps its own per-mutant
19
+ limit but uses `--capture-timeout` for its coverage capture;
20
+ `--test-command` uses neither. Either backend warns about a limit it was
21
+ given and does not use.
22
+ - **`--cache-dir DIR`**, also `cache_dir:` in `.mutineer.yml`, sets where the
23
+ coverage cache is written (default `.mutineer`, as before). Two runs from the
24
+ same project root shared `.mutineer/coverage.json` and could overwrite each
25
+ other's map; each can now have its own directory. `--daemon` uses it too.
26
+ A blank value (an unset CI variable) exits 2 rather than writing the cache
27
+ into the project root.
28
+ - **The README states how timeouts affect the score** (#62): a timed-out
29
+ mutant is left out of the score, and counts as a mutant with no verdict
30
+ under `--threshold`.
31
+ - **`--matrix` reports which tests kill each mutant** (`matrix: true` in
32
+ `.mutineer.yml`). Each mutant runs every test in its covering files instead
33
+ of stopping at the first failure, and the child sends each test's outcome to
34
+ the parent as it is recorded. The report names blind tests, which ran in a
35
+ complete row and killed no mutant, and redundant tests, whose every kill
36
+ another test also makes. Redundancy is judged one test at a time, so delete
37
+ redundant tests one at a time. The human report lists up to 20 of each, the
38
+ HTML report all of them, and the JSON report moves to schema `1.5` with a
39
+ `matrix` block that appears only with the flag. A test is its file and its
40
+ id (the example id under RSpec), so examples that share a description stay
41
+ apart and an example whose generated description changes with the mutant
42
+ stays one test. Verdicts, the score and the exit code do not change: each
43
+ mutant gets the verdict a run without the flag gives. After a serial test
44
+ fails, an exit, a crash or the timeout in a later test, class or group
45
+ leaves it `killed`; an end in the code that run still reaches (the failing
46
+ test's class wrapper or group hooks, the suite's own cleanup) and a failure
47
+ in a `parallelize_me!` class keep the exit status. A row is complete only
48
+ when the child's stream arrived in order and every test reported (under
49
+ Minitest the recorder saw every test, under RSpec every planned example),
50
+ so a caught `Interrupt` leaves it incomplete. The human report names up
51
+ to 20 incomplete rows and the HTML and JSON reports all of them; the human
52
+ and HTML reports warn not to delete a blind test until its rows are
53
+ complete.
54
+ Minitest and RSpec 3.3+ are supported, on the in-process backend only;
55
+ `--daemon`, `--test-command`, `--fail-fast` and `--dry-run` exit 2 with it.
56
+ `--no-matrix` and `--no-fail-fast` beat the matching `.mutineer.yml` key,
57
+ so a file that sets one can still run with a flag that conflicts with it.
58
+ - **`no_coverage[]` and `uncapturable[]` name each mutant.** Every entry in
59
+ the JSON report now has `operator`, `token` and `id`, as `ignored[]` has, so
60
+ mutants on one line can be told apart and an `id` can go in `ignore:`. The
61
+ entries are sorted by `(file, line, operator, id)`. `schema_version` is `1.6`.
62
+
63
+ - **The site and README explain "clean-room" and compare Mutineer with
64
+ Mutant**, and the landing page hero has a one-minute explainer video. It
65
+ plays at 55% volume while in view (not with reduced motion; muted until
66
+ the first click or key where the browser requires it), pauses when it scrolls
67
+ away or the tab is hidden, and has an Enlarge button.
68
+
69
+ ### Changed
70
+ - **The human report has a `Timeout:` row**, and the score line lists timeouts
71
+ apart from errored mutants. Before, the `Errored:` row and the score line
72
+ added the two together, while the JSON report kept them apart. The HTML
73
+ summary now shows a `timeout` count apart from `errored` too.
74
+ - **A test file given as a source after `--test` exits 2.** `--test` takes one
75
+ file, so in `mutineer run app/x.rb --test spec/a_spec.rb spec/b_spec.rb` the
76
+ second spec became a source to mutate, and the run tested with one spec file
77
+ without a word. Now a source named `*_test.rb` or `*_spec.rb` inside a
78
+ `test/` or `spec/` directory, given with `--test`, is a usage error that says
79
+ to repeat `--test`. The usage line now reads `--test <test> [--test <test>...]`.
80
+
81
+ ### Fixed
82
+ - **The RSpec runner keeps what gems configured before the run.** It called
83
+ `RSpec.reset`, which drops the whole configuration. Under `rails: true`, a
84
+ `--boot` file or `require:`, the app's gems load first, so a setting such as
85
+ rspec-retry's `verbose_retry` was gone, and a support file that set it
86
+ raised `NoMethodError`. The run stopped with "the unmutated suite is not
87
+ green". The runner now calls `RSpec.clear_examples`, which keeps the
88
+ configuration. Hooks that gems added now apply too: an example with
89
+ rspec-retry's `retry: 3` runs up to 3 times per mutant, as under `rspec`.
90
+ - **`--since` no longer scores a file that Git ignores** (#168). A file
91
+ outside the index is scored in full, so a new file is not missed. That
92
+ check also caught files that Git ignores on purpose (`.gitignore`,
93
+ `.git/info/exclude` or the global excludes file), such as generated code
94
+ inside a directory run. Those files now get no mutants under `--since`,
95
+ and a warning names each one. Other untracked files are still scored in
96
+ full, and so is a file when Git cannot tell, such as one beyond a symlink or
97
+ inside a submodule.
98
+ - **The human report escapes control characters from the source** (#163).
99
+ A surviving line that held a terminal control byte (for example ESC) was
100
+ printed as that byte, so it could change what the terminal showed. The
101
+ token, the replacement, the diff lines, method names, file paths and the
102
+ kill matrix's test names now print each control character except tab as
103
+ its Ruby escape (`\e`), and a byte that is not valid UTF-8 (a Latin-1
104
+ source) as `\xNN`. The JSON and HTML
105
+ reports already escaped text and do not change.
106
+ - **One edit on a line is scored once** (#159). Some opt-in operators emitted
107
+ two mutants that give the same source: `literal_mutation` on `0` (the
108
+ "change to 1" and "add 1" rules), `negation_removal` on `!!x` (either `!`
109
+ removed), and `chain_link` or `operand_removal` on a repeated part such as
110
+ `a.b.b.c` or `x || x`. Each copy had its own id, so one surviving edit
111
+ counted twice. Now only the first copy on a line runs. Copies that start
112
+ on different lines (a multi-line chain) both stay, so `--since` never
113
+ loses the edit. Ids are assigned before the copy is dropped, so every kept
114
+ mutant keeps its id, and the dropped copy's id simply stops appearing.
115
+ Default runs do not change. If you use these operators with `--baseline`,
116
+ regenerate the baseline after upgrading: the score can move with no code
117
+ change, and in a full (unscoped) comparison a dropped copy that survived
118
+ is listed as fixed. An ignore entry for one copy's id still ignores only
119
+ that copy; the other copy still runs.
120
+ - **`# mutineer:disable-line` inside a string no longer silences a line**
121
+ (#158). The marker was found with a text search, so a string such as
122
+ `"# mutineer:disable-line"` ignored every mutant on its line and left
123
+ them out of the score. Only a real `#` comment counts now. A heredoc or
124
+ regex that holds the text is ignored too.
125
+ - **`./lib/x.rb` and an absolute path pair with a test** (#104). Auto-pairing
126
+ found the test for `lib/calc.rb` but not for `./lib/calc.rb` or the
127
+ absolute path to the same file. It reported "no test found by convention"
128
+ and exited 2. A file argument inside the project is now made relative to
129
+ the project root first, so equivalent spellings (`./`, `..`, absolute)
130
+ pair with the same test and run once. A symlink keeps its own name, so it
131
+ stays a separate source from its target. Reports show that root-relative
132
+ path. A path outside the project stays as typed.
133
+ - **A mutant on another line of a multi-line statement runs its tests**
134
+ instead of being reported as `no_coverage`. Ruby counts one line of a
135
+ statement only. When the mutant's own line has no count, the runner now uses
136
+ the tests that ran the statement that holds the mutation. A heredoc body
137
+ belongs to its statement. This fallback skips a first or last line that also
138
+ holds other code, such as the `def` line, and the body of `x while c` and
139
+ `x until c`. It also gives no tests to a mutant in code of the statement
140
+ that runs only sometimes: a `when` or `in` condition, a `rescue` class
141
+ list, a parameter default, the rescue side of `x rescue y`, the value of
142
+ `||=` or `&&=`, the operand of `defined?`, the right side of `a ||` or
143
+ `a &&`, a branch of `x if c`, `x unless c`, a ternary or `else`, the
144
+ pattern of `v in p` or `v => p`, an
145
+ argument or block of `x&.m(...)`, and the value of `x&.m += v`. Those
146
+ mutants stay `no_coverage`, as before.
147
+ - **A survivor diff for a multi-line mutant applies with `git apply`** (#106).
148
+ The JSON `diff` header always read `@@ -N +N @@`, even when the mutant
149
+ removed two lines and added one, so `git apply` rejected the patch as
150
+ corrupt. The header now carries each side's line count (`@@ -3,2 +3 @@`).
151
+ A one-line diff keeps its old header. A mutant that empties a line now
152
+ shows that empty line as a `+` line. A side whose last line has no final
153
+ newline gets ``. Lines of a CRLF file now keep
154
+ their `\r`, so the patch applies to that file.
155
+ - **Split test files pair with their source** (#87). A source such as
156
+ `app/foo/bar.rb` now also uses `test/foo/bar_upsert_test.rb` and
157
+ `test/foo/bar_guards_test.rb`, together with `test/foo/bar_test.rb` when
158
+ that file exists. The same names under `test/lib/` pair with a `lib/`
159
+ source. Before, only the exact `bar_test.rb` name was tried, so a suite
160
+ split across `bar_*_test.rb` files was skipped and a gate could pass with
161
+ no mutants run. A spec the old rules already find is unchanged. The
162
+ split name is left for `user_session.rb` when that file exists, so
163
+ `user.rb` does not run `user_session_test.rb`. The same rule holds outside
164
+ `app/` and `lib/`: `src/user.rb` does not take `test/src/user_session_test.rb`
165
+ when `src/user_session.rb` exists. A longer source claims a split file
166
+ only when that source itself searches the directory the file is in.
167
+ A failed capture of `test/foo/bar_upsert_test.rb` can mark `app/foo/bar.rb`
168
+ uncapturable only when no successful capture covered that source and
169
+ `bar_upsert.rb` does not exist. A test outside that directory does not,
170
+ and neither does a source that only shares the test basename.
171
+ `user_session__test.rb` stays with `user.rb` when `user_session.rb`
172
+ exists, because that name is not the exact test or a split test for
173
+ `user_session`. A failed exact spec in the mirrored `spec/` or `spec/lib/` directory
174
+ marks an `app/` or `lib/` source the same way. A failed spec in another
175
+ directory does not. A project rooted at `/` still finds split tests
176
+ inside that root. A stored failure keeps the pairing from the moment it
177
+ was checked. Adding or deleting a longer source rebuilds the coverage
178
+ map, so that failure is not moved onto the shorter source.
179
+ - **A blank `operators` key no longer runs zero mutants and exits 0** (#167).
180
+ `operators:` with no value became an empty list. The run made no mutants
181
+ and the gate passed. A blank operator list is now an error, and so is a
182
+ list of only unknown operator names. An empty `require` or `ignore` list
183
+ is still valid. `--operators` on the command line still replaces the
184
+ file list, including a blank or unknown file list.
185
+ - **The daemon no longer runs the tool's Ruby settings inside the app** (#100).
186
+ Open3 keeps a variable when the spawn hash omits it, so a parent `RUBYOPT`
187
+ ran inside the app. The child now gets the environment Bundler saved before
188
+ it activated, passed with `unsetenv_others`. The app keeps its gem home,
189
+ excluded groups, Bundler config, and chruby path. An `RBENV_VERSION` or
190
+ `ASDF_RUBY_VERSION` already in the environment stays. That pin is often
191
+ the Ruby that started the tool. `.ruby-version` applies only when no pin
192
+ is set. The tool's `RUBYOPT` and `RUBYLIB` do not reach the child, including
193
+ a value Bundler had saved. A gem home, `BUNDLE_PATH`, or `BUNDLE_WITHOUT`
194
+ reaches the child only when Bundler saved the app's value. The same
195
+ rule covers `GEM_PATH` and `BUNDLER_VERSION`. An rbenv version
196
+ bin under `RBENV_ROOT`, or under `~/.rbenv` when that variable is unset,
197
+ leaves `PATH` only when that root has a shim directory. An asdf version
198
+ bin leaves only when an asdf shim directory exists, including
199
+ `ASDF_DATA_DIR/shims`. Without that shim, the bin stays and `bundle` can
200
+ still be found. The app Gemfile, an explicit Ruby pin, and Rails env are
201
+ applied after that.
202
+ - **The Action refuses to run when `output` and `baseline` are the same file** (#160).
203
+ That setup used to work, and a failing run copied its report onto the
204
+ baseline. The next run then treated the failed report as the baseline, so
205
+ the gate could pass. The Action now stops before the run and exits 2.
206
+ The baseline may come from the `baseline` input, from `--baseline` in
207
+ `extra-args`, or from `.mutineer.yml`. A numeric path such as `123` is
208
+ read the same way as the CLI. Write the report to a separate path. Copy
209
+ that file onto the baseline in a later step, and only when this step
210
+ exits 0. You can also refresh the baseline with the CLI, and keep the
211
+ file only when the run exits 0. The next release moves the `v1` tag, so
212
+ workflows on `@v1` get this refusal then.
213
+ - **A mutation no longer leaves a heredoc body behind as Ruby** (#134).
214
+ Replacing a node deletes the heredoc opener and not the body. The body
215
+ then parses as code, raises, and counts as a kill. A mutation that drops
216
+ the opener is skipped. A replacement that still contains the opener stays.
217
+ - **A bare `public`, `private` or `protected` ends `module_function` mode**,
218
+ so later `def`s in the module body are named as instance methods, not
219
+ singleton methods (#144).
220
+ - **Turning on reduced motion mid-visit finishes the landing page count-ups**,
221
+ so no number keeps changing after the visitor asks for less motion (#176).
222
+ - **`rake site:build` can no longer delete a directory such as `lib/` or
223
+ `.git`** (#162). The task removed the destination it was given, and only
224
+ refused the checkout, its parents and `docs/`. It now always builds into
225
+ `_site/` at the checkout root, and `rake "site:build[DEST]"` and
226
+ `SITE_BUILD_DEST` are gone.
227
+ - **Documentation matches the 1.4 behavior.** Config examples use supported keys,
228
+ agent loops check the exit code and a real score, and report totals explain
229
+ partial `--fail-fast` runs. The README lists every config key and its validation
230
+ rules. API README links work outside GitHub, repeated changelog headings have
231
+ unique anchors, and the website checks validate built links and anchors.
232
+ Mutant-id wording and the gem's Minitest/RSpec description are also corrected.
233
+ - **A mutant in a nested method counts once** (#157). A `def` inside
234
+ another method is a subject of its own, but most operators also mutated
235
+ its code on the outer method. The same edit then counted twice in the
236
+ score, with two ids. Every operator now skips a nested `def` when it
237
+ mutates the outer method. A `def` inside `class << obj`, where `obj` is
238
+ not `self`, is not a subject, so the outer method still mutates its code.
239
+ `array_literal`, `chain_link`, `condition_true`, `condition_false`,
240
+ `operand_removal`, `operator_assignment` and `return_nil` skipped that
241
+ code before. Their new mutants there can show as new survivors against a
242
+ baseline.
243
+
244
+ A mutant id includes an ordinal among the mutants with the same operator
245
+ and token in a subject. When the duplicates leave the outer method, the
246
+ ordinals of its own mutants change. An old duplicate id can then name a
247
+ different mutant on the outer method, and an `ignore:` entry or a
248
+ baseline entry with that id applies to that mutant with no warning. If
249
+ your code has nested methods, regenerate the `ignore:` entries and the
250
+ baseline for the outer methods.
251
+
252
+ Under `--strategy redefine` (the `--rails` default), a mutant in a nested
253
+ method can survive or error even when a test would catch it. `redefine`
254
+ loads only the inner method, and a call to the outer method defines the
255
+ original inner method again. Before, the copy of the edit on the outer
256
+ method was killed. Now each edit has one verdict, from the inner method.
257
+
258
+ ## [1.4.0] - 2026-09-30
259
+
260
+ ### Added
261
+ - **Condition-forcing operators** (Tier-2, opt-in via `--operators`):
262
+ `condition_true` and `condition_false` replace an `if`/`elsif`/`unless`,
263
+ ternary, modifier or `case`/`in` guard condition with `(true)` or `(false)`, so
264
+ its branch always runs or never runs. A surviving mutant means no selected
265
+ test detected the forced condition. A literal condition, also in
266
+ parentheses, makes no mutant: forcing it changes nothing or repeats the
267
+ `boolean_literal` flip. A condition that holds a heredoc makes no mutant.
268
+ A condition that assigns a variable keeps its code, and only its value
269
+ is forced (`(m = x; true)`), so later code still sees the variable.
270
+ The never-runs side of an else-less conditional can
271
+ be the same program as the `nil` that `statement_removal` or `return_nil`
272
+ puts in its place; it is still made, so these mutants and their ids do not
273
+ depend on which other operators run.
274
+ - **Operator-assignment operator** (Tier-2, opt-in via `--operators`):
275
+ `operator_assignment` swaps the operator of a compound assignment:
276
+ `+=` <-> `-=`, `*=` <-> `/=`, and `%=` and `**=` -> `*=`. It covers local,
277
+ instance, class and global variables, constants, calls (`a.b += 1`) and
278
+ index calls (`a[i] += 1`). The `arithmetic` operator never sees these
279
+ forms, because Prism does not parse them as calls. The operator does not
280
+ change `||=`, `&&=`, or the bitwise and shift forms (`|=`, `<<=`).
281
+
282
+ ### Fixed
283
+ - **A typed `--rails`, `--verbose` or `--debug` beats `rails: false` and
284
+ `verbose: false` in `.mutineer.yml`** (#103). The flag was dropped, so the
285
+ run went on without Rails boot or verbose output. The config layers now keep
286
+ only the keys the user wrote, and "did the user write this" comes from those
287
+ keys, not from a hand-kept list.
288
+ - **`framework: rspec` in `.mutineer.yml` survives test pairing** (#103).
289
+ Pairing re-detected the framework from the test file names, so an RSpec
290
+ suite in `test/calc_test.rb` ran under Minitest.
291
+ - **A bad number is an error, not a rounded value** (#105). `--jobs 1.9` and
292
+ `--baseline-epsilon abc` no longer become `1` and `0.0`. One option schema
293
+ parses each value once, where it enters, for both the command line and
294
+ `.mutineer.yml`.
295
+ - **`--since` keeps a source file that git does not track yet** (#156). Before,
296
+ such a file counted as unchanged. The run scored no mutants, and a positive
297
+ `--threshold` still exited 0. The file is now new in full, so all its
298
+ mutants run.
299
+
300
+ ### Changed
301
+ - **These now exit 2 with a message that names the option**: `jobs: 1.9` or
302
+ `jobs: true` in `.mutineer.yml` (before: `1`, or a crash); a bad, negative
303
+ or non-finite `--baseline-epsilon` (before: `0.0`); a boolean key in
304
+ `.mutineer.yml` that is not `true` or `false`, such as the string `"yes"`
305
+ (before: `false`).
306
+ - **`--jobs` takes plain digits only.** `0x2`, `+2`, `" 2"` and `1_0` exit 2.
307
+ Before, `Integer()` read them as 2, 2, 2 and 10. `--threshold` and
308
+ `--baseline-epsilon` take a plain decimal, such as `2` or `2.5`, from a
309
+ string. `0x10`, `+2`, `1_0` and `1e2` exit 2. Before, `Float()` read them
310
+ as 16.0, 2.0, 10.0 and 100.0.
311
+ - **A string option that gets `true`, `false` or no value exits 2**, such as
312
+ `only: false` or `baseline:` in `.mutineer.yml`. A blank `since` also exits 2,
313
+ in the file and on the command line, so an unset shell variable does not
314
+ turn scoping off. `since: false` still means no scoping.
315
+ - **Config file errors start with the file and key**, for example
316
+ `.mutineer.yml: threshold must be a number between 0 and 100`, where they
317
+ started with `--threshold`.
318
+ - **The docs site is built in CI** — a Pages workflow runs `rake site:build`
319
+ and deploys the result, so the YARD HTML under `/api/`, `llms-full.txt`,
320
+ `json-schema.html` and `sitemap.xml` are no longer committed. CI checks
321
+ that the site builds, in place of `rake yard:pages:check`. The site keeps
322
+ its URLs (#153).
323
+
9
324
  ## [1.3.0] - 2026-09-29
10
325
 
11
326
  ### Changed
@@ -611,6 +926,8 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
611
926
  - `.mutineer.yml` configuration (CLI > config > default precedence).
612
927
  - Byte-correct source handling for multibyte (UTF-8) sources.
613
928
 
929
+ [1.5.0]: https://github.com/davidteren/mutineer/releases/tag/v1.5.0
930
+ [1.4.0]: https://github.com/davidteren/mutineer/releases/tag/v1.4.0
614
931
  [1.3.0]: https://github.com/davidteren/mutineer/releases/tag/v1.3.0
615
932
  [1.2.0]: https://github.com/davidteren/mutineer/releases/tag/v1.2.0
616
933
  [1.1.0]: https://github.com/davidteren/mutineer/releases/tag/v1.1.0