evilution 0.27.0 → 0.29.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +65 -0
  3. data/.rubocop_todo.yml +0 -1
  4. data/CHANGELOG.md +39 -0
  5. data/README.md +19 -0
  6. data/lib/evilution/ast/constant_names.rb +28 -11
  7. data/lib/evilution/ast/pattern/parser.rb +29 -17
  8. data/lib/evilution/baseline.rb +5 -4
  9. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  10. data/lib/evilution/cli/commands/subjects.rb +6 -3
  11. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  12. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  13. data/lib/evilution/cli/parser/file_args.rb +3 -1
  14. data/lib/evilution/cli/parser/options_builder.rb +36 -1
  15. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  16. data/lib/evilution/cli/parser.rb +18 -20
  17. data/lib/evilution/cli/printers/environment.rb +19 -19
  18. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  19. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  20. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  21. data/lib/evilution/compare/diff_extractor.rb +6 -0
  22. data/lib/evilution/compare/fingerprint.rb +15 -72
  23. data/lib/evilution/compare/line_normalizer.rb +72 -0
  24. data/lib/evilution/compare/normalizer.rb +27 -9
  25. data/lib/evilution/config/validators/profile.rb +11 -0
  26. data/lib/evilution/config.rb +49 -32
  27. data/lib/evilution/disable_comment.rb +21 -12
  28. data/lib/evilution/integration/crash_detector.rb +2 -2
  29. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  30. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  31. data/lib/evilution/integration/minitest.rb +25 -16
  32. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  33. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
  34. data/lib/evilution/integration/rspec.rb +4 -0
  35. data/lib/evilution/isolation/fork.rb +43 -28
  36. data/lib/evilution/isolation/in_process.rb +10 -6
  37. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  38. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  39. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  40. data/lib/evilution/mcp/info_tool.rb +7 -3
  41. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  42. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  43. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  44. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  45. data/lib/evilution/mcp/session_tool.rb +27 -20
  46. data/lib/evilution/mutation.rb +60 -42
  47. data/lib/evilution/mutator/base.rb +23 -21
  48. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  49. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  50. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  51. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  52. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  53. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  54. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  55. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  56. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  57. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  58. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  59. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  60. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  61. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  62. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  63. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  64. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  65. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  66. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  67. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  68. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  69. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  70. data/lib/evilution/mutator/registry.rb +20 -0
  71. data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  73. data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
  74. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  75. data/lib/evilution/parallel/work_queue.rb +35 -18
  76. data/lib/evilution/process_cleanup.rb +19 -0
  77. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  78. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  79. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  80. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  81. data/lib/evilution/reporter/html/escape.rb +1 -1
  82. data/lib/evilution/reporter/html/section.rb +1 -1
  83. data/lib/evilution/reporter/html/sections.rb +4 -2
  84. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  85. data/lib/evilution/reporter/html.rb +8 -3
  86. data/lib/evilution/reporter/json.rb +52 -18
  87. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  88. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  89. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  90. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  91. data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
  92. data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
  93. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  94. data/lib/evilution/result/error_info.rb +20 -0
  95. data/lib/evilution/result/memory_stats.rb +20 -0
  96. data/lib/evilution/result/mutation_result.rb +30 -14
  97. data/lib/evilution/runner/baseline_runner.rb +16 -10
  98. data/lib/evilution/runner/diagnostics.rb +14 -11
  99. data/lib/evilution/runner/isolation_resolver.rb +12 -11
  100. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
  101. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
  102. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
  103. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
  104. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  105. data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
  106. data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
  107. data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
  108. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
  109. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
  110. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  111. data/lib/evilution/runner/mutation_executor.rb +14 -20
  112. data/lib/evilution/runner/mutation_planner.rb +38 -19
  113. data/lib/evilution/runner/report_publisher.rb +1 -2
  114. data/lib/evilution/runner/subject_pipeline.rb +22 -13
  115. data/lib/evilution/runner.rb +36 -34
  116. data/lib/evilution/session/diff.rb +15 -6
  117. data/lib/evilution/spec_ast_cache.rb +26 -12
  118. data/lib/evilution/version.rb +1 -1
  119. data/lib/evilution.rb +1 -0
  120. data/script/memory_check +14 -6
  121. data/scripts/benchmark_density +10 -9
  122. data/scripts/compare_mutations +38 -21
  123. data/scripts/mutant_json_adapter +7 -4
  124. metadata +15 -3
  125. data/lib/evilution/reporter/html/namespace.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12d485d3cce9569229a95a1e8f29403fbb11f8f067e0c438256919521f6d82dc
4
- data.tar.gz: 11a1adeca7c61fa905757137d6a737c6e389eae584b37fca91a8c9eed57926d0
3
+ metadata.gz: 607074e45fead28ec35facc8fadadbb44e0dc6429ee00ef37edf92145bc4b7fc
4
+ data.tar.gz: 3bc840215dff4272ea6da9aad5ebed982827507f765c6cadc444bd1f3fb63ddc
5
5
  SHA512:
6
- metadata.gz: ce9208fa3f3ed3160d2844cdd5e1479cf7b2fa90985371cb95d6978177f371d38ec4bdccf473f79af318cd6c617fda2b1962efe4f389eab1912ab348cc48b475
7
- data.tar.gz: 57ff4d971829430d5d47525ac0bc5e488b326e6916657aaf9acb3faf0e8795e909523ec8238b6b9fbaace13bdd60c0f437f30d94efbfa82861b42da802a54aac
6
+ metadata.gz: 5febff050f2670b2ab5f1cbeac9bfc41b0f30edc3e5ee39591036da0ef9ccf5d1273c0abe0cc828d289a959c2e135adb348980882a931d263df65e4b8a4e2299
7
+ data.tar.gz: 1f0d38cab915255ce1742350255cce291fd9c6e3bbdc649af778750089ca9b219c7f2553d1de40c6b2ac72b128310c56d7fff3ecc99ee2c14db42680dd0f3b2b
@@ -245,3 +245,68 @@
245
245
  {"id":"int-9d182026","kind":"field_change","created_at":"2026-04-25T17:54:26.23542935Z","actor":"Denis Kiselev","issue_id":"EV-67yh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #897"}}
