evilution 1.3.0 → 1.4.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 +62 -0
  3. data/.rubocop_todo.yml +5 -0
  4. data/CHANGELOG.md +77 -0
  5. data/README.md +80 -24
  6. data/docs/architecture.md +89 -9
  7. data/docs/ast_pattern_syntax.md +50 -5
  8. data/docs/isolation.md +39 -2
  9. data/docs/migration-from-mutant.md +1 -1
  10. data/lib/evilution/ast/aasm_declaration.rb +133 -0
  11. data/lib/evilution/ast/callback_declaration.rb +91 -0
  12. data/lib/evilution/ast/included_block.rb +27 -0
  13. data/lib/evilution/ast/literal_callable.rb +21 -0
  14. data/lib/evilution/ast/parser.rb +119 -16
  15. data/lib/evilution/ast/pattern/method_name.rb +63 -0
  16. data/lib/evilution/ast/pattern/parser.rb +14 -15
  17. data/lib/evilution/ast/regexp_pattern.rb +104 -0
  18. data/lib/evilution/ast/scope_declaration.rb +45 -0
  19. data/lib/evilution/ast/uncovered_code.rb +88 -0
  20. data/lib/evilution/ast/value_object_definition.rb +25 -0
  21. data/lib/evilution/baseline/failure_formatter.rb +27 -0
  22. data/lib/evilution/baseline/report.rb +66 -0
  23. data/lib/evilution/baseline/spec_failure.rb +38 -0
  24. data/lib/evilution/baseline.rb +67 -30
  25. data/lib/evilution/cli/parser/file_args.rb +2 -1
  26. data/lib/evilution/cli/parser/options_builder.rb +1 -1
  27. data/lib/evilution/cli.rb +3 -2
  28. data/lib/evilution/config/validators/spec_mappings.rb +2 -1
  29. data/lib/evilution/config.rb +3 -2
  30. data/lib/evilution/equivalent/detector.rb +3 -1
  31. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
  32. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
  33. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
  34. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
  35. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
  36. data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
  37. data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
  38. data/lib/evilution/example_filter.rb +45 -0
  39. data/lib/evilution/hooks/registry.rb +2 -1
  40. data/lib/evilution/integration/base.rb +2 -0
  41. data/lib/evilution/integration/known_failures.rb +26 -0
  42. data/lib/evilution/integration/loading/body_call_neutralizer.rb +132 -13
  43. data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
  44. data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
  45. data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
  46. data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
  47. data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
  48. data/lib/evilution/integration/minitest/test_ids.rb +21 -0
  49. data/lib/evilution/integration/minitest.rb +45 -6
  50. data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
  51. data/lib/evilution/integration/rspec/example_ids.rb +35 -0
  52. data/lib/evilution/integration/rspec/result_builder.rb +6 -1
  53. data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
  54. data/lib/evilution/integration/rspec/state_guard.rb +4 -1
  55. data/lib/evilution/integration/rspec.rb +22 -4
  56. data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
  57. data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
  58. data/lib/evilution/integration/test_unit.rb +37 -9
  59. data/lib/evilution/isolation/fork.rb +2 -1
  60. data/lib/evilution/isolation/in_process.rb +21 -4
  61. data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
  62. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
  63. data/lib/evilution/memory/leak_check.rb +27 -3
  64. data/lib/evilution/mutation.rb +7 -2
  65. data/lib/evilution/mutator/base.rb +49 -5
  66. data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
  67. data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
  68. data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
  69. data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
  70. data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
  71. data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
  72. data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
  73. data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
  74. data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
  75. data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
  76. data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
  77. data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
  78. data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
  79. data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
  80. data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
  81. data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
  82. data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
  83. data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
  84. data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
  85. data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
  86. data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
  87. data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
  88. data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
  89. data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
  90. data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
  91. data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
  92. data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
  93. data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
  94. data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
  95. data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
  96. data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
  97. data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
  98. data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
  99. data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
  100. data/lib/evilution/mutator/registry.rb +31 -2
  101. data/lib/evilution/mutator/rescue_handlers.rb +81 -0
  102. data/lib/evilution/process_supervisor.rb +3 -2
  103. data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
  104. data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
  105. data/lib/evilution/reporter/json/baseline.rb +15 -0
  106. data/lib/evilution/reporter/json.rb +7 -2
  107. data/lib/evilution/result/baseline_neutralization.rb +10 -0
  108. data/lib/evilution/result/mutation_result.rb +9 -1
  109. data/lib/evilution/result/summary.rb +42 -2
  110. data/lib/evilution/runner/baseline_runner.rb +9 -4
  111. data/lib/evilution/runner/canary.rb +2 -57
  112. data/lib/evilution/runner/canary_failure_message.rb +101 -0
  113. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
  114. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
  115. data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
  116. data/lib/evilution/runner/mutation_executor.rb +1 -1
  117. data/lib/evilution/runner/mutation_planner.rb +11 -2
  118. data/lib/evilution/runner/report_publisher.rb +3 -2
  119. data/lib/evilution/runner/subject_pipeline.rb +46 -1
  120. data/lib/evilution/runner.rb +4 -1
  121. data/lib/evilution/subject.rb +8 -2
  122. data/lib/evilution/version.rb +1 -1
  123. data/lib/evilution.rb +28 -0
  124. data/script/memory_check +62 -16
  125. metadata +83 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 171e27cbec4014afbffb3c058ecc0b666caa8f849c33532ed55f84e2ecee3a87
4
- data.tar.gz: bff682f7afeb96e4b565ae916950ad8b526f5eca2dc00bb88f17a28382b5e9d9
3
+ metadata.gz: 12d6c2e2e3a5da3abf68a795d1537e103f3156c52df6709fd7543968efca73a9
4
+ data.tar.gz: c072227e0cb2d7ea933ad818853e51adbd21c90268308ce527a8670b8fdbfc23
5
5
  SHA512:
6
- metadata.gz: c272e9c1cb1c5d0af0bb85856b02a59eb14dfad81bc4285d035eb693826eb8559acf803ac14482acf736da98e531c6b1444bb269fa8d0d3ecb340c16f2942f9e
7
- data.tar.gz: b5a31b2aa40568c6f43ab9147d8a3cde7c8d2ab9205466a3d459eaca5395b7b31cf2a94a1e997a2e6f982cc4ef8fd189be268f2bfc52c0f897a963eebc9f0718
6
+ metadata.gz: a90a01e42b93bc3409e8aa730df657eba157510078a338b87f96c255bbe89b5a01d74c8dd1561880c7b094c9c32fc50c22a78cd78b2e66537b7996db49de206e
7
+ data.tar.gz: 8c886d18f0c2acecc1c9673f540823860d3e40e41937ce1477f4f52578b549dade9c0e2ce019a94c4249bdc81d9cfaf7c9ffcc30381b8a5d7d4788844b3f6c2b
@@ -527,3 +527,65 @@
527
527
  {"id":"int-356e6b7a","kind":"field_change","created_at":"2026-09-28T02:59:40.442681333Z","actor":"Denis Kiselev","issue_id":"EV-ae7u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
528
528
  {"id":"int-a15bfc29","kind":"field_change","created_at":"2026-09-28T03:35:22.865923943Z","actor":"Denis Kiselev","issue_id":"EV-ziy0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
529
529
  {"id":"int-c4c3b0ee","kind":"field_change","created_at":"2026-09-28T05:00:30.703915081Z","actor":"Denis Kiselev","issue_id":"EV-qj40","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
530
+ {"id":"int-6b262792","kind":"field_change","created_at":"2026-10-02T07:16:11.010581665Z","actor":"Denis Kiselev","issue_id":"EV-in17.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Endless defs already covered by MethodBodyReplacement + MethodBodyToRaise; regression specs added instead of a new operator"}}
531
+ {"id":"int-20d9d25f","kind":"field_change","created_at":"2026-10-02T08:01:02.739469298Z","actor":"Denis Kiselev","issue_id":"EV-in17.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: DataStructMember operator (drop + adjacent swap), ArgumentRemoval skips member lists"}}
532
+ {"id":"int-b844f06a","kind":"field_change","created_at":"2026-10-02T08:55:07.467612422Z","actor":"Denis Kiselev","issue_id":"EV-in17.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: PinOperatorRemoval operator"}}
533
+ {"id":"int-813d5c0f","kind":"field_change","created_at":"2026-10-02T11:26:04.587491311Z","actor":"Denis Kiselev","issue_id":"EV-in17.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RightwardAssignment operator"}}
534
+ {"id":"int-d795672b","kind":"field_change","created_at":"2026-10-02T11:53:57.423520962Z","actor":"Denis Kiselev","issue_id":"EV-in17.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: NumberedParameterSwap operator (re-scoped from ImplicitBlockParam)"}}
535
+ {"id":"int-3548d60e","kind":"field_change","created_at":"2026-10-02T12:26:03.455515136Z","actor":"Denis Kiselev","issue_id":"EV-in17.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: ForwardedArgumentDrop operator (re-scoped from ArgumentForwarding)"}}
536
+ {"id":"int-97a35172","kind":"field_change","created_at":"2026-10-02T15:11:30.435617553Z","actor":"Denis Kiselev","issue_id":"EV-in17.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: PatternWildcardWidening operator"}}
537
+ {"id":"int-80bc2182","kind":"field_change","created_at":"2026-10-02T15:36:15.078727262Z","actor":"Denis Kiselev","issue_id":"EV-in17.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: NoMatchingPatternElse operator (else insertion; removal already in case_in)"}}
538
+ {"id":"int-074659be","kind":"field_change","created_at":"2026-10-02T15:51:58.478518221Z","actor":"Denis Kiselev","issue_id":"EV-gw8w","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Merged via PR #1693: constant and delimiters preserved in array/find pattern mutations"}}
539
+ {"id":"int-cf36f2ea","kind":"field_change","created_at":"2026-10-03T02:50:23.790936748Z","actor":"Denis Kiselev","issue_id":"EV-p7l7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: keyword_argument keeps an anonymous ** the body uses"}}
540
+ {"id":"int-c78705e8","kind":"field_change","created_at":"2026-10-03T03:34:56.023536028Z","actor":"Denis Kiselev","issue_id":"EV-wuwz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: delimiters helper replaces safe navigation in PatternMatchingArray"}}
541
+ {"id":"int-d742aa8a","kind":"field_change","created_at":"2026-10-03T04:11:14.742547553Z","actor":"Denis Kiselev","issue_id":"EV-a508","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: diff-lcs capped < 2, canary names gem activation conflicts, README defaults MCP to bundle exec"}}
542
+ {"id":"int-5015cfd3","kind":"field_change","created_at":"2026-10-03T05:16:17.707344911Z","actor":"Denis Kiselev","issue_id":"EV-70lx.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: ArgumentOrderPermutation operator with order-free skip list"}}
543
+ {"id":"int-2b6321e3","kind":"field_change","created_at":"2026-10-03T05:46:41.353299041Z","actor":"Denis Kiselev","issue_id":"EV-70lx.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: KeywordValueSwap operator"}}
544
+ {"id":"int-ac550ceb","kind":"field_change","created_at":"2026-10-03T06:08:06.145495467Z","actor":"Denis Kiselev","issue_id":"EV-70lx.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: covered by block_body_promotion; regression specs added"}}
545
+ {"id":"int-cb212c6d","kind":"field_change","created_at":"2026-10-03T11:26:40.545576039Z","actor":"Denis Kiselev","issue_id":"EV-70lx.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: covered by method_call_removal; regression specs added"}}
546
+ {"id":"int-db265ed7","kind":"field_change","created_at":"2026-10-03T12:19:57.106972968Z","actor":"Denis Kiselev","issue_id":"EV-70lx.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: ComparisonOperandSwap for <=>; between?/clamp covered by argument_order_permutation"}}
547
+ {"id":"int-a75daaf2","kind":"field_change","created_at":"2026-10-03T12:54:36.679091996Z","actor":"Denis Kiselev","issue_id":"EV-70lx.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: round/truncate swaps in send_mutation, new integer_division_to_fdiv; half: mode split to EV-qt7f"}}
548
+ {"id":"int-7f131376","kind":"field_change","created_at":"2026-10-03T15:01:33.349965452Z","actor":"Denis Kiselev","issue_id":"EV-70lx.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: OffByOneBoundary operator"}}
549
+ {"id":"int-ba54b5af","kind":"field_change","created_at":"2026-10-03T15:46:31.592677693Z","actor":"Denis Kiselev","issue_id":"EV-70lx.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: FormatSpecifierSwap operator; argument_order_permutation skips format methods"}}
550
+ {"id":"int-d2496545","kind":"field_change","created_at":"2026-10-03T16:55:37.885963941Z","actor":"Denis Kiselev","issue_id":"EV-70lx.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: ExceptionSwallow in strict profile; ignore_patterns grammar split to EV-a6ey"}}
551
+ {"id":"int-dc691f35","kind":"field_change","created_at":"2026-10-04T02:07:48.713969725Z","actor":"Denis Kiselev","issue_id":"EV-70lx.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: StatementReorder in strict profile"}}
552
+ {"id":"int-e47209dc","kind":"field_change","created_at":"2026-10-04T02:22:56.861285181Z","actor":"Denis Kiselev","issue_id":"EV-70lx.11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Won't do: measured on evilution's lib/ (409 files, true -> false, whole spec file per mutant): 384 survived, 18 killed, of which 17 were self-run artefacts (a no-op control mutant was killed on the same 17 files) and 1 real (status_glossary_spec asserts frozen strings). One real signal in 409 does not justify a file-level emission path."}}
553
+ {"id":"int-6259aee3","kind":"field_change","created_at":"2026-10-04T03:01:50.164062208Z","actor":"Denis Kiselev","issue_id":"EV-70lx.12","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: AliasRemoval operator; shared attribution refactor split to EV-u9rq"}}
554
+ {"id":"int-55084cba","kind":"field_change","created_at":"2026-10-04T04:36:27.295973647Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Decision: regexp_parser as runtime dependency, edit by offsets, compile-check every mutant. Evidence posted on #1524. Foundation work split to EV-ib1c.9."}}
555
+ {"id":"int-fc063d48","kind":"field_change","created_at":"2026-10-04T11:06:20.183354321Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: regexp_parser runtime dependency and Evilution::AST::RegexpPattern helper"}}
556
+ {"id":"int-135695f4","kind":"field_change","created_at":"2026-10-04T11:37:04.708217274Z","actor":"Denis Kiselev","issue_id":"EV-spob","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: regex_simplification moved onto RegexpPattern"}}
557
+ {"id":"int-474f5d9c","kind":"field_change","created_at":"2026-10-04T12:01:54.857943644Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpCharacterTypeComplement operator"}}
558
+ {"id":"int-8f7efc39","kind":"field_change","created_at":"2026-10-04T12:20:59.73456011Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpAnchorPromotion operator"}}
559
+ {"id":"int-e092039b","kind":"field_change","created_at":"2026-10-04T12:44:08.732168047Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpAlternationBranchDeletion operator, capped at 10 branches"}}
560
+ {"id":"int-89a03141","kind":"field_change","created_at":"2026-10-04T13:55:56.95130631Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpQuantifierMinimumSwap operator"}}
561
+ {"id":"int-90e07229","kind":"field_change","created_at":"2026-10-04T14:26:18.913357911Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpCaptureToPassive operator"}}
562
+ {"id":"int-c6376ed4","kind":"field_change","created_at":"2026-10-04T15:23:59.584161061Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpNamedGroupRename operator"}}
563
+ {"id":"int-f45f11c3","kind":"field_change","created_at":"2026-10-04T15:51:06.656516074Z","actor":"Denis Kiselev","issue_id":"EV-ib1c.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RegexpOptionRemoval operator"}}
564
+ {"id":"int-f74a65f3","kind":"field_change","created_at":"2026-10-05T01:59:49.116216501Z","actor":"Denis Kiselev","issue_id":"EV-waxv.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RescueHandlerPromotion operator"}}
565
+ {"id":"int-086b8b8f","kind":"field_change","created_at":"2026-10-05T02:25:10.675826812Z","actor":"Denis Kiselev","issue_id":"EV-waxv.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RescueHandlerConcatenation operator (keeps the rescue)"}}
566
+ {"id":"int-8504c32b","kind":"field_change","created_at":"2026-10-05T03:09:46.042410063Z","actor":"Denis Kiselev","issue_id":"EV-waxv.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: RescueElseConcatenation operator (broad rescues only)"}}
567
+ {"id":"int-1a680abd","kind":"field_change","created_at":"2026-10-05T03:45:51.492632284Z","actor":"Denis Kiselev","issue_id":"EV-waxv.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: ReturnKeywordRemoval operator"}}
568
+ {"id":"int-6be631a1","kind":"field_change","created_at":"2026-10-05T03:53:41.798959331Z","actor":"Denis Kiselev","issue_id":"EV-waxv.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged: covered by existing operators; cross-operator regression spec added"}}
569
+ {"id":"int-e7ea4f25","kind":"field_change","created_at":"2026-10-05T04:35:10.212440677Z","actor":"Denis Kiselev","issue_id":"EV-a6ey","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
570
+ {"id":"int-a9f620b2","kind":"field_change","created_at":"2026-10-05T05:20:43.639066857Z","actor":"Denis Kiselev","issue_id":"EV-8lcr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
571
+ {"id":"int-3b99b201","kind":"field_change","created_at":"2026-10-05T06:16:47.160706383Z","actor":"Denis Kiselev","issue_id":"EV-8gzw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
572
+ {"id":"int-72b4a08c","kind":"field_change","created_at":"2026-10-05T09:41:56.709809764Z","actor":"Denis Kiselev","issue_id":"EV-h3ir","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
573
+ {"id":"int-49f7ebbb","kind":"field_change","created_at":"2026-10-05T11:53:56.598210615Z","actor":"Denis Kiselev","issue_id":"EV-hoki","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
574
+ {"id":"int-34d1379a","kind":"field_change","created_at":"2026-10-05T14:22:47.087299042Z","actor":"Denis Kiselev","issue_id":"EV-56hj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
575
+ {"id":"int-6e4bcbc4","kind":"field_change","created_at":"2026-10-06T05:17:58.928359175Z","actor":"Denis Kiselev","issue_id":"EV-mrfr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
576
+ {"id":"int-f8e7bd2e","kind":"field_change","created_at":"2026-10-06T06:34:59.884610002Z","actor":"Denis Kiselev","issue_id":"EV-8ngq","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
577
+ {"id":"int-b1698bc2","kind":"field_change","created_at":"2026-10-06T07:20:23.824889017Z","actor":"Denis Kiselev","issue_id":"EV-vqw1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
578
+ {"id":"int-a6a57a8d","kind":"field_change","created_at":"2026-10-06T08:34:34.810437348Z","actor":"Denis Kiselev","issue_id":"EV-z6y1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
579
+ {"id":"int-a7b8436b","kind":"field_change","created_at":"2026-10-06T11:37:21.379002004Z","actor":"Denis Kiselev","issue_id":"EV-pvau","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
580
+ {"id":"int-316ed97b","kind":"field_change","created_at":"2026-10-06T11:37:21.692481188Z","actor":"Denis Kiselev","issue_id":"EV-e380","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
581
+ {"id":"int-deca8540","kind":"field_change","created_at":"2026-10-06T15:23:09.145528907Z","actor":"Denis Kiselev","issue_id":"EV-z4m2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
582
+ {"id":"int-36097126","kind":"field_change","created_at":"2026-10-06T14:41:05.739141414Z","actor":"Denis Kiselev","issue_id":"EV-84gq","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
583
+ {"id":"int-61e3037a","kind":"field_change","created_at":"2026-10-06T17:43:49.128023228Z","actor":"Denis Kiselev","issue_id":"EV-feu9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
584
+ {"id":"int-6c0d6f1f","kind":"field_change","created_at":"2026-10-07T02:23:54.251384914Z","actor":"Denis Kiselev","issue_id":"EV-lgk8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
585
+ {"id":"int-08fd2997","kind":"field_change","created_at":"2026-10-07T10:43:53.874903719Z","actor":"Denis Kiselev","issue_id":"EV-rm8v","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
586
+ {"id":"int-35f8d0ca","kind":"field_change","created_at":"2026-10-07T12:55:20.009979823Z","actor":"Denis Kiselev","issue_id":"EV-rm8v","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
587
+ {"id":"int-fa03cb81","kind":"field_change","created_at":"2026-10-07T13:07:12.57941145Z","actor":"Denis Kiselev","issue_id":"EV-wk0w","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
588
+ {"id":"int-e6956eaa","kind":"field_change","created_at":"2026-10-07T16:15:46.858274377Z","actor":"Denis Kiselev","issue_id":"EV-wk0w","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
589
+ {"id":"int-995cf63f","kind":"field_change","created_at":"2026-10-07T16:18:39.396678188Z","actor":"Denis Kiselev","issue_id":"EV-s06e","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
590
+ {"id":"int-f1fa1ab6","kind":"field_change","created_at":"2026-10-07T17:32:04.485879419Z","actor":"Denis Kiselev","issue_id":"EV-s06e","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
591
+ {"id":"int-5c22155e","kind":"field_change","created_at":"2026-10-07T17:32:39.183171753Z","actor":"Denis Kiselev","issue_id":"EV-0s4b","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
data/.rubocop_todo.yml CHANGED
@@ -29,3 +29,8 @@ Style/OneClassPerFile:
29
29
  Exclude:
30
30
  - "lib/evilution/cli.rb"
31
31
  - "lib/evilution/integration/rspec/state_guard.rb"
32
+
33
+ Metrics/ParameterLists:
34
+ Exclude:
35
+ - "lib/evilution/result/summary.rb"
36
+ - "lib/evilution/result/mutation_result.rb"
data/CHANGELOG.md CHANGED
@@ -2,6 +2,83 @@
2
2
 
3
3
  Versioning policy: see [docs/versioning.md](docs/versioning.md).
4
4
 
5
+ ## [1.4.0] - 2026-10-07
6
+
7
+ Two things in this release. Code that lives in a class body rather than in a method — scopes, AASM guards, callback conditions, value-object definitions — is now mutated, where a run aimed at it used to report `0 mutations`. And the `default` profile grows from 111 to 136 operators, with two more in `strict`. Mutation scores will move on both counts — every new subject and every new operator produces mutants your suite has never been measured against. Pin the gem version and the operator profile if you need a stable score across runs.
8
+
9
+ A red baseline also means something different now: it no longer hides survivors, and it no longer lets an already-failing example count as a kill. See "Changed".
10
+
11
+ Most of the subject and baseline work comes from a field report on a Rails application running 1.3.0 (GH #1740–#1744).
12
+
13
+ ### Added
14
+
15
+ - **Subjects outside `def`** — subjects were methods only, so a file or a line range holding nothing but class-body code got no mutations at all:
16
+ - **Value-object definitions** — `Point = Data.define(:x, :y)` and `class Coord < Struct.new(:lat, :lng)` outside any method are subjects of their own, named after the constant and mutated by the operators that apply to them. `--target Foo` matches a value-object constant named `Foo` (PR #1738, GH #1683)
17
+ - **ActiveRecord scopes** — `scope :recent, -> { where(recent: true) }` with a literal body is a subject named after the class method it defines (`Order.recent`) and mutated like a method body (PR #1747, GH #1742)
18
+ - **AASM guards and callbacks** — lambdas and blocks written out inside an `event` or `state` declaration, named after the method the declaration defines (`Order#ship`, `Order#paid?`) (PR #1755, GH #1750)
19
+ - **Callback and validation declarations** — the conditions and bodies of `validate :credit_limit, if: -> { paid? }`, `before_save { ... }`, `before_action ..., unless: -> { ... }`, named the way the declaration reads (`Order.validate(:credit_limit)`, `Order.before_save`). The mutated declaration takes the place of the original in its callback chain, so the two do not sit side by side and mask each other (PR #1759, GH #1749)
20
+ - **The same three inside a concern's `included do ... end` block**, named after the concern (`Publishable.published`, `Shippable#ship`, `Publishable.validates(:title)`) and re-declared on every class that already includes it (PRs #1757, #1764, #1766; GH #1748, #1756, #1758)
21
+ - **A warning when targeted lines hold code no subject covers** — a line range, or a whole file with no subject at all, that contains class-body code outside every subject (DSL calls, constant lists) now says so on stderr and in JSON as `summary.uncovered_code` (`[{ file, lines: ["2-4", "9"] }]`), instead of a bare `0 mutations` that reads as "nothing to test here" (PR #1746, GH #1741)
22
+ - **Seven operators for modern syntax (`default` profile)** — pattern matching, anonymous arguments and `Data` / `Struct` (epic GH #1428):
23
+ - **`data_struct_member`** — drops a member, or swaps adjacent members, of a `Data.define` / `Struct.new` definition (PR #1685, GH #1543)
24
+ - **`pin_operator_removal`** — `in ^expected` to `in expected`: the pattern captures instead of comparing (PR #1686, GH #1544)
25
+ - **`rightward_assignment`** — `value => [a, b]` to `value in [a, b]`: a mismatch returns `false` instead of raising (PR #1687, GH #1545)
26
+ - **`numbered_parameter_swap`** — `pairs.map { _1 - _2 }` to `pairs.map { _2 - _1 }` (PR #1688, GH #1546)
27
+ - **`forwarded_argument_drop`** — `g(*, **, &)` to `g(**, &)`; a `...` signature is spelled out so one part can be left out (PR #1690, GH #1547)
28
+ - **`pattern_wildcard_widening`** — `in [a, b]` to `in [a, b, *]`, `in { age: Integer }` to `in { age: _ }`: the pattern accepts shapes it rejected (PR #1692, GH #1548)
29
+ - **`no_matching_pattern_else`** — adds an empty `else` to a `case/in` that has none, so an unmatched value yields `nil` instead of raising (PR #1694, GH #1549)
30
+ - **Seven operators for arguments, numbers and declarations (`default` profile)** (epic GH #1429):
31
+ - **`argument_order_permutation`** — `compute(a, b)` to `compute(b, a)` for calls, `super` and `yield`. Calls that take arguments in any order, such as `OptionParser#on`, can be silenced with `ignore_patterns` (PR #1706, GH #1550)
32
+ - **`keyword_value_swap`** — `compute(x: a, y: b)` to `compute(x: b, y: a)`, keeping the keys (PR #1707, GH #1551)
33
+ - **`comparison_operand_swap`** — `x.age <=> y.age` to `y.age <=> x.age`: reverses a sort block or a custom ordering (PR #1710, GH #1554)
34
+ - **`integer_division_to_fdiv`** — `a / b` to `a.fdiv(b)`: integer division keeps its remainder (PR #1712, GH #1555)
35
+ - **`off_by_one_boundary`** — `n.times` to `(n - 1).times`, `items.first(n)` to `items.first(n - 1)`, for counts held in a variable (PR #1713, GH #1556)
36
+ - **`format_specifier_swap`** — `format("%05d", n)` to `format("%d", n)`, `"%.2f"` to `"%f"` / `"%s"` (PR #1714, GH #1557)
37
+ - **`alias_removal`** — drops an `alias` or `alias_method` declaration (PR #1719, GH #1561)
38
+ - **Seven structural regexp operators (`default` profile)** — built on `regexp_parser`, so each edits one token of the pattern rather than one byte (epic GH #1426; PR #1722, GH #1721):
39
+ - **`regexp_character_type_complement`** — `/\d+/` to `/\D+/`, `/\bword/` to `/\Bword/` (PR #1725, GH #1525)
40
+ - **`regexp_anchor_promotion`** — `^` to `\A`, `$` and `\Z` to `\z`: the pattern stops matching around newlines (PR #1726, GH #1526)
41
+ - **`regexp_alternation_branch_deletion`** — `/cat|dog/` to `/dog/` and `/cat/` (PR #1727, GH #1527)
42
+ - **`regexp_quantifier_minimum_swap`** — `*` to `+` and back, keeping lazy and possessive markers, so only the empty case changes (PR #1728, GH #1528)
43
+ - **`regexp_capture_to_passive`** — `/id=(\d+)/` to `/id=(?:\d+)/`: `$1` and `m[1]` lose their value (PR #1729, GH #1529)
44
+ - **`regexp_named_group_rename`** — `/(?<user>\w+)@/` to `/(?<_user>\w+)@/`: `m[:user]` no longer finds it (PR #1730, GH #1530)
45
+ - **`regexp_option_removal`** — drops `i` or `m` where it changes what the pattern matches (PR #1731, GH #1531)
46
+ - **Four operators for `rescue` and `return` (`default` profile)** (epic GH #1425):
47
+ - **`rescue_handler_promotion`** — runs a rescue handler instead of the code it protects, so the happy path never runs (PR #1732, GH #1519)
48
+ - **`rescue_handler_concatenation`** — runs the handler after the protected code as well, so a successful run also does what the handler does (PR #1733, GH #1520)
49
+ - **`rescue_else_concatenation`** — moves the `else` body into the protected code, so an error it raises is now rescued (PR #1734, GH #1521)
50
+ - **`return_keyword_removal`** — `return :neg if x.negative?` to `:neg if x.negative?`: control flow continues past a guard clause (PR #1735, GH #1522)
51
+ - **Two operators in the `strict` profile**:
52
+ - **`exception_swallow`** — `record.save!` to `record.save! rescue nil`, for statements that raise by convention: a survivor means no test makes it fail and checks the error comes out (PR #1716, GH #1558)
53
+ - **`statement_reorder`** — swaps two adjacent statements that both act, to surface side effects whose order is never asserted (PR #1717, GH #1559)
54
+ - **`ignore_patterns` accepts every method name** — names ending in `?`, `!` or `=` and operator methods are written as they are (`call{name=valid?|save!}`, `call{name=<=>}`); names made of reserved characters go in quotes (`call{name='|'}`). They used to raise `ConfigError`, which left the new operators' noisiest call sites impossible to silence. See `docs/ast_pattern_syntax.md` (PR #1737, GH #1715)
55
+ - **`index_to_fetch` under a guard on the same key is equivalent** — `config.fetch(:size)` cannot raise inside `if config[:size]`, nor after `return unless config[:size]`; both forms are now reported `equivalent` instead of surviving on every run (PRs #1754, #1763; GH #1744, #1753)
56
+ - **A red baseline says why it was red** — the failing examples with their first error line, or the error that stopped the file before any example ran (load error, timeout, a baseline process that died), on stderr once the baseline finishes, under the score line, and in JSON as `summary.baseline_failures`. The baseline used to report pass or fail and nothing else (PR #1752, GH #1743)
57
+
58
+ ### Changed
59
+
60
+ - **A red baseline no longer hides survivors, and no longer counts as a kill** — every survivor covered by a spec file that was red in the baseline used to be recorded `neutral`, although its tests had passed: real gaps disappeared behind full marks. And where the red example failed again in the mutation run, every mutation it ran against was reported `killed`. Now a survivor stays a survivor whatever the baseline did, and a mutation is `neutral` only when its tests failed on nothing but examples that were already failing. `summary.baseline_neutralized` counts those. Expect a lower score on a project with a red or flaky spec file — the earlier one was wrong in both directions (PR #1762, GH #1751)
61
+ - **The same under Minitest and Test::Unit** — tests are named `Class#test` and `test(Class)` in the baseline and in a mutation run alike (PR #1768, GH #1761)
62
+ - **`send_mutation` swaps rounding modes** — `round` to `floor` and `ceil` (PR #1712, GH #1555)
63
+ - **Dependencies** — `regexp_parser` (`>= 2.9, < 3`, the range RuboCop uses) is a new runtime dependency. `diff-lcs` is capped below 2 while `rspec-expectations` requires `< 2.0` (PRs #1722, #1704)
64
+ - **Run the MCP server through `bundle exec`** — the README setup now shows `"command": "bundle", "args": ["exec", "evilution", "mcp"]`, so the project's `Gemfile.lock` decides which gem versions are loaded (PR #1704, GH #1684)
65
+
66
+ ### Fixed
67
+
68
+ - **Methods inside `class << self` are named as class methods** — they were listed as `Gateway#create_group`, so `--target Gateway.create_group` matched nothing and `Gateway.` / `Gateway#` selected the wrong set (PR #1745, GH #1740)
69
+ - **`regex_simplification` no longer mutates group syntax as if it were a quantifier or an anchor** — `/(?:ab)+/` became `/(:ab)+/`, a lookahead became a capture of `=a`, and anchors inside a `(?#comment)` were "removed", producing mutants that could never be killed (PR #1723, GH #1720)
70
+ - **`pattern_matching_array` keeps the constant of a deconstruct pattern** — mutating `Point[x, y]` also removed the `Point` check, and parenthesized, qualified and find patterns lost their shape (PR #1693, GH #1691; fix by [@mikamikasuki](https://github.com/mikamikasuki))
71
+ - **`keyword_argument` no longer removes an anonymous `**` the body forwards** — `def call(*, **, &)` forwarding `target.call(*, **, &)` produced a mutant Ruby rejects (PR #1695, GH #1689)
72
+ - **A gem activation conflict is reported as one** — with the MCP server started outside Bundler, RubyGems activated `diff-lcs` 2.0 and RSpec then refused to load; the run stopped at the proof-of-life canary with a message about the canary. It now names the conflict and the fix (PR #1704, GH #1684)
73
+ - **Eight more advisory messages can no longer be turned into errors** — a project that raises on warnings promoted evilution's own notices ("HTML report written to …", "coverage targeting unavailable", …) to exceptions; they now go through `Evilution::Diagnostic` like the rest (PR #1739, GH #1591)
74
+ - **In-process RSpec runs no longer keep about 1 MB per mutation** — each run with a suite hook left its examples reachable through `RSpec::Core::AnonymousExampleGroup` for the life of the process (65 MB to 220 MB over 150 runs, flat after the fix) (PR #1765; fix by [@gipcompany](https://github.com/gipcompany))
75
+ - **`in_process` isolation runs every mutation's tests under Minitest and Test::Unit** — only the first mutation got its tests run; each later one was an error (`no Minitest tests executed (0 test methods ran)`), and since errors are left out of the score the report read as 100% on one mutation. A test file is now loaded once per process and its classes are dispatched again for every mutation. A suite that builds tests at load time from the code under test still needs `--isolation fork`; see [docs/isolation.md](docs/isolation.md) (PR #1775, GH #1767)
76
+ - **A looping mutant no longer hangs an `in_process` run** — the per-mutation timeout raised `Timeout::Error` once; the test framework recorded one failed test and moved on to the next, which reached the same loop with no timer left. The timeout now raises a `SignalException` that ends the whole test run, and the mutation is reported `timed_out` (PR #1777, GH #1776)
77
+ - **The uncovered-code warning no longer names lines that were mutated, or top-level `require`s** — it listed an `alias_method` line the same run had mutated, and `require` / `require_relative` lines that hold nothing to mutate (PR #1773, GH #1770)
78
+ - **`optional_parameter_to_required` no longer emits mutants Ruby rejects** — optional parameters must be contiguous, so dropping the default of one that sits between two others (`def f(a = nil, b = nil, c = nil)` to `def f(a = nil, b, c = nil)`) is a syntax error. Of several optional parameters only the first is mutated, and the last unless a `*rest` parameter follows (PR #1779, GH #1771)
79
+ - **`splat_operator` leaves the rest of a pattern alone** — `x => { key:, **opts }` became `x => { key:, opts }`, which does not parse. The rest of an array or find pattern (`in [a, *rest]`) is no longer mutated either, although that mutant parsed: expect fewer `splat_operator` mutants in code that uses them (PR #1778, GH #1772; fix by [@gipcompany](https://github.com/gipcompany))
80
+ - **`splat_operator` no longer demotes a `**` that follows another `**`** — `bar(**a, **b)` became `bar(**a, b)`, a positional argument after a keyword splat (PR #1782, GH #1780)
81
+
5
82
  ## [1.3.0] - 2026-09-28
6
83
 
7
84
  Operator expansion release: the `default` profile grows from 88 to 111 operators, mostly around call sites and blocks, and several existing operators gain new replacements. Mutation scores will move — every new operator produces mutants your suite has never been measured against. Pin the gem version and the operator profile if you need a stable score across runs.
data/README.md CHANGED
@@ -109,7 +109,7 @@ Every command, subcommand, and flag listed in this section is part of evilution'
109
109
  |------------------------------|---------|--------------|---------------------------------------------------|
110
110
  | `-t`, `--timeout N` | Integer | 30 | Per-mutation timeout in seconds. |
111
111
  | `-f`, `--format FORMAT` | String | `text` | Output format: `text`, `json`, or `html`. |
112
- | `--target EXPR` | String | _(none)_ | Only mutate matching methods. Supports method name (`Foo::Bar#calculate`), class (`Foo`), namespace wildcards (`Foo::Bar*`), method-type selectors (`Foo#`, `Foo.`), descendants (`descendants:Foo`), and source globs (`source:lib/**/*.rb`). |
112
+ | `--target EXPR` | String | _(none)_ | Only mutate matching subjects. Supports method name (`Foo::Bar#calculate`), class (`Foo`, which also matches a value-object constant named `Foo`), namespace wildcards (`Foo::Bar*`), method-type selectors (`Foo#`, `Foo.`), descendants (`descendants:Foo`), and source globs (`source:lib/**/*.rb`). |
113
113
  | `--output FILE` | String | _(stdout)_ | Write the report to FILE instead of stdout. Useful when a preloaded spec helper writes to stdout on exit. |
114
114
  | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
115
115
  | `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to auto-detection via `SpecResolver`, which also resolves non-mirrored (`spec/unit`, `test/unit`), dir-grouped (`test/unit/<class>/*_test.rb`), and flat `test_`-prefixed (`test/test_connection_pool_timed_stack.rb`) layouts. |
@@ -119,7 +119,7 @@ Every command, subcommand, and flag listed in this section is part of evilution'
119
119
  | `--example-targeting MODE` | String | `lexical` | How targeting picks examples: `lexical` (name-grep example bodies for the mutated method/class), `coverage` (run only the examples that actually execute the mutated line, from a cached line-coverage map), or `full_file` (run all resolved examples). |
120
120
  | `--example-targeting-fallback MODE` | String | `full_file` | Behavior when no example matches: `full_file` (run the whole spec file) or `unresolved` (skip the mutation as `:unresolved`). |
121
121
  | `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Uses demand-driven work distribution with pipe-based IPC. |
122
- | `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
122
+ | `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures, so that a mutation whose tests fail only on those is recorded `neutral` rather than killed. |
123
123
  | `--[no-]canary` | Boolean | _(enabled)_ | Run a proof-of-life synthetic mutation at session start; abort the run if the pipeline misreports it. Catches misconfigured isolation, broken autoload, and reporter-plugin eviction before any real score is produced. Pass `--no-canary` to skip (e.g. CI speed, or when the canary itself is the thing under test). |
124
124
  | `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
125
125
  | `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation; also prints error class, message, and first 5 backtrace lines for errored mutations. |
@@ -142,7 +142,7 @@ Every command, subcommand, and flag listed in this section is part of evilution'
142
142
  | `--related-specs-heuristic` | Boolean | false | When a mutation removes an `includes(...)` call, also run matching specs from `spec/{requests,integration,features,system}` (Rails-style domain match on the source file's basename). Trades extra spec runs for higher kill rate on ORM mutations. |
143
143
  | `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
144
144
  | `-e CODE`, `--eval CODE` | String | _(none)_ | Inline Ruby code for `util mutation` command. |
145
- | `--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. |
145
+ | `--profile NAME` | String | `default` | Operator profile: `default` or `strict`. `strict` adds aggressive mutators (e.g. replaces `x.predicate?` with `nil`, or appends `rescue nil` to a raising call) intended for pre-merge audits. |
146
146
  | `--strict` | Boolean | false | Shortcut for `--profile=strict`. |
147
147
 
148
148
  ### Options (for `session` subcommands)
@@ -165,8 +165,14 @@ Every command, subcommand, and flag listed in this section is part of evilution'
165
165
 
166
166
  Two profiles ship out of the box:
167
167
 
168
- - **`default`** — the 111 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
169
- - **`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.
168
+ - **`default`** — the 136 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
169
+ - **`strict`** — adds extra aggressive mutators on top of `default`:
170
+ - `PredicateToNil` replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values.
171
+ - `ExceptionSwallow` appends `rescue nil` to a statement that raises by convention — a bang method, `fetch` without a default, `Integer` / `Float` / `Rational` — to surface tests that never make it fail and check the error comes out (`record.save!` -> `record.save! rescue nil`). It skips Ruby core in-place bangs (`uniq!`, `sort_by!`, …), `exit!`, statements already under a rescue, and `raise`; project bangs that mutate rather than raise will still show up as survivors.
172
+
173
+ - `StatementReorder` swaps two adjacent commands (`charge(card); send_receipt(user)` -> `send_receipt(user); charge(card)`) to surface side effects whose order is never asserted. It only swaps statements that both act — call a method, append, write an index, yield, call super — and leaves alone pairs that share a variable, contain control flow or a heredoc, involve a local or instance variable assignment, write different literal keys of the same receiver, or move the last statement of a body. Expect survivors where the order only shows up in help text or in the order a list is built.
174
+
175
+ Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
170
176
 
171
177
  Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.evilution.yml`.
172
178
 
@@ -254,7 +260,7 @@ All keys recognised under `schema_version: 1`:
254
260
  | `quiet` | Boolean | `false` | Suppress output. |
255
261
  | `jobs` | Integer | `1` | Number of parallel workers. |
256
262
  | `fail_fast` | Integer / null | `null` | Stop after N surviving mutants. `null` = disabled. |
257
- | `baseline` | Boolean | `true` | Run baseline test suite to detect pre-existing failures (marked `:neutral`). |
263
+ | `baseline` | Boolean | `true` | Run baseline test suite to detect pre-existing failures (a mutation failing only on those is `:neutral`). |
258
264
  | `isolation` | String | `auto` | Isolation strategy: `auto`, `fork`, `in_process`. `auto` selects `fork` for Rails projects and packaged gems (`*.gemspec`). |
259
265
  | `incremental` | Boolean | `false` | Cache killed/timeout results across runs. |
260
266
  | `suggest_tests` | Boolean | `false` | Generate concrete test code in survivor suggestions (matches `integration`). |
@@ -321,11 +327,14 @@ Schema:
321
327
  "survived": "integer — mutations NOT detected (test passed = gap in coverage)",
322
328
  "timed_out": "integer — mutations that exceeded timeout",
323
329
  "errors": "integer — mutations that caused unexpected errors",
324
- "neutral": "integer — mutations whose tests already failed before mutation (baseline failure)",
330
+ "neutral": "integer — mutations with no verdict: their tests failed only on examples that were already failing in the baseline, or the test process crashed on infrastructure",
325
331
  "equivalent": "integer — mutations proven to have identical behavior to the original",
326
332
  "unresolved": "integer — mutations where no spec file resolved (coverage gap, not a failure)",
327
333
  "unresolved_target_files": "array of strings (optional) — target files that resolved to no spec at all; present only when non-empty, and the run fails when it is",
328
334
  "infra_retried": "integer (optional) — mutations a parallel pass could not judge because the test process crashed on infrastructure, re-run serially afterwards; present only when non-zero",
335
+ "uncovered_code": "array (optional) — targeted lines that hold code outside every subject, so no mutation was generated for them: [{ file, lines: ['2-4', '9'] }]; checked for a line range as given, and for a whole file only when it has no subject at all; present only when non-empty",
336
+ "baseline_neutralized": "integer (optional) — kills not counted because only examples already failing in the baseline failed; those mutations are recorded neutral; present only when non-zero",
337
+ "baseline_failures": "array (optional) — one entry per spec file that was red in the baseline: { spec_file, error: string|null — what stopped the file outside any example (load error, runner exception, timeout, a baseline process that died), failing_examples: integer — every failing example, examples: [{ id, description, message }] — the first few of them }; present only when non-empty",
329
338
  "unparseable": "integer — mutations whose mutated source did not parse (short-circuited, never executed)",
330
339
  "score": "float — killed / (total - errors - neutral - equivalent - unresolved - unparseable), range 0.0-1.0, rounded to 4 decimals",
331
340
  "duration": "float — total wall-clock seconds, rounded to 4 decimals",
@@ -427,7 +436,7 @@ Compatibility policy for the `1.x` gem line:
427
436
  | `survived` | No test failed — gap in coverage | denominator only |
428
437
  | `timeout` | Test run exceeded `--timeout` — treated like survived for scoring | denominator only |
429
438
  | `error` | Mutation caused an unexpected error (syntax error, boot failure, etc.) | excluded from denominator |
430
- | `neutral` | Baseline tests already failed before mutation, or the test process crashed on infrastructure (DB lock, statement timeout) rather than on the mutation. Every neutral records which of the two, and the report groups them by it | excluded |
439
+ | `neutral` | No verdict: the tests failed only on examples that were already failing in the baseline, or the test process crashed on infrastructure (DB lock, statement timeout) rather than on the mutation. Every neutral records which of the two, and the report groups them by it | excluded |
431
440
  | `equivalent` | Mutation is provably identical to the original (e.g. no-op replacement) | excluded |
432
441
  | `unresolved` | No spec file resolved for the mutated source — **coverage gap, not a failure**. Use `--fallback-full-suite` to run the full suite instead. | excluded |
433
442
  | `unparseable` | Mutated source failed to parse (e.g. dangling heredoc opener after `method_body_replacement`). Short-circuited — never executed. | excluded |
@@ -459,10 +468,15 @@ On evilution's own `lib/evilution/reporter/json/subjects.rb` this moved the repo
459
468
 
460
469
  ### Neutral Mutations
461
470
 
462
- Neutral covers two unrelated situations that want opposite responses: a spec file that was already red before any mutation ran, and a test process that died on infrastructure rather than on the mutation. Each neutral records which, and the report groups by it, naming the spec or the error class (GH #1606):
471
+ Neutral means the run has no verdict on a mutation. It covers two unrelated situations that want opposite responses, and each neutral records which; the report groups by it, naming the spec or the error class (GH #1606):
472
+
473
+ - **The tests failed, but only on examples that were already failing before any mutation ran.** Such an example fails whatever the mutation is, so its failure is not a kill. Nothing that was passing caught the mutation either, but the red example might have, once fixed.
474
+ - **The test process died on infrastructure** rather than on the mutation.
463
475
 
464
476
  ```
465
477
  Score: 100.00% (10/10 verified of 17 mutations, 7 neutral)
478
+ ! 7 kills not counted for spec/tally_spec.rb: only examples already failing in the baseline failed, so those mutations have no verdict.
479
+ ./spec/tally_spec.rb[1:3] Tally adds -- NameError: undefined local variable or method 'tally'
466
480
 
467
481
  Neutral mutations (7, not verified):
468
482
  baseline already failing (spec/tally_spec.rb):
@@ -470,9 +484,17 @@ Neutral mutations (7, not verified):
470
484
  integer_literal: lib/tally.rb:9
471
485
  ```
472
486
 
473
- The score line names the remainder whenever the run left mutations out of the denominator, because full marks over a fraction of a run otherwise reads as a verdict on all of it. A clean run still prints the plain `Score: 100.00% (17/17)`.
487
+ The score line names the remainder whenever the run left mutations out of the denominator, because full marks over a fraction of a run otherwise reads as a verdict on all of it. A clean run still prints the plain `Score: 100.00% (17/17)`. The line under it says how many kills were not counted, for which spec file, and why the baseline was red for it — the failing examples with their first error line, or the error that stopped the file before any example ran. The same detail is printed to stderr once per red spec file as soon as the baseline finishes, and is in JSON output as `summary.baseline_neutralized` and `summary.baseline_failures`.
488
+
489
+ What a red baseline does not do:
490
+
491
+ - **It does not touch a kill in which something that was passing failed too.** That is a kill like any other.
492
+ - **It does not touch survivors.** A mutation whose tests passed is a survivor whatever the baseline did — including when the baseline's failure did not happen again in the mutation run (a flaky example, a failure of the baseline process itself). Earlier versions recorded every survivor covered by a red spec file as neutral, which hid exactly those gaps.
493
+ - **It does not second-guess a baseline failure with no failing example to name** — a file that did not load, a timeout, a baseline process that died. A mutation run that hits the same problem reports `error` or `timeout` itself.
494
+
495
+ Recognising an already-failing example needs the test framework to name the examples that failed, the same way in the baseline and in a mutation run. RSpec examples are named by spec file and position, Minitest tests as `Class#test`, Test::Unit tests as `test(Class)`. Two Minitest test classes without a name that share a test method name cannot be told apart.
474
496
 
475
- Those seven mutations were survivors until the spec file went red — a neutral of this kind is a hidden coverage gap, not a clean bill of health. JSON output carries `neutral_reason` as `{ kind, detail }` on neutral entries that have one; `detail` is null where no single spec can be named (an explicit `--spec` run), and the field is absent on a result recorded without a reason, which the text report shows as `reason not recorded`.
497
+ JSON output carries `neutral_reason` as `{ kind, detail }` on neutral entries that have one; `detail` is null where no single spec can be named (an explicit `--spec` run), and the field is absent on a result recorded without a reason, which the text report shows as `reason not recorded`.
476
498
 
477
499
  ### Per-Subject Scores
478
500
 
@@ -490,7 +512,7 @@ Subjects needing attention (2 subjects in 1 file):
490
512
 
491
513
  A subject is listed when something survived, or when nothing reached it at all — zero verdicts, every mutation unresolved or neutral. Fully-killed subjects are not listed, so the section stays actionable. JSON output carries every subject under `subjects`, whether or not it needs attention, so a CI step can assert on `reached` or on a per-subject `score`.
492
514
 
493
- ## Mutation Operators (111 total)
515
+ ## Mutation Operators (136 total)
494
516
 
495
517
  Each operator name is stable and appears in JSON output under `survived[].operator`.
496
518
 
@@ -517,6 +539,7 @@ Each operator name is stable and appears in JSON output under `survived[].operat
517
539
  | `method_body_replacement` | Replace entire method body | Method body -> `nil`, `self`, `super` |
518
540
  | `negation_insertion` | Negate predicate methods | `x.empty?` -> `!x.empty?` |
519
541
  | `return_value_removal` | Strip return values | `return x` -> `return` |
542
+ | `return_keyword_removal` | Drop the `return` keyword and keep its value, so control flow continues past a guard clause or early return (several values become an array; skips returns in tail position, where the value is the result anyway, and bare `return`) | `return :neg if x.negative?` -> `:neg if x.negative?` |
520
543
  | `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
521
544
  | `collection_return` | Replace collection return values | `return [1]` -> `return []` |
522
545
  | `scalar_return` | Replace scalar return values | `return 42` -> `return 0` |
@@ -546,6 +569,27 @@ Each operator name is stable and appears in JSON output under `survived[].operat
546
569
  | `block_body_to_nil` | Replace a block body with `nil`, keeping the iteration (skips `loop` and endless `cycle` / `cycle(nil)`, which would hang) | `xs.each { \|x\| log(x) }` -> `xs.each { \|x\| nil }` |
547
570
  | `block_body_to_raise` | Replace a block body with `raise` to prove the block is invoked (skips bodies with a `rescue` clause, which would swallow it) | `xs.each { \|x\| log(x) }` -> `xs.each { \|x\| raise }` |
548
571
  | `block_body_promotion` | Replace a call with its parameter-less block body, run once (skips blocks with parameters or a `rescue` / `ensure` clause) | `Base.transaction { save! }` -> `save!`, `3.times { poll }` -> `poll` |
572
+ | `data_struct_member` | Drop a member, or swap adjacent members, of a `Data.define` / `Struct.new` definition (a definition outside any method — `Point = Data.define(...)`, `class Coord < Struct.new(...)` — is its own subject, named after the constant; skips single-member and splatted lists) | `Data.define(:a, :b)` -> `Data.define(:b)`, `Data.define(:b, :a)` |
573
+ | `pin_operator_removal` | Drop the pin of a pattern variable so it captures instead of comparing (local variables only; skips pins inside alternative patterns, where a capture is not allowed) | `in ^expected` -> `in expected` |
574
+ | `rightward_assignment` | Turn a rightward pattern match into a pattern predicate, so a mismatch returns `false` instead of raising (skips a bare capture, which cannot fail) | `value => [a, b]` -> `value in [a, b]` |
575
+ | `numbered_parameter_swap` | Swap neighbouring numbered block parameters that the body reads (skips blocks reading a single one; `it` has nothing to swap with) | `pairs.map { _1 - _2 }` -> `pairs.map { _2 - _1 }` |
576
+ | `forwarded_argument_drop` | Stop forwarding one part of the arguments a method passes through unnamed; a `...` signature is spelled out so a single part can be left out (skips a lone anonymous argument and named splats) | `g(*, **, &)` -> `g(**, &)`, `def f(...) = g(...)` -> `def f(*, **, &) = g(*, **)` |
577
+ | `pattern_wildcard_widening` | Widen a pattern so it accepts shapes it rejects: append a rest to an array pattern, wildcard or drop a hash-pattern pair, remove `**nil` (skips pairs that bind a variable and patterns that already have a rest) | `in [a, b]` -> `in [a, b, *]`, `in { age: Integer }` -> `in { age: _ }` |
578
+ | `no_matching_pattern_else` | Add an empty `else` to a `case/in` that has none, so an unmatched value yields `nil` instead of raising (skips a `case/in` with a bare capture clause, which matches everything) | `case v; in Integer then 1; end` -> `case v; in Integer then 1; else; end` |
579
+ | `argument_order_permutation` | Swap neighbouring positional arguments of a call, `super` or `yield` (keeps keywords, block and splats in place; skips identical neighbours, `raise` / `fail`, `format` / `sprintf` / `printf`, index writes, `start_with?` / `end_with?` and `Set[...]`). Calls that accept arguments in any order, such as `OptionParser#on`, can be silenced with `ignore_patterns`, e.g. `call{name=on, receiver=local_variable_read{name=opts}}` | `compute(a, b)` -> `compute(b, a)` |
580
+ | `keyword_value_swap` | Swap the values of neighbouring keyword arguments, keeping the keys (keeps positional arguments, `**splat` and block in place; skips identical values and braced hash arguments; spells out shorthand keys) | `compute(x: a, y: b)` -> `compute(x: b, y: a)` |
581
+ | `comparison_operand_swap` | Swap the operands of a spaceship comparison, reversing a sort block or a custom ordering (skips identical operands and safe navigation; `between?` / `clamp` bounds are swapped by `argument_order_permutation`) | `x.age <=> y.age` -> `y.age <=> x.age` |
582
+ | `integer_division_to_fdiv` | Turn a division into `fdiv`, so integer division keeps its remainder (groups a compound dividend; skips float literals, `/=` and the explicit `./(...)` form) | `a / b` -> `a.fdiv(b)` |
583
+ | `off_by_one_boundary` | Lower a count held in a variable by one: the receiver of `times`, the argument of `upto` / `downto` / `take` / `first` / `last` / `drop` / `each_slice` / `each_cons` (skips literal counts, which `integer_literal` shifts, and safe navigation) | `n.times` -> `(n - 1).times`, `items.first(n)` -> `items.first(n - 1)` |
584
+ | `format_specifier_swap` | Loosen one conversion specifier of a literal format string in `format` / `sprintf` / `printf` / `String#%`: drop flags, width and precision, or render a non-integer number as `%s` (skips `%%`, `%{name}`, `*` widths, `%c`, `%p` and interpolated or heredoc strings) | `format("%05d", n)` -> `format("%d", n)`, `"%.2f"` -> `"%f"`, `"%s"` |
585
+ | `alias_removal` | Drop an `alias` or `alias_method` declaration from a class, module or `class << self` body, attributed to the first method of that scope (skips aliases inside methods, `alias_method` sent to another receiver and global-variable aliases) | `alias length size` -> _(removed)_ |
586
+ | `regexp_character_type_complement` | Flip a regexp character type or word boundary to its complement, one at a time (leaves `\X`, `\R` and Unicode properties alone) | `/\d+/` -> `/\D+/`, `/\bword/` -> `/\Bword/` |
587
+ | `regexp_anchor_promotion` | Promote a regexp line anchor to a string anchor, so the pattern stops matching around newlines (`^` -> `\A`, `$` and `\Z` -> `\z`) | `/^admin$/` -> `/\Aadmin$/`, `/^admin\z/` |
588
+ | `regexp_alternation_branch_deletion` | Delete one branch of a regexp alternation at a time (skips alternations of more than ten branches, which are lookup tables, and deletions that would break a reference inside the pattern) | `/cat\|dog/` -> `/dog/`, `/cat/` |
589
+ | `regexp_quantifier_minimum_swap` | Swap a regexp quantifier between zero-or-more and one-or-more, keeping it lazy or possessive, so only the empty case changes | `/\A\d*\z/` -> `/\A\d+\z/`, `/a+?/` -> `/a*?/` |
590
+ | `regexp_capture_to_passive` | Turn a regexp capture group into a passive group, so `$1`, `m[1]` or `\1` in a replacement loses its value (skips patterns passed to `match?` / `!~`, patterns with named groups, and patterns with numbered references) | `/id=(\d+)/` -> `/id=(?:\d+)/` |
591
+ | `regexp_named_group_rename` | Rename a named regexp group, with its references inside the pattern, so `m[:name]` and the local bound by `/(?<name>…)/ =~ s` no longer find it (skips patterns passed to `match?` / `!~`) | `/(?<user>\w+)@/` -> `/(?<_user>\w+)@/` |
592
+ | `regexp_option_removal` | Drop the `i` or `m` option of a regexp literal where it changes something: `i` with a cased letter in the pattern, `m` with a `.` outside a character class | `/admin/i` -> `/admin/`, `/a.b/m` -> `/a.b/` |
549
593
  | `keyword_argument` | Remove keyword defaults/params | `def foo(bar: 42)` -> `def foo(bar:)` |
550
594
  | `multiple_assignment` | Remove targets or swap order | `a, b = 1, 2` -> `b, a = 1, 2` |
551
595
  | `block_removal` | Remove blocks from method calls | `items.map { \|x\| x * 2 }` -> `items.map` |
@@ -554,7 +598,7 @@ Each operator name is stable and appears in JSON output under `survived[].operat
554
598
  | `regexp_mutation` | Replace regexp with always/never matching | `/pat/` -> `/a\A/` |
555
599
  | `regex_simplification` | Simplify regex quantifiers, anchors, ranges | `/\d+/` -> `/\d/`, `/[a-z]/` -> `/[az]/` |
556
600
  | `receiver_replacement` | Drop explicit `self` receiver | `self.foo` -> `foo` |
557
- | `send_mutation` | Swap semantically related methods | `detect` -> `find`, `map` -> `flat_map` |
601
+ | `send_mutation` | Swap semantically related methods | `detect` -> `find`, `map` -> `flat_map`, `round` -> `floor` / `ceil` |
558
602
  | `compound_assignment` | Swap compound assignment operators | `+=` -> `-=`, `&&=` -> `\|\|=` |
559
603
  | `local_variable_assignment` | Replace variable assignment with `nil` | `x = expr` -> `x = nil` |
560
604
  | `instance_variable_write` | Replace ivar assignment with `nil` | `@x = expr` -> `@x = nil` |
@@ -564,6 +608,9 @@ Each operator name is stable and appears in JSON output under `survived[].operat
564
608
  | `superclass_removal` | Remove class inheritance | `class Foo < Bar` -> `class Foo` |
565
609
  | `rescue_removal` | Remove rescue clauses | Deletes rescue block |
566
610
  | `rescue_body_replacement` | Replace rescue body with `nil` | Rescue body -> `nil` |
611
+ | `rescue_handler_promotion` | Run a rescue handler instead of the code it protects, so the happy path never runs: each handler of a `begin` / method / block rescue in turn, and the fallback of a rescue modifier (drops `else`, keeps `ensure`; skips handlers that read the exception, re-raise or retry) | `begin; fetch(id); rescue NotFound; default; end` -> `begin; default; end`, `fetch(id) rescue default` -> `default` |
612
+ | `rescue_handler_concatenation` | Run a rescue handler after the protected code as well, keeping the rescue, so a successful run also does what the handler does (skips empty and value-only handlers, handlers that read the exception, re-raise or retry, and rescue modifiers) | `begin; fetch(id); rescue NotFound; rollback; end` -> `begin; fetch(id); rollback; rescue NotFound; rollback; end` |
613
+ | `rescue_else_concatenation` | Move the `else` body of a rescue into the protected code, so an error it raises is now rescued (only under a bare `rescue` or one naming `StandardError` / `Exception`; keeps the rescue and `ensure`) | `begin; a; rescue; b; else; c; end` -> `begin; a; c; rescue; b; end` |
567
614
  | `inline_rescue` | Remove inline rescue fallback | `expr rescue val` -> `expr` |
568
615
  | `ensure_removal` | Remove ensure blocks | Deletes ensure block |
569
616
  | `break_statement` | Remove break statements | `break` -> removed |
@@ -582,7 +629,7 @@ Each operator name is stable and appears in JSON output under `survived[].operat
582
629
  | `pattern_matching_alternative` | Remove/reorder alternatives | `pat1 \| pat2` -> `pat1` |
583
630
  | `pattern_matching_array` | Remove/wildcard array elements | `[a, b]` -> `[a, _]` |
584
631
  | `yield_statement` | Remove yield or its arguments | `yield(x)` -> `yield` |
585
- | `splat_operator` | Remove splat/double-splat | `foo(*args)` -> `foo(args)` |
632
+ | `splat_operator` | Remove splat/double-splat (skips `**` in a hash literal, `**` after a keyword argument or another `**`, and the rest of a pattern) | `foo(*args)` -> `foo(args)` |
586
633
  | `defined_check` | Replace `defined?` with `true` | `defined?(x)` -> `true` |
587
634
  | `regex_capture` | Swap or nil-ify capture refs | `$1` -> `$2`, `$1` -> `nil` |
588
635
  | `loop_flip` | Swap while/until loops | `while cond` -> `until cond` |
@@ -591,7 +638,7 @@ Each operator name is stable and appears in JSON output under `survived[].operat
591
638
  | `method_body_to_super` | Replace a method body with bare `super` where a super target exists | `def foo; body; end` -> `def foo; super; end` |
592
639
  | `typed_default_return` | Replace a single-expression body with the empty value of its inferred type | `def names(u); u.map(&:name); end` -> `def names(u); []; end` |
593
640
  | `block_parameter_drop` | Drop a block's single parameter | `users.each { |u| touch(u) }` -> `users.each { touch(u) }` |
594
- | `optional_parameter_to_required` | Drop an optional positional parameter's default | `def f(a = 1)` -> `def f(a)` |
641
+ | `optional_parameter_to_required` | Drop an optional positional parameter's default; of several, only the ones Ruby lets become required: the first, and the last unless a `*rest` parameter follows | `def f(a = 1)` -> `def f(a)` |
595
642
  | `optional_default_injection` | Overwrite an optional parameter with its own default at the top of the body | `def f(a = 1); body; end` -> `def f(a = 1); a = 1; body; end` |
596
643
  | `block_destructuring_expansion` | Flatten a destructuring group in a block's parameters | `pairs.each_with_index { |(k, v), i| use(k, v, i) }` -> `pairs.each_with_index { |k, v, i| use(k, v, i) }` |
597
644
  | `forwarding_super_to_explicit` | Give a forwarding `super` an empty argument list | `def f(a); super; end` -> `def f(a); super(); end` |
@@ -621,15 +668,15 @@ Create a `.mcp.json` file in your project root:
621
668
  "mcpServers": {
622
669
  "evilution": {
623
670
  "type": "stdio",
624
- "command": "evilution",
625
- "args": ["mcp"],
671
+ "command": "bundle",
672
+ "args": ["exec", "evilution", "mcp"],
626
673
  "env": {}
627
674
  }
628
675
  }
629
676
  }
630
677
  ```
631
678
 
632
- If using Bundler, set the command to `bundle` and args to `["exec", "evilution", "mcp"]`.
679
+ Run the server through `bundle exec` so the project's `Gemfile.lock` decides which versions of evilution, your test framework and their shared dependencies get loaded. Started outside Bundler, RubyGems picks the newest installed copy of each gem, which your test framework may refuse — the run then stops at the proof-of-life canary with a gem activation conflict. Projects without a Gemfile can use `"command": "evilution"` and `"args": ["mcp"]`.
633
680
 
634
681
  The server exposes the following tools:
635
682
 
@@ -637,7 +684,7 @@ The server exposes the following tools:
637
684
  |---|---|
638
685
  | `evilution-mutate` | Run mutation testing on target files with structured JSON results |
639
686
  | `evilution-session` | Inspect mutation testing history — `action: list` browses saved sessions, `action: show` displays one, `action: diff` compares two (fixed/new/persistent survivors, score delta) |
640
- | `evilution-info` | Discovery before mutation — `action: subjects` lists mutatable methods with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config, `action: statuses` returns the mutation-result status glossary, `action: feedback` returns the public Discussions URL plus consent + privacy guidance for posting feedback |
687
+ | `evilution-info` | Discovery before mutation — `action: subjects` lists the mutation subjects (methods, and class-body subjects such as scopes and callback declarations) with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config, `action: statuses` returns the mutation-result status glossary, `action: feedback` returns the public Discussions URL plus consent + privacy guidance for posting feedback |
641
688
 
642
689
  ### Verbosity Control
643
690
 
@@ -655,7 +702,7 @@ What survives trimming matters when you are deciding whether to trust a score:
655
702
 
656
703
  - `neutral` entries — and with them each `neutral_reason` — are dropped at `summary` and `minimal`. Use `full` to see why mutations were neutralised.
657
704
  - `subjects` is kept at `full` and `summary`, and dropped at `minimal`, which keeps only `summary` and `survived`.
658
- - Everything inside `summary` survives at every level, including `unresolved_target_files`, `infra_retried` and the `neutral` count — so even a `minimal` response still says whether a target file went untested and how much of the run the score covers.
705
+ - Everything inside `summary` survives at every level, including `unresolved_target_files`, `uncovered_code`, `infra_retried`, `baseline_neutralized`, `baseline_failures` and the `neutral` count — so even a `minimal` response still says whether a target file went untested and how much of the run the score covers.
659
706
 
660
707
  ### Enriched Survived Entries
661
708
 
@@ -804,6 +851,14 @@ Methods whose body overlaps the requested range are included. Mix targeted and w
804
851
  evilution run lib/foo.rb:15-30 lib/bar.rb --format json
805
852
  ```
806
853
 
854
+ A range is matched against subjects, and not every line belongs to one. Methods are subjects, and so are scopes, AASM guards and callbacks, callback and validation declarations, and `Data.define` / `Struct.new` definitions (see step 2 under [Internals](#internals-for-context-not-for-direct-use)). Other class-body code — a constant list, a serializer attribute, a DSL call evilution does not know — is not. When the range holds such code, the run says so on stderr and in `summary.uncovered_code`:
855
+
856
+ ```
857
+ [evilution] app/models/order.rb:12-14 holds code outside every subject (class-body code such as DSL calls and constants is not mutated); no mutations target those lines.
858
+ ```
859
+
860
+ Read `0 mutations` next to that message as "not measured", not as "nothing to test".
861
+
807
862
  ### 4. Method-name targeted scan
808
863
 
809
864
  ```bash
@@ -830,13 +885,14 @@ Pass multiple file paths on a single invocation to amortise startup cost. The fr
830
885
 
831
886
  ### What to read before acting on a score
832
887
 
833
- A score describes only the mutations that got a verdict. Four fields say what it leaves out, and each points at a different action:
888
+ A score describes only the mutations that got a verdict. Five fields say what it leaves out, and each points at a different action:
834
889
 
835
890
  | Field | Meaning | What to do |
836
891
  |---|---|---|
837
892
  | `summary.unresolved_target_files` | A file you named resolved to no spec and was never tested; the run fails on this alone | Write a spec, pass `--spec`, or map it in `spec_mappings` — do not trust the score until this is empty |
838
893
  | `subjects[].reached == false` | Mutations were generated for that method but none got a verdict | The method is untested even where its file scores well; start here rather than with `survived[]` |
839
- | `neutral[].neutral_reason.kind == "baseline_failure"` | The spec file was already red before any mutation ran; `detail` names it | Fix that spec first — nothing about these mutations is measurable until it is green |
894
+ | `summary.uncovered_code` | Lines you targeted hold code outside every subject — class-body code such as constant lists or DSL calls evilution does not treat as a subject — so no mutation was generated for them; the same is printed to stderr | A run that reports `0 mutations` for those lines has not shown they are tested. Target the methods that code calls, or cover it with ordinary tests |
895
+ | `neutral[].neutral_reason.kind == "baseline_failure"` | The mutation's tests failed only on examples that were already failing before any mutation ran; `detail` names the red spec file | Read `summary.baseline_failures` for why it was red, and fix that first — these mutations have no verdict until it is green |
840
896
  | `neutral[].neutral_reason.kind == "infra_error"` | The test process crashed on infrastructure (DB lock, timeout); `detail` names the class | Not a coverage gap. Give parallel workers their own database, or run `-j 1` |
841
897
 
842
898
  `summary.infra_retried` reports how many mutations had to be re-run serially because of the last case; a large number means the parallel run was fighting shared infrastructure rather than measuring your suite.
@@ -1002,9 +1058,9 @@ For the full contributor architecture — module map, data flow, and extension
1002
1058
  points — see [docs/architecture.md](docs/architecture.md).
1003
1059
 
1004
1060
  1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
1005
- 2. **Extract** — Methods are identified as mutation subjects
1061
+ 2. **Extract** — Methods are identified as mutation subjects, and so are value-object definitions outside any method (`Point = Data.define(:x, :y)`, `class Coord < Struct.new(:lat, :lng)`), which only the operators that apply to them mutate, and ActiveRecord scopes declared with a literal body in a class body (`scope :recent, -> { where(recent: true) }`), named after the class method they define (`Order.recent`) and mutated like a method body — also when declared in a concern's `included do ... end` block, where they are named after the concern (`Publishable.published`); likewise the guards and callbacks written out as lambdas or blocks inside an AASM `event` or `state` declaration (`event :ship, guard: -> { address? }`), named after the method that declaration defines (`Order#ship`, `Order#paid?`), or after the concern when the machine is declared in its `included do ... end` block (`Shippable#ship`); and the conditions and bodies written out in callback and validation declarations of a class body or of a concern's `included do ... end` block (`validate :credit_limit, if: -> { paid? }`, `before_save { ... }`), named the way the declaration reads (`Order.validate(:credit_limit)`, `Order.before_save`, `Publishable.validates(:title)`)
1006
1062
  3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
1007
- 4. **Mutate** — 111 operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing); heredoc literal text is skipped by default. Identical byte-mutations from different operators are deduplicated by `(file_path, mutated_source)` so the count is not inflated by overlap
1063
+ 4. **Mutate** — 136 operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing); heredoc literal text is skipped by default. Identical byte-mutations from different operators are deduplicated by `(file_path, mutated_source)` so the count is not inflated by overlap
1008
1064
  5. **Isolate** — Mutations are applied to temporary file copies (never modifying originals); load-path redirection ensures `require` resolves the mutated copy. Default isolation is in-process for plain Ruby projects (no gemspec) and fork for Rails projects and packaged gems (auto-detected); `--isolation fork` forces forked child processes. Both sequential and parallel (`--jobs N`) modes respect the configured isolation strategy
1009
1065
  6. **Test** — The configured test framework (RSpec, Minitest, or Test::Unit) executes against the mutated source
1010
1066
  7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention