mutineer 0.11.2 → 0.11.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 457a9cdee5d5291635fb56dd01caf17293bc500443ca2e43e298a81c0c1f73bf
4
- data.tar.gz: ffe7d126be066573d5c9c42969f994e7a9be71ecb8d301643a6deab3de5d7af3
3
+ metadata.gz: 85591c1b61bcdc3c8a1d39b5b657f667bf7de9039d61e05ab3e5691692f555f0
4
+ data.tar.gz: 76acdc054e82d9ea10dee4a63bcbb5f0f86db37dd585323f734991f413f21e61
5
5
  SHA512:
6
- metadata.gz: 63e9df1191cdc62802dcd9dd674eb2954c416b8016017aa3bb9e689f777859f547450416e4a62f23b34f9c9171b063ff12a6639e3cda1ceb9180c48bbea673df
7
- data.tar.gz: 0ace3fd9df09a67b535d55ee25f0e30d60f1dd3f03ca1027600cc216d11d9dd468f320e9b11190d595e204d6310bfe05f61ec80e6eeb33f36c123e0981ed1810
6
+ metadata.gz: b5855d2e0a9d4e226fe0ad2034cc4d08f4b5ac5e2d34df9e92f97b9d344ea5a50c4df01bbdcb0973dc66e8ccde717baab8842935fa07a4cad1238f77b9bebf42
7
+ data.tar.gz: 3a5c57795de651128a3d3597b2f1776fd32ff76aeaea4edf14037a1ec32e13e72e54c38a56ef8c209f3ee1d6afc0ffbb5fd47153967803c841e64d989d80a047
data/CHANGELOG.md CHANGED
@@ -6,6 +6,71 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.4] - 2026-07-29
10
+
11
+ ### Fixed
12
+ - **`--threshold` now gates on the run being complete, not just its score** — a
13
+ score is `killed / (killed + survived)`, so mutants that error, time out or come
14
+ back uncapturable are excluded from the denominator: a broken harness *raised*
15
+ the score instead of lowering it. Ninety errored mutants and ten that ran (nine
16
+ killed) reported 90% and exited 0, so CI could not tell a complete run from a
17
+ mostly-broken one. Past 10% of attempted mutants producing no verdict, a positive
18
+ `--threshold` now exits 1 whatever the score, and the report says which states
19
+ broke, on every `--format`. A single bad mutant never trips it, however small the
20
+ run, so a `--since` PR with a handful of mutants keeps its flake tolerance (#78).
21
+
22
+ ### Added
23
+ - **`no_verdict[]` in the JSON report** (`schema_version` 1.2) — every attempted
24
+ mutant that produced no verdict, with its `status` and, where there is one, the
25
+ `details` explaining the cause. `Result#details` was built and rendered in no
26
+ format at all, so a daemon crash reached the user as nothing but a larger errored
27
+ count. `summary` gains `attempted` and `no_verdict`, the two figures the
28
+ completeness gate is computed from (#78).
29
+
30
+ ## [0.11.3] - 2026-07-29
31
+
32
+ ### Fixed
33
+ - **Nothing to mutate now costs nothing** — `--since` matching no changed line (a
34
+ docs-only PR, which README documents `--since origin/<base>` for) still did the
35
+ expensive part before discovering there was no work: `--daemon` booted the app
36
+ once for the coverage map and again for every `--jobs` worker, and
37
+ `--test-command` ran the whole suite for a smoke check that calibrates a timeout
38
+ no mutant would use. Both backends now return as soon as the job list is empty.
39
+ The in-process path is unchanged: it boots before collecting jobs, so it cannot
40
+ know the list is empty yet. One consequence worth stating: a zero-job `--daemon`
41
+ run no longer boots the app, so an app that fails to boot now exits 0 (nothing
42
+ to do) instead of exit 1 (#76).
43
+ - **A dead daemon ends the run instead of scoring the rest against it** — a
44
+ daemon that dies while running one mutant was already handled: `DaemonClient`
45
+ respawns and answers `error` for that mutant. But a daemon that could not come
46
+ back was not. `restart!` closes the pipes before respawning, so if the respawn
47
+ failed at the OS level (`EMFILE` or `ENOMEM` under `--jobs N`, `ENOENT` when
48
+ `bundle` does not resolve) the client was left permanently dead while raising
49
+ errors that read as ordinary per-mutant failures. Every remaining mutant scored
50
+ `error` against nothing, and Mutineer printed a mutation score built on the
51
+ fraction of mutants that ran before the daemon died. `DaemonClient` now raises
52
+ `DaemonBootError` whenever it is gone for good — a refused spawn, a failed boot
53
+ handshake on respawn, closed pipes, or `MAX_RESTARTS` crashes — and the CLI
54
+ reports it as a message and exit 1 rather than a backtrace. Under `--jobs N`
55
+ the remaining queue is dropped so sibling workers stop too. Errored mutants
56
+ still cannot fail the `--threshold` gate once any mutant is scored (#78).
57
+
58
+ ### Changed
59
+ - **Daemon orchestration split out of `Runner`** — the persistent-daemon backend
60
+ (job fan-out, worker DBs, coverage-map build, boot config, verdict mapping) now
61
+ lives in `Mutineer::DaemonBackend`. `Runner` keeps job collection, coverage
62
+ selection and the single in-process mutant run, and drops from 662 to 439 lines.
63
+ The external backend's orchestration stays on `Runner` for now, so the two
64
+ backends are not yet symmetrical. No CLI or behaviour change (#58).
65
+ Internal-only removals: `Runner.execute_daemon`, `Runner.daemon_coverage_map`
66
+ and `Runner::DAEMON_TIMEOUT` no longer exist. They were never documented —
67
+ the supported programmatic contract is the JSON report, the stable mutant ids
68
+ and the exit codes — so only code calling `Runner` internals directly is
69
+ affected.
70
+ - **Comment diet on orchestration files** — present-tense YARD contracts; drop
71
+ ticket/phase/KTD history tags from runner, CLI, daemon pair, coverage map,
72
+ reporter, result, and related modules. Safety and score invariants kept (#57).
73
+
9
74
  ## [0.11.2] - 2026-07-24
10
75
 
11
76
  ### Fixed
@@ -253,6 +318,8 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
253
318
  - `.mutineer.yml` configuration (CLI > config > default precedence).
254
319
  - Byte-correct source handling for multibyte (UTF-8) sources.
255
320
 
321
+ [0.11.4]: https://github.com/davidteren/mutineer/releases/tag/v0.11.4
322
+ [0.11.3]: https://github.com/davidteren/mutineer/releases/tag/v0.11.3
256
323
  [0.11.2]: https://github.com/davidteren/mutineer/releases/tag/v0.11.2
257
324
  [0.11.1]: https://github.com/davidteren/mutineer/releases/tag/v0.11.1
258
325
  [0.11.0]: https://github.com/davidteren/mutineer/releases/tag/v0.11.0
data/README.md CHANGED
@@ -47,7 +47,7 @@ mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
47
47
  |------|---------|
48
48
  | `--test FILE` | Test file covering the sources (repeatable) |
49
49
  | `--operators LIST` | Comma-separated operator names (default: the Tier-1 set) |
50
- | `--threshold FLOAT` | Exit 1 when the score is below FLOAT (default: 0 = off) |
50
+ | `--threshold FLOAT` | Exit 1 when the score is below FLOAT, or when nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted (default: 0 = off) |
51
51
  | `--only NAME` | Restrict to one fully-qualified subject, e.g. `Calculator#add` |
52
52
  | `--framework NAME` | `minitest` (default) or `rspec`; auto-detected as rspec when most `--test` files end in `_spec.rb` |
53
53
  | `--since REF` | Only mutate lines changed since git `REF` (e.g. `origin/main`) — ideal for PR CI |
@@ -70,7 +70,7 @@ mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
70
70
  | Code | Meaning |
71
71
  |------|---------|
72
72
  | `0` | Score ≥ threshold (or no threshold set) |
73
- | `1` | Survivors below threshold, or a runtime error |
73
+ | `1` | Score below threshold, nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted, a `--baseline` regression, or a runtime error |
74
74
  | `2` | Usage / invalid-flag error |
75
75
 
76
76
  ### Operators
@@ -4,29 +4,27 @@ require "json"
4
4
  require_relative "config" # for Mutineer::ConfigError
5
5
 
6
6
  module Mutineer
7
- # #13: CI baseline/delta gating. A baseline is literally a prior
8
- # `mutineer run --format json` document (KTD-1) — no bespoke format to
9
- # version.
10
- # We diff the current run against it by the #10 stable survivor id (KTD-2):
11
- # a NEW survivor (id present now, absent in the baseline) OR a score drop is
12
- # a regression the CLI turns into exit 1. Pure data stdlib `json` only, no
13
- # fork, no Rails — so it's testable in isolation from a canned JSON + a hand-
14
- # built AggregateResult.
7
+ # CI baseline/delta gating. A baseline is a prior
8
+ # `mutineer run --format json` document (no bespoke format to version).
9
+ # Diff the current run against it by the stable survivor id: a NEW survivor
10
+ # (id present now, absent in the baseline) OR a score drop is a regression the
11
+ # CLI turns into exit 1. Pure data, stdlib `json` only, no fork, no Rails, so
12
+ # it is testable in isolation from a canned JSON + a hand-built AggregateResult.
15
13
  class Baseline
16
14
  # The verdict of diffing a current run against the baseline.
17
- # new_survivors current Result objects whose stable id is absent from
15
+ # new_survivors - current Result objects whose stable id is absent from
18
16
  # the baseline (the regressions to name).
19
- # fixed_survivors baseline survivor hashes absent from the current run
17
+ # fixed_survivors - baseline survivor hashes absent from the current run
20
18
  # (informational, never gates).
21
- # score_drop current score < baseline score - epsilon. nil on
19
+ # score_drop - current score < baseline score - epsilon. nil on
22
20
  # either side skips the check (see #diff).
23
- # regressed any new survivors OR a score drop.
21
+ # regressed - any new survivors OR a score drop.
24
22
  Delta = Data.define(:new_survivors, :fixed_survivors,
25
23
  :score_before, :score_after, :score_drop, :regressed)
26
24
 
27
- # Load a prior --format json run. Raises ConfigError (NOT exit — R8: a data
28
- # class must never kill the host) on a missing/unreadable file, unparseable
29
- # JSON, or a doc that isn't a baseline shape, so the CLI maps it to exit 2
25
+ # Load a prior --format json run. Raises ConfigError (NOT exit: a data class
26
+ # must never kill the host) on a missing/unreadable file, unparseable JSON,
27
+ # or a doc that is not a baseline shape, so the CLI maps it to exit 2
30
28
  # (usage) like every other bad-path flag.
31
29
  #
32
30
  # @param path [String] baseline JSON file path.
@@ -72,7 +70,7 @@ module Mutineer
72
70
 
73
71
  current_score = aggregate.mutation_score
74
72
  # nil-score discipline (mirrors Reporter#exit_code): a score absent on
75
- # either side can't be compared skip the drop check, keep the new-
73
+ # either side cannot be compared. Skip the drop check, keep the new-
76
74
  # survivor check.
77
75
  score_drop = !@score.nil? && !current_score.nil? &&
78
76
  current_score < @score - epsilon
data/lib/mutineer/cli.rb CHANGED
@@ -18,7 +18,7 @@ module Mutineer
18
18
  # Command-line entry point. `start` is the single public method called by
19
19
  # bin/mutineer; it parses argv, acts, and exits with a pinned code.
20
20
  #
21
- # Exit codes (taxonomy consistent across M1–M5):
21
+ # Exit codes:
22
22
  # 0 success / requested output (--version, --help, score >= threshold)
23
23
  # 1 survivors below threshold, or a runtime error
24
24
  # 2 usage / flag error (unknown subcommand, invalid flag, unknown operator,
@@ -78,7 +78,7 @@ module Mutineer
78
78
  # @return [void]
79
79
  def self.start(argv)
80
80
  opts = {} # symbol => value, the CLI-provided Config fields
81
- explicit = Set.new # precedence keys the user typed (KTD3)
81
+ explicit = Set.new # precedence keys the user typed
82
82
  show_operators = false
83
83
 
84
84
  parser = OptionParser.new do |o|
@@ -116,15 +116,15 @@ module Mutineer
116
116
  o.on("--debug") { opts[:verbose] = true } # alias of --verbose
117
117
  o.on("--format FORMAT") { |v| opts[:format] = v }
118
118
  o.on("--output FILE") { |v| opts[:output] = v }
119
- # #13: --baseline is also a .mutineer.yml key, so mark it explicit when
120
- # typed (CLI wins over the file). --baseline-epsilon is CLI-only.
119
+ # --baseline is also a .mutineer.yml key, so mark it explicit when typed
120
+ # (CLI wins over the file). --baseline-epsilon is CLI-only.
121
121
  o.on("--baseline FILE") { |v| opts[:baseline] = v; explicit << :baseline }
122
122
  o.on("--baseline-epsilon FLOAT") { |v| opts[:baseline_epsilon] = v.to_f }
123
- # #27: run the target suite as a subprocess in the app's OWN runtime so
123
+ # Run the target suite as a subprocess in the app's OWN runtime so
124
124
  # mutineer (Ruby >= 3.4) can mutation-test apps pinned to an older Ruby.
125
125
  o.on("--test-command CMD") { |v| opts[:test_command] = v; explicit << :test_command }
126
- # #26/#27 Phase 2: boot the app ONCE in a persistent daemon and fork per
127
- # mutant, with per-worker DB isolation so --jobs N is safe under Rails.
126
+ # Boot the app ONCE in a persistent daemon and fork per mutant, with
127
+ # per-worker DB isolation so --jobs N is safe under Rails.
128
128
  o.on("--daemon") { opts[:daemon] = true; explicit << :daemon }
129
129
  end
130
130
 
@@ -150,7 +150,7 @@ module Mutineer
150
150
  file_hash = file_path ? Config.from_file(file_path) : {}
151
151
  config = Config.resolve(opts, file_hash, explicit)
152
152
  rescue Mutineer::ConfigError => e
153
- # R8: the lib layer raises instead of killing the host; the CLI maps a
153
+ # The lib layer raises instead of killing the host; the CLI maps a
154
154
  # config (usage) error to exit 2.
155
155
  warn "mutineer: #{e.message}"
156
156
  exit 2
@@ -158,7 +158,7 @@ module Mutineer
158
158
 
159
159
  case argv.first
160
160
  when "run"
161
- # #11: a directory source expands to its **/*.rb files; literal files pass
161
+ # A directory source expands to its **/*.rb files; literal files pass
162
162
  # through. Test inference (when --test is omitted) happens in validate!.
163
163
  config.sources = Pairing.expand_sources(argv[1..], project_root: config.project_root)
164
164
  run(config, explicit)
@@ -197,7 +197,7 @@ module Mutineer
197
197
  warn "mutineer: #{e.message}"
198
198
  exit 2
199
199
  rescue SystemCallError => e
200
- # R5: a missing/unreadable path reaches here as Errno::ENOENT etc. a plain
200
+ # A missing/unreadable path reaches here as Errno::ENOENT etc. A plain
201
201
  # message and usage exit, never a raw backtrace.
202
202
  warn "mutineer: #{e.message}"
203
203
  exit 2
@@ -210,15 +210,19 @@ module Mutineer
210
210
  warn "mutineer: error reading: #{e.message}"
211
211
  exit 1
212
212
  rescue Mutineer::SmokeCheckError => e
213
- # #27: the unmutated suite isn't green under --test-command a broken
213
+ # The unmutated suite is not green under --test-command: a broken
214
214
  # environment, not weak tests. Runtime error (exit 1), not usage (exit 2).
215
215
  warn "mutineer: #{e.message}"
216
216
  exit 1
217
+ rescue Mutineer::DaemonBootError => e
218
+ # The daemon is gone for good, so the run ended rather than scoring the rest
219
+ # against it. A deliberate stop deserves a message, not a raw backtrace.
220
+ warn "mutineer: #{e.message}"
221
+ exit 1
217
222
  end
218
223
 
219
- # Flag validation: every flag/usage failure exits 2 (C7), consistent with the
220
- # taxonomy above CI can tell "mistyped flag" from "tests too weak."
221
- # Validates the run configuration.
224
+ # Flag validation: every flag/usage failure exits 2, consistent with the
225
+ # taxonomy above. CI can tell "mistyped flag" from "tests too weak."
222
226
  #
223
227
  # @api private
224
228
  # @param config [Mutineer::Config] run configuration.
@@ -279,13 +283,12 @@ module Mutineer
279
283
  validate_paths!(config)
280
284
  end
281
285
 
282
- # #27: --test-command runs the target suite in the app's own runtime. Validate
283
- # its shape up front (usage errors → exit 2) and force serial execution: each
286
+ # --test-command runs the target suite in the app's own runtime. Validate its
287
+ # shape up front (usage errors → exit 2) and force serial execution: each
284
288
  # subprocess boots the app and opens its own fixture transaction against the
285
- # same DB, so --jobs > 1 would corrupt results (the #12 fixture-contention
286
- # hazard). Unlike --rails, this path has NO per-worker DB isolation to opt into,
287
- # so an explicit --jobs N is forced to 1 rather than honored (KTD-5).
288
- # Validates the --test-command configuration.
289
+ # same DB, so --jobs > 1 would corrupt results (fixture-contention hazard).
290
+ # Unlike --rails, this path has NO per-worker DB isolation to opt into, so an
291
+ # explicit --jobs N is forced to 1 rather than honored.
289
292
  #
290
293
  # @api private
291
294
  # @param config [Mutineer::Config] run configuration.
@@ -347,7 +350,6 @@ module Mutineer
347
350
 
348
351
  # --since needs a real git repo and a resolvable ref; either failure is a
349
352
  # usage error (exit 2) so CI sees "bad invocation," not "tests too weak."
350
- # Validates the --since ref.
351
353
  #
352
354
  # @api private
353
355
  # @param config [Mutineer::Config] run configuration.
@@ -370,10 +372,9 @@ module Mutineer
370
372
  exit 2
371
373
  end
372
374
 
373
- # R5: validate path existence up front so a typo is a clean usage error (exit
374
- # 2), not an Errno::ENOENT backtrace from deep in the run. Flag checks run
375
- # first so a bad flag still reports the flag, not the missing file.
376
- # Validates source and test paths.
375
+ # Validate path existence up front so a typo is a clean usage error (exit 2),
376
+ # not an Errno::ENOENT backtrace from deep in the run. Flag checks run first
377
+ # so a bad flag still reports the flag, not the missing file.
377
378
  #
378
379
  # @api private
379
380
  # @param config [Mutineer::Config] run configuration.
@@ -387,14 +388,13 @@ module Mutineer
387
388
  exit 2
388
389
  end
389
390
 
390
- # #11: auto-pair sources to tests by path convention when no --test was given
391
- # (explicit --test wins — R5). Each source with an inferred test on disk joins
392
- # the run; a source with none is dropped with a one-line stderr warning (R3)
393
- # and the run continues with the rest. If every source is dropped: in boot mode
394
- # the dedicated --boot/--rails-requires-test check reports it; otherwise exit 2
391
+ # Auto-pair sources to tests by path convention when no --test was given
392
+ # (explicit --test wins). Each source with an inferred test on disk joins the
393
+ # run; a source with none is dropped with a one-line stderr warning and the
394
+ # run continues with the rest. If every source is dropped: in boot mode the
395
+ # dedicated --boot/--rails-requires-test check reports it; otherwise exit 2
395
396
  # with a usage message. The framework is re-detected from the inferred set
396
397
  # unless it was set explicitly (a spec-only project loads/reports as rspec).
397
- # Auto-pairs sources and tests when --test is absent.
398
398
  #
399
399
  # @api private
400
400
  # @param config [Mutineer::Config] run configuration.
@@ -421,11 +421,10 @@ module Mutineer
421
421
  exit 2
422
422
  end
423
423
 
424
- # #13: a missing/unreadable/unparseable baseline is a usage error (exit 2),
424
+ # A missing/unreadable/unparseable baseline is a usage error (exit 2),
425
425
  # mirroring --output/--since preflight, so CI sees "bad invocation," not a
426
426
  # backtrace mid-run. Validating up front = attempting the load (it raises
427
427
  # ConfigError/SystemCallError; the actual diff reloads in execute).
428
- # Preflights a baseline file.
429
428
  #
430
429
  # @api private
431
430
  # @param path [String] baseline file path.
@@ -464,7 +463,7 @@ module Mutineer
464
463
  aggregate, source_map = Runner.execute(config)
465
464
  reporter = Reporter.new(aggregate, source_map)
466
465
 
467
- # #13: diff the current run against the baseline (preflighted above) by the
466
+ # Diff the current run against the baseline (preflighted above) by the
468
467
  # stable survivor id. The delta is rendered inline (human section / additive
469
468
  # json block) and gates exit independently of --threshold.
470
469
  delta = (Baseline.load(config.baseline).diff(aggregate, epsilon: config.baseline_epsilon) if config.baseline)
@@ -476,28 +475,27 @@ module Mutineer
476
475
  # is not comparable to an in-process run: no coverage narrowing (uncovered
477
476
  # mutants count as survivors), and an infra failure is scored as a kill
478
477
  # (upper bound). Daemon coverage fallback warnings are emitted from the runner
479
- # only when the map is unavailable not on every --daemon run.
478
+ # only when the map is unavailable, not on every --daemon run.
480
479
  if config.test_command
481
480
  warn "[mutineer] --test-command score is an upper bound, not comparable to an " \
482
481
  "in-process run: no coverage narrowing (uncovered mutants count as survivors) " \
483
482
  "and an infra failure is scored as a kill."
484
483
  end
485
484
 
486
- # Nudge toward the opt-in tier-2 operators (human report only never
485
+ # Nudge toward the opt-in tier-2 operators (human report only: never
487
486
  # pollute JSON output).
488
487
  if !%w[json html].include?(config.format) && (hint = tier2_hint(config.operators))
489
488
  puts hint
490
489
  end
491
490
 
492
- # #13/KTD-4: --baseline and --threshold are independent gates OR'd together.
491
+ # --baseline and --threshold are independent gates OR'd together.
493
492
  # `max` of two 0/1 codes is the OR; usage (2) is handled earlier and wins.
494
493
  baseline_exit = delta&.regressed ? 1 : 0
495
494
  exit [reporter.exit_code(threshold: config.threshold), baseline_exit].max
496
495
  end
497
496
 
498
497
  # The tier-2 operators not in the active set, as a one-line hint (or nil when
499
- # they're all already enabled). `active` nil means the default (Tier-1) set.
500
- # Builds a Tier-2 operator hint.
498
+ # they are all already enabled). `active` nil means the default (Tier-1) set.
501
499
  #
502
500
  # @param active [Array<String>, nil] active operator names.
503
501
  # @return [String, nil] hint text or nil.
@@ -4,20 +4,20 @@ require "etc"
4
4
  require "yaml"
5
5
 
6
6
  module Mutineer
7
- # Raised by the config layer instead of calling exit/abort a data class must
8
- # never kill the host process (R8). The CLI rescues this and maps it to exit 2.
7
+ # Raised by the config layer instead of calling exit/abort. A data class must
8
+ # never kill the host process. The CLI rescues this and maps it to exit 2.
9
9
  class ConfigError < StandardError; end
10
10
 
11
11
  # Plain run configuration, populated by the CLI (or directly by the
12
12
  # integration test). `operators` nil means "all default operators";
13
13
  # `threshold` 0.0 means the CI gate is off (spec §10).
14
14
  #
15
- # M5 adds: jobs (parallel workers), format (human|json), output (report file),
16
- # strategy (reload|redefine), require_paths (extra files to load). Config loading and
17
- # the CLI > file > default precedence merge live here (KTD3/KTD4).
15
+ # Also holds: jobs (parallel workers), format (human|json), output (report
16
+ # file), strategy (reload|redefine), require_paths (extra files to load).
17
+ # Config loading and the CLI > file > default precedence merge live here.
18
18
  #
19
- # Boot mode adds: boot (a file to require ONCE in the parent so the app env
20
- # e.g. Rails is booted before forking; sources are then NOT manually required)
19
+ # Boot mode adds: boot (a file to require ONCE in the parent so the app env,
20
+ # e.g. Rails, is booted before forking; sources are then NOT manually required)
21
21
  # and rails (sugar: defaults boot to config/environment and strategy to redefine,
22
22
  # and reconnects ActiveRecord per fork).
23
23
  Config = Struct.new(
@@ -25,7 +25,7 @@ module Mutineer
25
25
  :cache_dir, :project_root, :load_paths,
26
26
  :jobs, :format, :output, :strategy, :require_paths,
27
27
  :boot, :rails, :since, :framework, :verbose, :ignore,
28
- # :daemon is user-facing as of U8 (--daemon flag + KNOWN_KEYS + boolean coerce).
28
+ # :daemon is user-facing (--daemon flag + KNOWN_KEYS + boolean coerce).
29
29
  # :daemon_timeout stays programmatic (set by tests/Runner; no flag yet).
30
30
  :baseline, :baseline_epsilon, :fail_fast, :test_command,
31
31
  :daemon, :daemon_timeout,
@@ -33,7 +33,7 @@ module Mutineer
33
33
  ) do
34
34
  # Config file name.
35
35
  CONFIG_FILE = ".mutineer.yml"
36
- # Keys accepted in .mutineer.yml (R7). `require` maps to the :require_paths field.
36
+ # Keys accepted in .mutineer.yml. `require` maps to the :require_paths field.
37
37
  KNOWN_KEYS = %w[operators jobs threshold only require boot rails since framework verbose ignore baseline fail_fast test_command daemon].freeze
38
38
 
39
39
  def initialize(**kwargs)
@@ -59,8 +59,8 @@ module Mutineer
59
59
 
60
60
  # Walk from `start` toward `home`, returning the first .mutineer.yml path found
61
61
  # or nil. Checks `home` itself, then stops; if `start` is above `home`
62
- # (e.g. /tmp), the walk continues to the filesystem root (KTD4). Pure
63
- # discovery — reads no file content.
62
+ # (e.g. /tmp), the walk continues to the filesystem root. Pure discovery;
63
+ # reads no file content.
64
64
  def self.find_file(start = Dir.pwd, home = File.expand_path("~"))
65
65
  dir = File.expand_path(start)
66
66
  loop do
@@ -77,9 +77,9 @@ module Mutineer
77
77
  end
78
78
 
79
79
  # Parse a .mutineer.yml into a symbol-keyed hash of recognized keys. Unknown
80
- # keys / unknown operator names emit a one-line stderr warning and are ignored
81
- # (R7). A YAML syntax error raises ConfigError (R7a/R8) — never a silent
82
- # fallback to defaults, and never an exit from the lib layer.
80
+ # keys / unknown operator names emit a one-line stderr warning and are ignored.
81
+ # A YAML syntax error raises ConfigError: never a silent fallback to defaults,
82
+ # and never an exit from the lib layer.
83
83
  def self.from_file(path)
84
84
  raw = YAML.safe_load(File.read(path)) || {}
85
85
  name = File.basename(path)
@@ -103,7 +103,7 @@ module Mutineer
103
103
  raise ConfigError, "#{File.basename(path)} parse error: #{e.message}"
104
104
  end
105
105
 
106
- # Apply precedence (KTD3): start from the CLI-provided values, then fill in a
106
+ # Apply precedence: start from the CLI-provided values, then fill in a
107
107
  # config-file value only for keys the user did NOT type on the command line.
108
108
  # `explicit` is a Set of field symbols the CLI saw with a value; a Set (not
109
109
  # nil-sentinels) is used because some valid values are zero/false.
@@ -114,7 +114,7 @@ module Mutineer
114
114
 
115
115
  # --rails sugar: boot config/environment. Prefer redefine only for the
116
116
  # in-process path (daemon is whole-file reload only). In-process --rails
117
- # shares one test database force serial unless --daemon.
117
+ # shares one test database, so force serial unless --daemon.
118
118
  if config.rails
119
119
  config.boot ||= "config/environment"
120
120
  unless config.daemon || explicit.include?(:strategy)
@@ -138,7 +138,6 @@ module Mutineer
138
138
 
139
139
  # Pick rspec when a MAJORITY of the given test files end with _spec.rb;
140
140
  # otherwise minitest. Empty/ambiguous -> minitest (the safe default).
141
- # Detects the test framework from the file list.
142
141
  #
143
142
  # @param tests [Array<String>] test file paths.
144
143
  # @return [String] `"rspec"` or `"minitest"`.
@@ -187,10 +186,9 @@ module Mutineer
187
186
  end
188
187
  end
189
188
 
190
- # Drop (with a warning) operator names the registry doesn't know (R7).
189
+ # Drop (with a warning) operator names the registry does not know.
191
190
  # Referenced lazily so config.rb carries no load-order dependency on the
192
191
  # registry; by the time a config is parsed at runtime, it is loaded.
193
- # Filters out unknown operator names.
194
192
  #
195
193
  # @api private
196
194
  # @param names [Array<String>] operator names.