246
246
  {"id":"int-7ede49f5","kind":"field_change","created_at":"2026-04-25T18:16:18.358350457Z","actor":"Denis Kiselev","issue_id":"EV-zvhp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #898"}}
247
247
  {"id":"int-c28f7ee0","kind":"field_change","created_at":"2026-04-26T02:28:38.321564801Z","actor":"Denis Kiselev","issue_id":"EV-vev8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #899"}}
248
+ {"id":"int-111f631a","kind":"field_change","created_at":"2026-04-26T07:57:40.59622849Z","actor":"Denis Kiselev","issue_id":"EV-p5vh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Released in v0.27.0 (PR #901 merged, release PR #902 shipped)"}}
249
+ {"id":"int-5a159da9","kind":"field_change","created_at":"2026-04-29T11:51:40.86913186Z","actor":"Denis Kiselev","issue_id":"EV-vm61","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
250
+ {"id":"int-1787900f","kind":"field_change","created_at":"2026-04-30T02:40:52.288627178Z","actor":"Denis Kiselev","issue_id":"EV-v1i6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #911"}}
251
+ {"id":"int-08958dc0","kind":"field_change","created_at":"2026-04-30T03:15:14.369272687Z","actor":"Denis Kiselev","issue_id":"EV-x3s0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #912"}}
252
+ {"id":"int-e013b002","kind":"field_change","created_at":"2026-04-30T05:46:44.70653253Z","actor":"Denis Kiselev","issue_id":"EV-eww3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #913"}}
253
+ {"id":"int-7b15a1b0","kind":"field_change","created_at":"2026-04-30T09:30:21.057039785Z","actor":"Denis Kiselev","issue_id":"EV-7rov","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #914"}}
254
+ {"id":"int-71c9f98d","kind":"field_change","created_at":"2026-04-30T12:30:36.528587334Z","actor":"Denis Kiselev","issue_id":"EV-318q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
255
+ {"id":"int-42eadbdc","kind":"field_change","created_at":"2026-04-30T16:09:00.539400752Z","actor":"Denis Kiselev","issue_id":"EV-voay","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
256
+ {"id":"int-cb4c0ebf","kind":"field_change","created_at":"2026-05-01T02:38:05.091612839Z","actor":"Denis Kiselev","issue_id":"EV-t918","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #918"}}
257
+ {"id":"int-ffa8f0b2","kind":"field_change","created_at":"2026-05-01T17:47:23.967998678Z","actor":"Denis Kiselev","issue_id":"EV-m3ta","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
258
+ {"id":"int-b9cb7738","kind":"field_change","created_at":"2026-05-01T18:06:39.17748775Z","actor":"Denis Kiselev","issue_id":"EV-gffv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
259
+ {"id":"int-983c2fe6","kind":"field_change","created_at":"2026-05-02T02:47:27.113692488Z","actor":"Denis Kiselev","issue_id":"EV-3t8l","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Audit confirmed feature already fully implemented in current master: --fail-fast CLI flag, .evilution.yml key, FailFast validator, ResultNotifier trips at threshold, sequential+parallel strategies short-circuit, summary.truncated? indicator, reporter notices (CLI/HTML/JSON), full spec coverage across notifier/sequential/parallel/runner/parser. CI signal available via summary.truncated? in reports."}}
260
+ {"id":"int-1790a8b7","kind":"field_change","created_at":"2026-05-02T17:53:56.73309749Z","actor":"Denis Kiselev","issue_id":"EV-2gpj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Audit confirmed refactor already done: ProcessCleanup.safe_kill / safe_wait helpers extracted to lib/evilution/process_cleanup.rb (lines 8-18), used by baseline.rb (lines 85,93,94) and parallel/work_queue/worker.rb. No Style/RescueModifier disables remain anywhere in lib/. bundle exec rubocop lib/evilution/baseline.rb clean. Existing specs pass."}}
261
+ {"id":"int-a4ad60a6","kind":"field_change","created_at":"2026-05-06T03:30:34.744627494Z","actor":"Denis Kiselev","issue_id":"EV-s24s","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
262
+ {"id":"int-076a3c83","kind":"field_change","created_at":"2026-05-06T03:30:41.864846348Z","actor":"Denis Kiselev","issue_id":"EV-s24s","extra":{"field":"status","new_value":"in_progress","old_value":"closed"}}
263
+ {"id":"int-01566863","kind":"field_change","created_at":"2026-05-06T07:19:50.242235949Z","actor":"Denis Kiselev","issue_id":"EV-y27w","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
264
+ {"id":"int-2ead0736","kind":"field_change","created_at":"2026-05-06T07:19:50.702277885Z","actor":"Denis Kiselev","issue_id":"EV-2luk","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
265
+ {"id":"int-57a7f7bd","kind":"field_change","created_at":"2026-05-06T07:19:51.207502619Z","actor":"Denis Kiselev","issue_id":"EV-6pj3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
266
+ {"id":"int-6c362b36","kind":"field_change","created_at":"2026-05-06T07:19:51.688513357Z","actor":"Denis Kiselev","issue_id":"EV-675y","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
267
+ {"id":"int-5debf846","kind":"field_change","created_at":"2026-05-06T07:19:52.17818897Z","actor":"Denis Kiselev","issue_id":"EV-g8pq","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
268
+ {"id":"int-53e2ac7d","kind":"field_change","created_at":"2026-05-06T07:19:52.618140112Z","actor":"Denis Kiselev","issue_id":"EV-7wi7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
269
+ {"id":"int-dccffc47","kind":"field_change","created_at":"2026-05-06T07:19:53.069605194Z","actor":"Denis Kiselev","issue_id":"EV-cz6e","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
270
+ {"id":"int-58d8260f","kind":"field_change","created_at":"2026-05-06T07:19:53.543437653Z","actor":"Denis Kiselev","issue_id":"EV-rmzx","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
271
+ {"id":"int-789dee22","kind":"field_change","created_at":"2026-05-06T07:19:53.965510546Z","actor":"Denis Kiselev","issue_id":"EV-5qg6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
272
+ {"id":"int-85fdbe1f","kind":"field_change","created_at":"2026-05-06T07:19:54.37777323Z","actor":"Denis Kiselev","issue_id":"EV-pfz5","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
273
+ {"id":"int-c61f5bf7","kind":"field_change","created_at":"2026-05-06T07:19:54.831384506Z","actor":"Denis Kiselev","issue_id":"EV-ynvi","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
274
+ {"id":"int-f66ee100","kind":"field_change","created_at":"2026-05-06T07:19:55.253094559Z","actor":"Denis Kiselev","issue_id":"EV-ltmi","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
275
+ {"id":"int-d0d38103","kind":"field_change","created_at":"2026-05-06T07:19:55.730451387Z","actor":"Denis Kiselev","issue_id":"EV-dha6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
276
+ {"id":"int-14872708","kind":"field_change","created_at":"2026-05-06T07:19:56.276618255Z","actor":"Denis Kiselev","issue_id":"EV-gml5","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
277
+ {"id":"int-3a46d2c8","kind":"field_change","created_at":"2026-05-06T07:19:56.746453231Z","actor":"Denis Kiselev","issue_id":"EV-wg1v","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
278
+ {"id":"int-7e6cad25","kind":"field_change","created_at":"2026-05-06T07:19:57.199497315Z","actor":"Denis Kiselev","issue_id":"EV-jf8v","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
279
+ {"id":"int-596241e1","kind":"field_change","created_at":"2026-05-06T07:19:57.648508843Z","actor":"Denis Kiselev","issue_id":"EV-p699","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
280
+ {"id":"int-47d13782","kind":"field_change","created_at":"2026-05-06T07:19:58.119871705Z","actor":"Denis Kiselev","issue_id":"EV-mur2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
281
+ {"id":"int-5bcb3ba0","kind":"field_change","created_at":"2026-05-06T07:19:58.593580083Z","actor":"Denis Kiselev","issue_id":"EV-t05i","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
282
+ {"id":"int-d0a8a49b","kind":"field_change","created_at":"2026-05-06T07:19:59.066030169Z","actor":"Denis Kiselev","issue_id":"EV-t3os","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
283
+ {"id":"int-c5fd767e","kind":"field_change","created_at":"2026-05-06T07:19:59.519830731Z","actor":"Denis Kiselev","issue_id":"EV-70v7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
284
+ {"id":"int-ee96343f","kind":"field_change","created_at":"2026-05-06T07:19:59.945079843Z","actor":"Denis Kiselev","issue_id":"EV-g067","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
285
+ {"id":"int-1088cf0f","kind":"field_change","created_at":"2026-05-06T07:20:00.369713429Z","actor":"Denis Kiselev","issue_id":"EV-vybg","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
286
+ {"id":"int-677bcb4c","kind":"field_change","created_at":"2026-05-06T07:20:00.849595223Z","actor":"Denis Kiselev","issue_id":"EV-b2j8","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
287
+ {"id":"int-49853be8","kind":"field_change","created_at":"2026-05-06T07:20:01.309796737Z","actor":"Denis Kiselev","issue_id":"EV-jxio","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
288
+ {"id":"int-7d1d715d","kind":"field_change","created_at":"2026-05-06T07:20:01.802408893Z","actor":"Denis Kiselev","issue_id":"EV-v1df","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
289
+ {"id":"int-f8751a4f","kind":"field_change","created_at":"2026-05-06T07:20:02.269343453Z","actor":"Denis Kiselev","issue_id":"EV-05x3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
290
+ {"id":"int-98efe12b","kind":"field_change","created_at":"2026-05-06T07:20:02.712854653Z","actor":"Denis Kiselev","issue_id":"EV-neqr","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
291
+ {"id":"int-2fab6f44","kind":"field_change","created_at":"2026-05-06T07:20:03.192828055Z","actor":"Denis Kiselev","issue_id":"EV-ugrl","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
292
+ {"id":"int-64c45537","kind":"field_change","created_at":"2026-05-06T07:20:03.645870239Z","actor":"Denis Kiselev","issue_id":"EV-1czz","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
293
+ {"id":"int-b226aaaa","kind":"field_change","created_at":"2026-05-06T07:20:04.115581656Z","actor":"Denis Kiselev","issue_id":"EV-nsp3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
294
+ {"id":"int-40a144bf","kind":"field_change","created_at":"2026-05-06T07:20:04.57274421Z","actor":"Denis Kiselev","issue_id":"EV-bx35","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
295
+ {"id":"int-bb3376cc","kind":"field_change","created_at":"2026-05-06T07:20:05.130537402Z","actor":"Denis Kiselev","issue_id":"EV-2viy","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
296
+ {"id":"int-6ae5b449","kind":"field_change","created_at":"2026-05-06T07:20:05.597150157Z","actor":"Denis Kiselev","issue_id":"EV-0nre","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
297
+ {"id":"int-f37c08ed","kind":"field_change","created_at":"2026-05-06T07:20:06.22033588Z","actor":"Denis Kiselev","issue_id":"EV-n4ai","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
298
+ {"id":"int-f1cc76b6","kind":"field_change","created_at":"2026-05-06T07:20:06.659235495Z","actor":"Denis Kiselev","issue_id":"EV-oyus","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
299
+ {"id":"int-7d54a6e7","kind":"field_change","created_at":"2026-05-06T07:20:07.091511841Z","actor":"Denis Kiselev","issue_id":"EV-h46p","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
300
+ {"id":"int-2bea4b64","kind":"field_change","created_at":"2026-05-06T07:20:06.87585377Z","actor":"Denis Kiselev","issue_id":"EV-n05g","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
301
+ {"id":"int-3b25e06c","kind":"field_change","created_at":"2026-05-06T07:20:06.448623384Z","actor":"Denis Kiselev","issue_id":"EV-htfi","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
302
+ {"id":"int-9fedcb1f","kind":"field_change","created_at":"2026-05-06T07:20:06.870511987Z","actor":"Denis Kiselev","issue_id":"EV-86au","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
303
+ {"id":"int-55d2834d","kind":"field_change","created_at":"2026-05-06T07:20:07.313207411Z","actor":"Denis Kiselev","issue_id":"EV-fyq8","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
304
+ {"id":"int-44442358","kind":"field_change","created_at":"2026-05-06T07:20:07.761317317Z","actor":"Denis Kiselev","issue_id":"EV-jxr1","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
305
+ {"id":"int-1ec5b145","kind":"field_change","created_at":"2026-05-06T07:20:08.263779524Z","actor":"Denis Kiselev","issue_id":"EV-0lhb","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
306
+ {"id":"int-3177494f","kind":"field_change","created_at":"2026-05-06T07:20:08.690447157Z","actor":"Denis Kiselev","issue_id":"EV-6xeh","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
307
+ {"id":"int-c3e862ed","kind":"field_change","created_at":"2026-05-06T07:20:09.150754493Z","actor":"Denis Kiselev","issue_id":"EV-n5vx","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
308
+ {"id":"int-bbb95b03","kind":"field_change","created_at":"2026-05-06T07:20:09.610416065Z","actor":"Denis Kiselev","issue_id":"EV-pbo4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
309
+ {"id":"int-42dadd38","kind":"field_change","created_at":"2026-05-06T07:20:10.034048271Z","actor":"Denis Kiselev","issue_id":"EV-45kf","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
310
+ {"id":"int-2973ca4f","kind":"field_change","created_at":"2026-05-06T07:20:10.503854404Z","actor":"Denis Kiselev","issue_id":"EV-t2o9","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
311
+ {"id":"int-523d3c8c","kind":"field_change","created_at":"2026-05-06T07:20:11.036667234Z","actor":"Denis Kiselev","issue_id":"EV-qtvs","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
312
+ {"id":"int-be905e1b","kind":"field_change","created_at":"2026-05-06T07:20:11.472283074Z","actor":"Denis Kiselev","issue_id":"EV-psit","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Obsolete: methods refactored on master (commit c6177ee). Rubocop ABC clean for these files."}}
data/.rubocop_todo.yml CHANGED
@@ -3,5 +3,4 @@
3
3
 
4
4
  Metrics/AbcSize:
5
5
  Exclude:
6
- - "lib/evilution/config.rb"
7
6
  - "lib/evilution/runner.rb"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.29.0] - 2026-05-06
4
+
5
+ ### Changed
6
+
7
+ - **Internal codebase hygiene sweep — `Metrics/AbcSize` ceiling tightened from `25` → `17`** — every method exceeding the new threshold was refactored via pure extract-method (no behavior change). ~48 sites across `lib/evilution/{ast,cli,compare,config,disable_comment,integration,mcp,mutation,mutator,parallel,reporter,runner,session,spec_ast_cache}` plus supporting `scripts/` utilities. No public API, CLI flag, or output changes; mutation operators and report emission are bit-identical. The upper bound on per-method ABC is now strictly enforced repo-wide — only `lib/evilution/runner.rb` remains in `.rubocop_todo.yml` (#371, PR #1160 + per-file sub-PRs)
8
+ - **Tuple-return methods across the runner pipeline migrated to named `Data.define` value objects** — internal-only refactor introducing typed return shapes for `Runner::MutationExecutor#call` (→`ExecutionResult`), `Runner::MutationPlanner#call` (→`Plan` plus internal `GenerationResult` / `DisabledFilterResult` / `SigFilterResult` / `EquivalentFilterResult`), `Parallel::WorkQueue` outputs, `Cache#partition` (→`Partition`), `Config.normalize_limit` (→`LimitResult`), `Mutation::Slicer.collect_chain` (→`Chain`), `slice_affected_lines` (→`AffectedSlices`), `CLI::Parser::FilesAndRanges` (→`ParsedPaths`), and assorted CLI command helpers. Improves call-site readability without affecting external behavior (#948, PR #1094; #949, PR #1095; #950, PR #1096; #951, PR #1097; #952, PR #1098; #953, PR #1099; #954, PR #1100; #955, PR #1101; #956, PR #1102)
9
+
10
+ ## [0.28.0] - 2026-05-03
11
+
12
+ ### Added
13
+
14
+ - **Operator profiles: `default` (current 72-operator set) and `strict` (adds aggressive truthiness mutators)** — pre-merge audits can opt into a more sensitive operator mix. The `strict` profile registers `PredicateToNil`, which replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values. Wired through CLI (`--profile=strict`, `--strict` shortcut), `.evilution.yml` (`profile: strict`), and a new `Evilution::Mutator::Registry.for_profile(:default | :strict)` factory. `default` is unchanged, so existing CI runs are not affected (#920, PR #926)
15
+ - **Multi-file batch invocation documented** — `evilution path/a.rb path/b.rb path/c.rb` runs every file in a single Runner invocation so the framework (Rails, Sorbet, etc.) and the `preload` chain load **once** in the parent process. With `--isolation=fork` (default for Rails projects under `auto`), every per-mutation fork branches off the warmed parent — materially faster than `for f in ...; do bundle exec evilution run "$f"; done`. README now has a "5a. Multi-file batch scan" workflow section and an end-to-end runner spec covers two positional file paths; session save/load preserves per-file paths in `survived[].file` (#922, PR #927)
16
+
17
+ ### Fixed
18
+
19
+ - **`Compare::Normalizer` mis-classified Mutant payload lines whose mutated source started with `--` or `++` as unified-diff headers** — pre-existing bug in `extract_from_mutant_diff` that would, for example, drop a removed line `--flag` (emitted as `---flag` in the diff). The new `DiffExtractor::Mutant` requires a trailing space after `---`/`+++` to match a header, preserving real payload. Equivalent Evilution/Mutant mutations on such lines now hash identically and `compare` no longer reports false additions/removals (#917, PR #934)
20
+
21
+ ### Changed
22
+
23
+ - **Internal `Evilution::Compare::Fingerprint` SOLID refactor** — module-function form replaced with a class taking injectable `(extractor:, normalizer:)` collaborators and a `#call(diff:, file_path:, line:)` interface. Diff parsing extracted into `Evilution::Compare::DiffExtractor::{Evilution,Mutant}` strategy classes (one per format, common duck-typed interface), enabling open/closed extension for future tools without touching the orchestrator. `Compare::Normalizer` constructs both fingerprints once and reuses them across records (#917, PR #934)
24
+ - **`Evilution::Mutation` migrated to value-object composition** — sources, slice, parse status now Data.define-backed value objects (#822, PR #907)
25
+ - **`Evilution::Result::MutationResult` encapsulates memory and error state in dedicated Data.define value objects** — `MemoryStats` and `ErrorInfo` instead of flat positional fields (#823, PR #907)
26
+ - **`Reporter::Suggestion` registry/templates and the RSpec/Minitest template builders unified** — single `build` entrypoint per format (#824, PR #908; #849, PR #905; #850, PR #904)
27
+ - **`Reporter::HTML` namespace inlined into `report.rb`** — separate `namespace.rb` removed, autoload pattern adopted for sub-templates (#826, PR #909)
28
+ - **`Compare::LineNormalizer` extracted into its own class** — whitespace collapse separated from fingerprint orchestration (#829, PR #832)
29
+ - **`Evilution::Config` attribute assignment migrated to a transformation map** — single source of truth for type coercion across simple attributes (#830)
30
+ - **`Runner` `require` chain consolidated** — sub-component loading now centralized; circular-require pitfalls in `MutationExecutor` resolved with `Module#autoload` for child strategy/neutralizer files (#831)
31
+ - **Process cleanup helpers extracted into `Evilution::ProcessCleanup`** — `safe_kill(sig, pid)` and `safe_wait(pid)` shared by `Baseline`, `Isolation::Fork`, and `WorkQueue::Worker`, replacing scattered inline `rescue` modifiers swallowing `Errno::ESRCH`/`ECHILD` (#838)
32
+ - **`ProgressStreamer` and `Loop` error handling tightened** — generic `StandardError` rescues replaced with specific `Errno::EPIPE`/`Errno::EBADF`/etc.; once-only warning suppression added so a flood of failures cannot drown stderr (#827, #840)
33
+ - **Crash detector predicate methods renamed** — `has_*?` → `*?` per Ruby/RSpec conventions (`have_X` matcher calls `has_X?`; the renamed methods are still picked up by `be_X` matchers used in specs) (#839)
34
+ - **Rubocop hygiene sweep across 6 sites** — `Style/RescueModifier`, `Lint/UnusedMethodArgument`, `Lint/SuppressedException` (3 instances), `Security/Eval`, and `Security/MarshalLoad` (3 instances) inline disable comments removed in favor of either narrowed code, explanatory rescue-body comments, or main-`.rubocop.yml` per-file Excludes documented with the underlying trust boundary (#832, #833, #834, #835, #836, #837)
35
+
36
+ ### Documentation
37
+
38
+ - **README "Operator Profiles" subsection** — explains the `default` vs `strict` profiles, how to opt in (CLI, config, shortcut), and what `strict` adds today (#920, PR #926)
39
+ - **README "5a. Multi-file batch scan" workflow** — documents Rails-loads-once amortisation and qualifies the speed claim by isolation mode (`fork` vs `in_process`) (#922, PR #927)
40
+ - **`.evilution.yml` template gained a `profile:` block** — generated by `evilution init` (#920, PR #926)
41
+
3
42
  ## [0.27.0] - 2026-04-26
4
43
 
5
44
  ### Added
data/README.md CHANGED
@@ -118,6 +118,17 @@ The shorter alias `evil` ships alongside `evilution` and accepts identical argum
118
118
  | `--fallback-full-suite` | Boolean | false | When no matching spec/test resolves for a mutation, run the whole test suite instead of marking it `:unresolved` and skipping. |
119
119
  | `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
120
120
  | `-e CODE`, `--eval CODE` | String | _(none)_ | Inline Ruby code for `util mutation` command. |
121
+ | `--profile NAME` | String | `default` | Operator profile: `default` or `strict`. `strict` adds aggressive truthiness mutators (e.g. replaces `x.predicate?` with `nil`) intended for pre-merge audits. |
122
+ | `--strict` | Boolean | false | Shortcut for `--profile=strict`. |
123
+
124
+ ### Operator Profiles
125
+
126
+ Two profiles ship out of the box:
127
+
128
+ - **`default`** — the 72 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
129
+ - **`strict`** — adds extra truthiness mutators on top of `default`. Currently `PredicateToNil` (replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values). Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
130
+
131
+ Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.evilution.yml`.
121
132
 
122
133
  ### Exit Codes
123
134
 
@@ -484,6 +495,14 @@ bundle exec evilution run lib/specific_file.rb --format json
484
495
 
485
496
  Use when you know which file was modified and want to verify its test coverage.
486
497
 
498
+ ### 5a. Multi-file batch scan
499
+
500
+ ```bash
501
+ bundle exec evilution run lib/models/user.rb lib/models/account.rb lib/models/order.rb
502
+ ```
503
+
504
+ Pass multiple file paths on a single invocation to amortise startup cost. The framework (Rails, Sorbet, etc.) and the `preload` chain (`spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb`) load **once** in the parent process. When `--isolation=fork` is selected (the default `--isolation=auto` resolves to `fork` on Rails projects), every subsequent mutation across all files forks from that warmed parent — materially faster than scripting a `for f in ...; do bundle exec evilution run "$f"; done` loop, which pays the bootstrap per file. With `--isolation=in_process` (default for non-Rails projects under `auto`), there is no per-mutation fork, but the parent-process boot still runs once instead of N times. Per-file paths and line numbers are preserved in the report (`survived[].file`, HTML grouping by source file).
505
+
487
506
  ### 6. Fixing surviving mutants
488
507
 
489
508
  For each entry in `survived[]`:
@@ -17,18 +17,35 @@ class Evilution::AST::ConstantNames
17
17
  private
18
18
 
19
19
  def collect(node, nesting = [])
20
- names = []
21
20
  case node
22
- when Prism::ModuleNode, Prism::ClassNode
23
- const = node.constant_path.full_name
24
- qualified = nesting.any? && !const.include?("::") ? "#{nesting.join("::")}::#{const}" : const
25
- names << qualified
26
- names.concat(collect(node.body, nesting + [const])) if node.body
27
- when Prism::ProgramNode
28
- names.concat(collect(node.statements, nesting)) if node.statements
29
- when Prism::StatementsNode
30
- node.body.each { |child| names.concat(collect(child, nesting)) }
21
+ when Prism::ModuleNode, Prism::ClassNode then collect_class(node, nesting)
22
+ when Prism::ProgramNode then collect_program(node, nesting)
23
+ when Prism::StatementsNode then collect_statements(node, nesting)
24
+ else []
31
25
  end
32
- names
26
+ end
27
+
28
+ def collect_class(node, nesting)
29
+ const = node.constant_path.full_name
30
+ qualified = qualify(const, nesting)
31
+ return [qualified] if node.body.nil?
32
+
33
+ [qualified] + collect(node.body, nesting + [const])
34
+ end
35
+
36
+ def collect_program(node, nesting)
37
+ return [] if node.statements.nil?
38
+
39
+ collect(node.statements, nesting)
40
+ end
41
+
42
+ def collect_statements(node, nesting)
43
+ node.body.flat_map { |child| collect(child, nesting) }
44
+ end
45
+
46
+ def qualify(const, nesting)
47
+ return const if nesting.empty? || const.include?("::")
48
+
49
+ "#{nesting.join("::")}::#{const}"
33
50
  end
34
51
  end
@@ -75,24 +75,36 @@ class Evilution::AST::Pattern::Parser
75
75
 
76
76
  def parse_value
77
77
  skip_whitespace
78
+ parse_negation || parse_deep_wildcard || parse_single_wildcard || parse_any_node || parse_value_or_nested
79
+ end
78
80
 
79
- if current_char == "!"
80
- advance(1)
81
- skip_whitespace
82
- inner = parse_value
83
- Evilution::AST::Pattern::NegationMatcher.new(inner)
84
- elsif current_char == "*" && !peek_string("**")
85
- advance(1)
86
- Evilution::AST::Pattern::WildcardValueMatcher.new
87
- elsif peek_string("**")
88
- advance(2)
89
- Evilution::AST::Pattern::DeepWildcardMatcher.new
90
- elsif current_char == "_" && !identifier_continues?(1)
91
- advance(1)
92
- Evilution::AST::Pattern::AnyNodeMatcher.new
93
- else
94
- parse_value_or_nested
95
- end
81
+ def parse_negation
82
+ return nil unless current_char == "!"
83
+
84
+ advance(1)
85
+ skip_whitespace
86
+ Evilution::AST::Pattern::NegationMatcher.new(parse_value)
87
+ end
88
+
89
+ def parse_deep_wildcard
90
+ return nil unless peek_string("**")
91
+
92
+ advance(2)
93
+ Evilution::AST::Pattern::DeepWildcardMatcher.new
94
+ end
95
+
96
+ def parse_single_wildcard
97
+ return nil unless current_char == "*"
98
+
99
+ advance(1)
100
+ Evilution::AST::Pattern::WildcardValueMatcher.new
101
+ end
102
+
103
+ def parse_any_node
104
+ return nil unless current_char == "_" && !identifier_continues?(1)
105
+
106
+ advance(1)
107
+ Evilution::AST::Pattern::AnyNodeMatcher.new
96
108
  end
97
109
 
98
110
  def parse_value_or_nested
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "spec_resolver"
4
+ require_relative "process_cleanup"
4
5
 
5
6
  class Evilution::Baseline
6
7
  Result = Struct.new(:failed_spec_files, :duration) do
@@ -72,7 +73,7 @@ class Evilution::Baseline
72
73
  Process.wait(pid)
73
74
  return false if data.empty?
74
75
 
75
- result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
76
+ result = Marshal.load(data)
76
77
  result[:passed]
77
78
  else
78
79
  terminate_child(pid)
@@ -81,7 +82,7 @@ class Evilution::Baseline
81
82
  end
82
83
 
83
84
  def terminate_child(pid)
84
- Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
85
+ Evilution::ProcessCleanup.safe_kill("TERM", pid)
85
86
  _, status = Process.waitpid2(pid, Process::WNOHANG)
86
87
  return if status
87
88
 
@@ -89,8 +90,8 @@ class Evilution::Baseline
89
90
  _, status = Process.waitpid2(pid, Process::WNOHANG)
90
91
  return if status
91
92
 
92
- Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
93
- Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
93
+ Evilution::ProcessCleanup.safe_kill("KILL", pid)
94
+ Evilution::ProcessCleanup.safe_wait(pid)
94
95
  end
95
96
 
96
97
  private
@@ -14,10 +14,7 @@ class Evilution::CLI::Commands::SessionDiff < Evilution::CLI::Command
14
14
  def perform
15
15
  raise Evilution::ConfigError, "two session file paths required" unless @files.length == 2
16
16
 
17
- store = Evilution::Session::Store.new
18
- base_data = store.load(@files[0])
19
- head_data = store.load(@files[1])
20
- result = Evilution::Session::Diff.new.call(base_data, head_data)
17
+ result = compute_diff(@files)
21
18
  Evilution::CLI::Printers::SessionDiff.new(result, format: @options[:format]).render(@stdout)
22
19
  0
23
20
  rescue ::JSON::ParserError => e
@@ -25,6 +22,11 @@ class Evilution::CLI::Commands::SessionDiff < Evilution::CLI::Command
25
22
  rescue SystemCallError => e
26
23
  raise Evilution::Error, e.message
27
24
  end
25
+
26
+ def compute_diff(files)
27
+ store = Evilution::Session::Store.new
28
+ Evilution::Session::Diff.new.call(store.load(files[0]), store.load(files[1]))
29
+ end
28
30
  end
29
31
 
30
32
  Evilution::CLI::Dispatcher.register(:session_diff, Evilution::CLI::Commands::SessionDiff)
@@ -9,6 +9,9 @@ require_relative "../../runner"
9
9
  require_relative "../../mutator"
10
10
 
11
11
  class Evilution::CLI::Commands::Subjects < Evilution::CLI::Command
12
+ EntriesResult = Data.define(:entries, :total)
13
+ private_constant :EntriesResult
14
+
12
15
  private
13
16
 
14
17
  def perform
@@ -23,8 +26,8 @@ class Evilution::CLI::Commands::Subjects < Evilution::CLI::Command
23
26
  return 0
24
27
  end
25
28
 
26
- entries, total = collect_entries(subjects, config)
27
- Evilution::CLI::Printers::Subjects.new(entries, total_mutations: total).render(@stdout)
29
+ result = collect_entries(subjects, config)
30
+ Evilution::CLI::Printers::Subjects.new(result.entries, total_mutations: result.total).render(@stdout)
28
31
  0
29
32
  end
30
33
 
@@ -43,7 +46,7 @@ class Evilution::CLI::Commands::Subjects < Evilution::CLI::Command
43
46
  subj.release_node!
44
47
  end
45
48
 
46
- [entries, total]
49
+ EntriesResult.new(entries: entries, total: total)
47
50
  end
48
51
  end
49
52
 
@@ -11,11 +11,14 @@ require_relative "../../mutator/registry"
11
11
  require_relative "../../ast/parser"
12
12
 
13
13
  class Evilution::CLI::Commands::UtilMutation < Evilution::CLI::Command
14
+ SourceInput = Data.define(:source, :file_path)
15
+ private_constant :SourceInput
16
+
14
17
  private
15
18
 
16
19
  def perform
17
- source, file_path = resolve_util_mutation_source
18
- subjects = parse_source_to_subjects(source, file_path)
20
+ input = resolve_util_mutation_source
21
+ subjects = parse_source_to_subjects(input.source, input.file_path)
19
22
  config = Evilution::Config.new(**@options)
20
23
  registry = Evilution::Mutator::Registry.default
21
24
  operator_options = build_operator_options(config)
@@ -33,24 +36,26 @@ class Evilution::CLI::Commands::UtilMutation < Evilution::CLI::Command
33
36
  end
34
37
 
35
38
  def resolve_util_mutation_source
36
- if @options[:eval]
37
- tmpfile = Tempfile.new(["evilution_eval", ".rb"])
38
- tmpfile.write(@options[:eval])
39
- tmpfile.flush
40
- @util_tmpfile = tmpfile
41
- [@options[:eval], tmpfile.path]
42
- elsif @files.first
43
- path = @files.first
44
- raise Evilution::Error, "file not found: #{path}" unless File.exist?(path)
39
+ return build_eval_source(@options[:eval]) if @options[:eval]
40
+ return build_file_source(@files.first) if @files.first
45
41
 
46
- begin
47
- [File.read(path), path]
48
- rescue SystemCallError => e
49
- raise Evilution::Error, e.message
50
- end
51
- else
52
- raise Evilution::Error, "source required: use -e 'code' or provide a file path"
53
- end
42
+ raise Evilution::Error, "source required: use -e 'code' or provide a file path"
43
+ end
44
+
45
+ def build_eval_source(code)
46
+ tmpfile = Tempfile.new(["evilution_eval", ".rb"])
47
+ tmpfile.write(code)
48
+ tmpfile.flush
49
+ @util_tmpfile = tmpfile
50
+ SourceInput.new(source: code, file_path: tmpfile.path)
51
+ end
52
+
53
+ def build_file_source(path)
54
+ raise Evilution::Error, "file not found: #{path}" unless File.exist?(path)
55
+
56
+ SourceInput.new(source: File.read(path), file_path: path)
57
+ rescue SystemCallError => e
58
+ raise Evilution::Error, e.message
54
59
  end
55
60
 
56
61
  def parse_source_to_subjects(source, file_label)
@@ -20,6 +20,13 @@ class Evilution::CLI::Parser::CommandExtractor
20
20
  ENVIRONMENT_SUBCOMMANDS = { "show" => :environment_show }.freeze
21
21
  UTIL_SUBCOMMANDS = { "mutation" => :util_mutation }.freeze
22
22
 
23
+ SUBCOMMAND_FAMILIES = {
24
+ "session" => [SESSION_SUBCOMMANDS, "session", "list, show, diff, gc"],
25
+ "tests" => [TESTS_SUBCOMMANDS, "tests", "list"],
26
+ "environment" => [ENVIRONMENT_SUBCOMMANDS, "environment", "show"],
27
+ "util" => [UTIL_SUBCOMMANDS, "util", "mutation"]
28
+ }.freeze
29
+
23
30
  Result = Struct.new(:command, :remaining_argv, :parse_error)
24
31
 
25
32
  def self.call(argv)
@@ -46,18 +53,9 @@ class Evilution::CLI::Parser::CommandExtractor
46
53
  @argv.shift
47
54
  elsif first == "run"
48
55
  @argv.shift
49
- elsif first == "session"
50
- @argv.shift
51
- extract_subcommand(SESSION_SUBCOMMANDS, "session", "list, show, diff, gc")
52
- elsif first == "tests"
53
- @argv.shift
54
- extract_subcommand(TESTS_SUBCOMMANDS, "tests", "list")
55
- elsif first == "environment"
56
- @argv.shift
57
- extract_subcommand(ENVIRONMENT_SUBCOMMANDS, "environment", "show")
58
- elsif first == "util"
56
+ elsif SUBCOMMAND_FAMILIES.key?(first)
59
57
  @argv.shift
60
- extract_subcommand(UTIL_SUBCOMMANDS, "util", "mutation")
58
+ extract_subcommand(*SUBCOMMAND_FAMILIES[first])
61
59
  end
62
60
  end
63
61
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution::CLI::Parser::FileArgs
4
+ ParsedPaths = Data.define(:files, :ranges)
5
+
4
6
  module_function
5
7
 
6
8
  def parse(raw_args)
@@ -15,7 +17,7 @@ module Evilution::CLI::Parser::FileArgs
15
17
  ranges[file] = parse_line_range(range_str)
16
18
  end
17
19
 
18
- [files, ranges]
20
+ ParsedPaths.new(files: files, ranges: ranges)
19
21
  end
20
22
 
21
23
  def expand_spec_dir(dir)
@@ -21,6 +21,9 @@ class Evilution::CLI::Parser::OptionsBuilder
21
21
  add_core_options(opts)
22
22
  add_filter_options(opts)
23
23
  add_flag_options(opts)
24
+ add_runner_mode_options(opts)
25
+ add_output_options(opts)
26
+ add_profile_options(opts)
24
27
  add_extra_flag_options(opts)
25
28
  add_session_options(opts)
26
29
  add_compare_options(opts)
@@ -46,11 +49,19 @@ class Evilution::CLI::Parser::OptionsBuilder
46
49
  end
47
50
 
48
51
  def add_filter_options(opts)
52
+ add_spec_filter_options(opts)
53
+ add_targeting_options(opts)
54
+ end
55
+
56
+ def add_spec_filter_options(opts)
49
57
  opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
50
58
  opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
51
59
  opts.on("--spec-dir DIR", "Include all specs in DIR") { |d| expand_spec_dir(d) }
52
60
  opts.on("--spec-pattern GLOB",
53
61
  "Restrict resolved spec candidates to files matching GLOB") { |p| @options[:spec_pattern] = p }
62
+ end
63
+
64
+ def add_targeting_options(opts)
54
65
  opts.on("--no-example-targeting",
55
66
  "Disable per-mutation example targeting (run all examples in resolved spec files)") do
56
67
  @options[:example_targeting] = false
@@ -75,13 +86,19 @@ class Evilution::CLI::Parser::OptionsBuilder
75
86
  "Use --no-incremental to override `incremental: true` from the config file for one run.") do |v|
76
87
  @options[:incremental] = v
77
88
  end
89
+ opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
90
+ end
91
+
92
+ def add_runner_mode_options(opts)
78
93
  opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
79
94
  opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
80
95
  opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
81
96
  "(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
82
97
  "test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
83
98
  opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
84
- opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
99
+ end
100
+
101
+ def add_output_options(opts)
85
102
  opts.on("--suggest-tests", "Generate concrete test code in suggestions (RSpec or Minitest)") { @options[:suggest_tests] = true }
86
103
  opts.on("--no-progress", "Disable progress bar") { @options[:progress] = false }
87
104
  opts.on("--quiet-children",
@@ -95,7 +112,19 @@ class Evilution::CLI::Parser::OptionsBuilder
95
112
  end
96
113
  end
97
114
 
115
+ def add_profile_options(opts)
116
+ opts.on("--profile NAME", "Operator profile: default, strict (default: default). " \
117
+ "strict adds aggressive truthiness mutators for pre-merge audits.") { |p| @options[:profile] = p }
118
+ opts.on("--strict", "Shortcut for --profile=strict") { @options[:profile] = "strict" }
119
+ end
120
+
98
121
  def add_extra_flag_options(opts)
122
+ add_mutation_behavior_options(opts)
123
+ add_session_persistence_options(opts)
124
+ add_misc_extra_options(opts)
125
+ end
126
+
127
+ def add_mutation_behavior_options(opts)
99
128
  opts.on("--skip-heredoc-literals", "Skip all string literal mutations inside heredocs") { @options[:skip_heredoc_literals] = true }
100
129
  opts.on("--related-specs-heuristic", "Append related request/integration/feature/system specs for includes() mutations") do
101
130
  @options[:related_specs_heuristic] = true
@@ -105,8 +134,14 @@ class Evilution::CLI::Parser::OptionsBuilder
105
134
  @options[:fallback_to_full_suite] = true
106
135
  end
107
136
  opts.on("--show-disabled", "Report mutations skipped by # evilution:disable") { @options[:show_disabled] = true }
137
+ end
138
+
139
+ def add_session_persistence_options(opts)
108
140
  opts.on("--baseline-session PATH", "Compare against a baseline session in HTML report") { |p| @options[:baseline_session] = p }
109
141
  opts.on("--save-session", "Save session results to .evilution/results/") { @options[:save_session] = true }
142
+ end
143
+
144
+ def add_misc_extra_options(opts)
110
145
  opts.on("-e", "--eval CODE", "Evaluate code snippet (for util mutation)") { |c| @options[:eval] = c }
111
146
  opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
112
147
  opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
@@ -22,7 +22,7 @@ class Evilution::CLI::Parser::StdinReader
22
22
  line = line.strip
23
23
  lines << line unless line.empty?
24
24
  end
25
- files, ranges = Evilution::CLI::Parser::FileArgs.parse(lines)
26
- Result.new(files, ranges, nil)
25
+ parsed_paths = Evilution::CLI::Parser::FileArgs.parse(lines)
26
+ Result.new(parsed_paths.files, parsed_paths.ranges, nil)
27
27
  end
28
28
  end