evilution 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +70 -0
  3. data/CHANGELOG.md +50 -0
  4. data/README.md +64 -22
  5. data/docs/architecture.md +56 -12
  6. data/docs/isolation.md +82 -0
  7. data/lib/evilution/ast/class_body.rb +101 -0
  8. data/lib/evilution/ast/included_block.rb +20 -6
  9. data/lib/evilution/ast/never_nil.rb +36 -0
  10. data/lib/evilution/ast/parser.rb +94 -31
  11. data/lib/evilution/ast/round_half_mode.rb +38 -0
  12. data/lib/evilution/ast/source_surgeon.rb +2 -3
  13. data/lib/evilution/ast/syntax_check.rb +56 -0
  14. data/lib/evilution/ast/uncovered_code.rb +7 -7
  15. data/lib/evilution/cli/exit_guard.rb +26 -4
  16. data/lib/evilution/integration/base.rb +10 -2
  17. data/lib/evilution/integration/loading/body_call_neutralizer.rb +59 -14
  18. data/lib/evilution/integration/loading/concern_redeclaration.rb +33 -14
  19. data/lib/evilution/integration/loading/concern_state_cleaner.rb +3 -3
  20. data/lib/evilution/integration/loading/mutation_applier.rb +33 -6
  21. data/lib/evilution/integration/loading/own_methods.rb +87 -0
  22. data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
  23. data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
  24. data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
  25. data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
  26. data/lib/evilution/isolation/fork.rb +82 -10
  27. data/lib/evilution/isolation/in_process.rb +79 -4
  28. data/lib/evilution/mcp/info_tool/status_glossary.rb +13 -2
  29. data/lib/evilution/mcp/info_tool.rb +3 -2
  30. data/lib/evilution/mutation.rb +18 -11
  31. data/lib/evilution/mutator/base.rb +27 -7
  32. data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
  33. data/lib/evilution/mutator/operator/alias_removal.rb +11 -81
  34. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
  35. data/lib/evilution/mutator/operator/argument_removal.rb +2 -2
  36. data/lib/evilution/mutator/operator/array_literal.rb +29 -0
  37. data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
  38. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
  39. data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
  40. data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
  41. data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
  42. data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
  43. data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
  44. data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
  45. data/lib/evilution/mutator/operator/data_struct_member.rb +1 -1
  46. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
  47. data/lib/evilution/mutator/operator/float_literal.rb +54 -19
  48. data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
  49. data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
  50. data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
  51. data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
  52. data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
  53. data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
  54. data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
  55. data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
  56. data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
  57. data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
  58. data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
  59. data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
  60. data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
  61. data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
  62. data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
  63. data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
  64. data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
  65. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
  66. data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
  67. data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
  68. data/lib/evilution/mutator/operator/splat_operator.rb +16 -24
  69. data/lib/evilution/mutator/operator/string_literal.rb +29 -0
  70. data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
  71. data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
  72. data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
  73. data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
  74. data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
  75. data/lib/evilution/mutator/primitives.rb +24 -2
  76. data/lib/evilution/mutator/receiver_positions.rb +79 -0
  77. data/lib/evilution/mutator/registry.rb +29 -2
  78. data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
  79. data/lib/evilution/parallel/work_queue/channel.rb +58 -5
  80. data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
  81. data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
  82. data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
  83. data/lib/evilution/parallel/work_queue/worker.rb +13 -4
  84. data/lib/evilution/parallel/work_queue.rb +15 -9
  85. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
  86. data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
  87. data/lib/evilution/runner/subject_pipeline.rb +20 -51
  88. data/lib/evilution/runner.rb +1 -1
  89. data/lib/evilution/subject.rb +18 -1
  90. data/lib/evilution/version.rb +1 -1
  91. data/lib/evilution.rb +62 -0
  92. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12d6c2e2e3a5da3abf68a795d1537e103f3156c52df6709fd7543968efca73a9
4
- data.tar.gz: c072227e0cb2d7ea933ad818853e51adbd21c90268308ce527a8670b8fdbfc23
3
+ metadata.gz: 5f47321127e4fb0090ec208b430b7c14da3ca3b9b16e883f076b9707c787744a
4
+ data.tar.gz: 78951f4619c9f8911e94272734b74606df0633131741201c7056c3a3dd1977dd
5
5
  SHA512:
6
- metadata.gz: a90a01e42b93bc3409e8aa730df657eba157510078a338b87f96c255bbe89b5a01d74c8dd1561880c7b094c9c32fc50c22a78cd78b2e66537b7996db49de206e
7
- data.tar.gz: 8c886d18f0c2acecc1c9673f540823860d3e40e41937ce1477f4f52578b549dade9c0e2ce019a94c4249bdc81d9cfaf7c9ffcc30381b8a5d7d4788844b3f6c2b
6
+ metadata.gz: 57ec220fad323e96e3bb3a4cb75b58dac381018d1b22334b4eb27b58c43db6219f732df58387eb41328332c90fb82d023829ac7df5632748223ada9aac048e26
7
+ data.tar.gz: 5960714fac3df7b1beb118043e899961a4069dad090887e36727cda63d0b81eda52661ec9cb8745df7def51481e39aad9340673c9ec255fa59c1d32f506be3a9
@@ -589,3 +589,73 @@
589
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
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
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"}}
592
+ {"id":"int-a14e860c","kind":"field_change","created_at":"2026-10-08T08:20:24.543215213Z","actor":"Denis Kiselev","issue_id":"EV-u9rq","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1784"}}
593
+ {"id":"int-bf8b9522","kind":"field_change","created_at":"2026-10-08T08:52:17.811825439Z","actor":"Denis Kiselev","issue_id":"EV-qt7f","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
594
+ {"id":"int-14d78256","kind":"field_change","created_at":"2026-10-08T10:27:35.588313336Z","actor":"Denis Kiselev","issue_id":"EV-5pr3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1786"}}
595
+ {"id":"int-d497edf6","kind":"field_change","created_at":"2026-10-08T10:49:38.951908776Z","actor":"Denis Kiselev","issue_id":"EV-d6nh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1782"}}
596
+ {"id":"int-a89400aa","kind":"field_change","created_at":"2026-10-08T10:49:39.784886076Z","actor":"Denis Kiselev","issue_id":"EV-16yn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1787"}}
597
+ {"id":"int-063a91c3","kind":"field_change","created_at":"2026-10-08T14:30:58.962802382Z","actor":"Denis Kiselev","issue_id":"EV-44o7.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1788"}}
598
+ {"id":"int-ce1db8e8","kind":"field_change","created_at":"2026-10-08T15:22:23.92716218Z","actor":"Denis Kiselev","issue_id":"EV-44o7.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1789"}}
599
+ {"id":"int-f2edc8c0","kind":"field_change","created_at":"2026-10-08T15:50:38.509740051Z","actor":"Denis Kiselev","issue_id":"EV-44o7.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
600
+ {"id":"int-4532acdd","kind":"field_change","created_at":"2026-10-08T16:05:32.697825805Z","actor":"Denis Kiselev","issue_id":"EV-44o7.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
601
+ {"id":"int-3faeb9f1","kind":"field_change","created_at":"2026-10-08T16:24:22.591450952Z","actor":"Denis Kiselev","issue_id":"EV-44o7.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
602
+ {"id":"int-19e1698e","kind":"field_change","created_at":"2026-10-09T01:14:41.828480992Z","actor":"Denis Kiselev","issue_id":"EV-44o7.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1793"}}
603
+ {"id":"int-039ad18d","kind":"field_change","created_at":"2026-10-09T01:36:15.256754769Z","actor":"Denis Kiselev","issue_id":"EV-44o7.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
604
+ {"id":"int-51ef3062","kind":"field_change","created_at":"2026-10-09T04:48:31.278559024Z","actor":"Denis Kiselev","issue_id":"EV-44o7.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
605
+ {"id":"int-ed3b7e74","kind":"field_change","created_at":"2026-10-09T05:54:31.526895048Z","actor":"Denis Kiselev","issue_id":"EV-44o7.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
606
+ {"id":"int-9874144e","kind":"field_change","created_at":"2026-10-09T06:26:03.337252742Z","actor":"Denis Kiselev","issue_id":"EV-44o7.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1797"}}
607
+ {"id":"int-aeb0703e","kind":"field_change","created_at":"2026-10-09T15:13:18.21752595Z","actor":"Denis Kiselev","issue_id":"EV-44o7.11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1798"}}
608
+ {"id":"int-332bf695","kind":"field_change","created_at":"2026-10-09T15:32:32.76074471Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
609
+ {"id":"int-8335751c","kind":"field_change","created_at":"2026-10-09T16:48:20.829398546Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
610
+ {"id":"int-077fb2db","kind":"field_change","created_at":"2026-10-09T17:25:15.331671484Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1801"}}
611
+ {"id":"int-6e37d720","kind":"field_change","created_at":"2026-10-10T02:39:49.114281637Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1802"}}
612
+ {"id":"int-05630371","kind":"field_change","created_at":"2026-10-10T02:42:37.400418751Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Not built: subsumed by conditional_negation (see notes)"}}
613
+ {"id":"int-ec1c86a5","kind":"field_change","created_at":"2026-10-10T03:11:41.589463361Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Not built: covered by argument_removal and argument_list_removal (see notes)"}}
614
+ {"id":"int-8e02e146","kind":"field_change","created_at":"2026-10-10T03:43:33.223836416Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1803"}}
615
+ {"id":"int-0e6a6164","kind":"field_change","created_at":"2026-10-10T04:15:43.08219523Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1804"}}
616
+ {"id":"int-65f057dd","kind":"field_change","created_at":"2026-10-10T04:39:55.8764372Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
617
+ {"id":"int-03c6c7d4","kind":"field_change","created_at":"2026-10-10T05:29:16.915905317Z","actor":"Denis Kiselev","issue_id":"EV-2mcz.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
618
+ {"id":"int-2ae5b260","kind":"field_change","created_at":"2026-10-10T06:06:58.238154954Z","actor":"Denis Kiselev","issue_id":"EV-09tf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1807"}}
619
+ {"id":"int-4242cd56","kind":"field_change","created_at":"2026-10-10T06:07:20.049330881Z","actor":"Denis Kiselev","issue_id":"EV-qbli","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Already resolved by PR #1752 (see notes)"}}
620
+ {"id":"int-1fe4ff62","kind":"field_change","created_at":"2026-10-10T06:39:22.070064099Z","actor":"Denis Kiselev","issue_id":"EV-pdf2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
621
+ {"id":"int-f5478aa6","kind":"field_change","created_at":"2026-10-10T08:43:08.630764823Z","actor":"Denis Kiselev","issue_id":"EV-pb28","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
622
+ {"id":"int-3bb8846f","kind":"field_change","created_at":"2026-10-10T17:02:23.92423546Z","actor":"Denis Kiselev","issue_id":"EV-mmnr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
623
+ {"id":"int-0a46b82d","kind":"field_change","created_at":"2026-10-10T17:19:28.950843549Z","actor":"Denis Kiselev","issue_id":"EV-z62t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
624
+ {"id":"int-5ffd7f55","kind":"field_change","created_at":"2026-10-10T18:13:32.786243533Z","actor":"Denis Kiselev","issue_id":"EV-r671","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged in PR #1813"}}
625
+ {"id":"int-05b35017","kind":"field_change","created_at":"2026-10-11T02:08:31.809173892Z","actor":"Denis Kiselev","issue_id":"EV-du3x.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
626
+ {"id":"int-0bdb6c53","kind":"field_change","created_at":"2026-10-11T02:30:52.265602529Z","actor":"Denis Kiselev","issue_id":"EV-du3x.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
627
+ {"id":"int-ebf31a7d","kind":"field_change","created_at":"2026-10-11T02:54:50.995980383Z","actor":"Denis Kiselev","issue_id":"EV-du3x.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
628
+ {"id":"int-9201eec7","kind":"field_change","created_at":"2026-10-11T03:20:24.150429118Z","actor":"Denis Kiselev","issue_id":"EV-du3x.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
629
+ {"id":"int-1615cdd7","kind":"field_change","created_at":"2026-10-11T03:35:40.33005889Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.3","extra":{"field":"status","new_value":"open","old_value":"in_progress"}}
630
+ {"id":"int-dc67c789","kind":"field_change","created_at":"2026-10-11T03:35:40.330186872Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.3","extra":{"field":"assignee","new_value":"","old_value":"Denis Kiselev"}}
631
+ {"id":"int-a2eb413a","kind":"field_change","created_at":"2026-10-11T03:37:08.729446267Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
632
+ {"id":"int-575b9a13","kind":"field_change","created_at":"2026-10-11T04:05:34.244398732Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
633
+ {"id":"int-8b79c268","kind":"field_change","created_at":"2026-10-11T05:05:14.944704585Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.13","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
634
+ {"id":"int-362d232a","kind":"field_change","created_at":"2026-10-11T06:06:19.598640958Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
635
+ {"id":"int-6131204f","kind":"field_change","created_at":"2026-10-11T06:40:39.507468072Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Dropped: same signal as constant_write_to_nil, see notes"}}
636
+ {"id":"int-31b475dc","kind":"field_change","created_at":"2026-10-11T06:40:41.935593905Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.5","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Dropped: same signal as constant_write_to_nil, see notes"}}
637
+ {"id":"int-68909b2c","kind":"field_change","created_at":"2026-10-11T07:21:23.395993815Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
638
+ {"id":"int-16097dd9","kind":"field_change","created_at":"2026-10-11T07:34:24.726032091Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
639
+ {"id":"int-0be9025f","kind":"field_change","created_at":"2026-10-11T09:03:21.790906842Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
640
+ {"id":"int-3f1237f4","kind":"field_change","created_at":"2026-10-11T14:51:19.252166741Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
641
+ {"id":"int-3de7b591","kind":"field_change","created_at":"2026-10-11T15:04:24.700626826Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
642
+ {"id":"int-8be80121","kind":"field_change","created_at":"2026-10-11T15:25:09.306475739Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
643
+ {"id":"int-ba0e15a2","kind":"field_change","created_at":"2026-10-11T16:00:28.519385107Z","actor":"Denis Kiselev","issue_id":"EV-8v7d.12","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
644
+ {"id":"int-6d7bd577","kind":"field_change","created_at":"2026-10-11T16:59:42.035608579Z","actor":"Denis Kiselev","issue_id":"EV-ygg9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
645
+ {"id":"int-547fecfe","kind":"field_change","created_at":"2026-10-11T17:22:19.47687736Z","actor":"Denis Kiselev","issue_id":"EV-ye3m","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
646
+ {"id":"int-bcd11e26","kind":"field_change","created_at":"2026-10-12T02:41:08.259992159Z","actor":"Denis Kiselev","issue_id":"EV-g6hd","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
647
+ {"id":"int-f8ca392d","kind":"field_change","created_at":"2026-10-12T03:01:03.096649962Z","actor":"Denis Kiselev","issue_id":"EV-xl1j","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
648
+ {"id":"int-de6c4d1e","kind":"field_change","created_at":"2026-10-12T06:59:29.699566039Z","actor":"Denis Kiselev","issue_id":"EV-m78s","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
649
+ {"id":"int-ed6de844","kind":"field_change","created_at":"2026-10-12T08:28:48.148716841Z","actor":"Denis Kiselev","issue_id":"EV-yub9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
650
+ {"id":"int-1f781f0a","kind":"field_change","created_at":"2026-10-12T10:31:54.974975403Z","actor":"Denis Kiselev","issue_id":"EV-ubcq","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
651
+ {"id":"int-0dfc1b96","kind":"field_change","created_at":"2026-10-12T10:54:10.977752113Z","actor":"Denis Kiselev","issue_id":"EV-ri8j","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
652
+ {"id":"int-b79f62f6","kind":"field_change","created_at":"2026-10-12T11:14:05.558635903Z","actor":"Denis Kiselev","issue_id":"EV-wf52","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
653
+ {"id":"int-5c475ad8","kind":"field_change","created_at":"2026-10-12T12:55:34.339718031Z","actor":"Denis Kiselev","issue_id":"EV-j1jw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
654
+ {"id":"int-3b9be5e5","kind":"field_change","created_at":"2026-10-13T03:07:40.88128569Z","actor":"Denis Kiselev","issue_id":"EV-2gf0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
655
+ {"id":"int-c15e7ffc","kind":"field_change","created_at":"2026-10-13T05:08:40.172189792Z","actor":"Denis Kiselev","issue_id":"EV-04b1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
656
+ {"id":"int-3e12caeb","kind":"field_change","created_at":"2026-10-13T05:39:14.357741483Z","actor":"Denis Kiselev","issue_id":"EV-2nj3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
657
+ {"id":"int-ec3489ad","kind":"field_change","created_at":"2026-10-13T07:48:03.231041662Z","actor":"Denis Kiselev","issue_id":"EV-i2qf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
658
+ {"id":"int-4ebb6cc6","kind":"field_change","created_at":"2026-10-13T10:22:13.338137009Z","actor":"Denis Kiselev","issue_id":"EV-70w5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
659
+ {"id":"int-e24299f1","kind":"field_change","created_at":"2026-10-13T17:20:30.564416401Z","actor":"Denis Kiselev","issue_id":"EV-x38e","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
660
+ {"id":"int-2efcd224","kind":"field_change","created_at":"2026-10-14T01:56:27.118884805Z","actor":"Denis Kiselev","issue_id":"EV-9zfe","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
661
+ {"id":"int-5a397b18","kind":"field_change","created_at":"2026-10-14T06:28:38.598622816Z","actor":"Denis Kiselev","issue_id":"EV-23dn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"README update merged in PR #1867 (GH #1864)"}}
data/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  Versioning policy: see [docs/versioning.md](docs/versioning.md).
4
4
 
5
+ ## [1.5.0] - 2026-10-14
6
+
7
+ The `default` profile grows from 136 to 155 operators, `strict` gains one more, and several existing operators emit more mutants than before: literals, collections, constants, indexes, variable reads and compound writes are all probed more closely. Constant assignments outside a method are subjects now, as class-body code was in 1.4.0. Mutation scores will move: every new mutant is one 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
+ The other half of the release is about trusting a verdict. Several mutants were counted as kills they had not earned, or survived whatever the tests did; several ways a test process can die now give a verdict instead of an error or a lost run. See "Fixed". Most of these came from the stability runs on real-world repositories before this release.
10
+
11
+ ### Added
12
+
13
+ - **Constant assignments are subjects** — `LIMIT = 10`, `Config::LIMIT = 10` and `TOTAL += 1` outside any method are subjects of their own, named after the constant (`Config::LIMIT`); a value-object definition (`Data.define` / `Struct.new`) stays the subject it was. The literal operators mutate the assigned value, `constant_write_to_nil` the assignment, `compound_assignment` an operator write. Code that read the constant while the file loaded keeps the old value (PRs #1822, #1823, #1827; GH #1820, #1509, #1515)
14
+ - **A concern's `prepended do ... end` block is treated like `included do ... end`** — scopes, AASM guards and callbacks, and callback and validation declarations written in it are subjects, named after the concern (PR #1851, GH #1846)
15
+ - **Nineteen operators in the `default` profile**:
16
+ - **Literals** — `complex_literal` (`5i` -> `0i`, `1i`, `6i`, `4i`, `nil`) and `rational_literal` (`1.5r` -> `0r`, `1r`, `2.5r`, `0.5r`, `nil`) (PRs #1791, #1792; GH #1499, #1500)
17
+ - **Constants** (epic GH #1424) — `constant_read_to_nil` (`Config::MAX` -> `nil`), `constant_namespace_strip` (`A::B::C` -> `C`, `B::C`), `constant_write_to_nil` (`LIMIT = 10` -> `LIMIT = nil`) (PRs #1818, #1819, #1823; GH #1507, #1508, #1509)
18
+ - **Variable reads** — `variable_read_to_nil`: a read of a local, instance, class or global variable, or `self`, becomes `nil` (`build(name: @name)` -> `build(name: nil)`). It skips call receivers, `if` / `unless` / `while` / `until` conditions and the left operand of `&&` / `||` (PR #1824, GH #1512)
19
+ - **Indexes** (epic GH #1422) — `index_receiver_to_self` (`h[k]` -> `self[k]`), `index_to_key_predicate` (`h[k]` -> `h.key?(k)`), `index_range_to_drop` (`a[n..-1]` -> `a.drop(n)`), `index_write_to_value` (`h[k] = v` -> `v`) (PRs #1814–#1817; GH #1490, #1491, #1492, #1495)
20
+ - **`operator_write_receiver_promotion`** — `@account.balance += x` -> `@account += x` (PR #1830, GH #1518)
21
+ - **Ruby idioms** (epic GH #1427) — `memoization_break` (`@x ||= expr` -> `@x = expr`), `short_circuit_to_eager` (`a && b` -> `a & b`), `freeze_removal` (`LIST = %w[a b].freeze` -> `LIST = %w[a b]`), `rescue_class_widening` (`rescue KeyError` -> `rescue StandardError`, `rescue Exception`), `visibility_removal` (drops `private` / `protected` / `module_function`), `attr_accessor_swap` (`attr_reader :name` -> `attr_accessor :name`, or removed), `sleep_to_zero` (`sleep delay` -> `sleep 0`) (PRs #1799, #1800, #1802–#1806; GH #1532, #1533, #1535, #1538–#1541)
22
+ - **`round_half_mode_swap`** — `amount.round(2, half: :even)` -> `half: :up`, `half: :down`: a survivor means no example rounds a value that sits on a tie. `symbol_literal` leaves these modes alone (PR #1785, GH #1711)
23
+ - **`safe_navigation_insertion` in the `strict` profile** — `user.name` -> `user&.name`, to surface calls no test reaches with a nil receiver. It touches nearly every call with a receiver, hence `strict` (PR #1801, GH #1534)
24
+ - **More mutants from existing operators**:
25
+ - **`integer_literal`** — `n - 1` beside `n + 1` (and `-1` for `0`), plus one sentinel past the next integer width above the literal (`42` -> `167`, int8 to int64) to surface missing overflow checks (PRs #1788, #1789; GH #1496, #1497)
26
+ - **`float_literal`** — `Float::NAN`, `Float::INFINITY` and `-Float::INFINITY` (PR #1790, GH #1498)
27
+ - **`array_literal`** — deletes each element in turn, and replaces a single-element array with its element (`[x]` -> `x`) (PRs #1793, #1794; GH #1501, #1502)
28
+ - **`hash_literal`** — deletes each pair in turn, and renames each label key in turn (PRs #1795, #1796; GH #1503, #1504)
29
+ - **`string_literal` / `symbol_literal`** — an interpolated string or symbol is replaced as a whole (`"a #{x}"` -> `""`, `nil`); a backtick or `%x` command is replaced with `nil` (PRs #1797, #1798; GH #1505, #1506)
30
+ - **`compound_assignment`** — operator and logical writes on attributes (`obj.count += 1`, `obj.name ||= x`), on indexes (`counts[key] += 1`, `cache[key] ||= x`) and operator writes on constants, where only plain variables were mutated before (PRs #1825–#1829; GH #1513–#1517)
31
+
32
+ ### Changed
33
+
34
+ - **A mutant that makes its own file fail to load is `killed`** — `OPERATORS = %i[+ -].to_set` with the array turned into `nil` raises while the file loads, before any test runs. When the original file loads, that is a kill (`error_message` starts with `load failed:`); when the original fails the same way, it stays an `error` (PR #1845, GH #1834)
35
+ - **A test process that dies on an unhandled exception gives a verdict** — under fork isolation a child that died on an exception no handler saw was an `error` reading `child exited 1 without writing a result`. While the tests ran it is now `killed`, as a crashing test would be (`error_message` starts with `child died` and names the exception); before they ran it is an `error` carrying the exception's own class (PR #1855, GH #1852)
36
+ - **Under `in_process`, an exception outside `StandardError` is contained** — a `SystemStackError` from a mutant that recurses without end is reported for that mutation (`killed`, `error_message` starting with `test run ended by`, or `error` when raised before the tests) and the run goes on (PR #1857, GH #1853)
37
+ - **Exit status 2 for an `in_process` run that a mutation took down** — when a mutation ends the process in a way that cannot be contained (a stack overflow through `const_missing` under the usual stack limit), stderr names the mutation and suggests `--isolation fork`, and the run ends with 2, the status of a tool error. It ended with Ruby's 1, which reads as a score below `--min-score`, although no report was written. A VM abort (`[BUG]`) still ends as an abort does. See [docs/isolation.md](docs/isolation.md#what-in_process-cannot-contain) (PRs #1857, #1866; GH #1853, #1863)
38
+ - **Class-body operators share one rule for which method a mutant belongs to** — `mixin_removal`, `superclass_removal`, `data_struct_member` and `alias_removal` each had their own copy and disagreed. Now the first method of the scope in source order, searching body blocks such as `included do ... end`; a class whose methods all sit in `class << self` is reached through them, where it got no class-body mutants before (PR #1784, GH #1718)
39
+
40
+ ### Fixed
41
+
42
+ - **In-process isolation no longer kills mutants that fork isolation reports as survivors** — a file that installs an anonymous module on its host (`Object.extend(Module.new { def const_missing ... })`) left one more module behind each time it was evaluated, so a mutation reached every earlier mutation's code through `super` and was reported killed for a predecessor's bug. The methods of those modules are now removed when the file is evaluated again (PR #1860, GH #1856)
43
+ - **A constant mutation no longer outlives its file under `in_process`** — the original source is put back after every mutation, so `LIMIT = nil` from a file's last mutation cannot fail the tests of the next file's mutants and count as their kill (PR #1841, GH #1832)
44
+ - **Mutations of a concern with an `included` block are no longer falsely killed under fork** — re-evaluating the mutated file blanked the `included do ... end` block, so a class including the concern during the example got none of its declarations and crashed whatever the mutant did. The block is kept; the same for `prepended` (PRs #1842, #1847; GH #1833, #1843)
45
+ - **Mutations of a method defined in a concern's `included` or `prepended` block reach classes that took the concern before the run** — they survived every time, since those classes kept the original method (PR #1850, GH #1848)
46
+ - **A result that cannot be read no longer aborts the run** — under fork isolation, bytes on the result pipe that were not a result raised in the parent and the run ended without a report; the mutation is now an `error` reading `child exited 0; its result was unreadable (...)`. Under `--jobs`, a frame a worker did not write, or one cut short (ten seconds are allowed for the rest), replaces that worker and records `worker's result could not be read; the worker was replaced`, where it raised, hung or was taken for a result (PRs #1859, #1861; GH #1854, #1858)
47
+ - **No more mutants that only Prism accepts** — on an interpreter that compiles with parse.y (Ruby 3.3 by default), a mutant such as `def f(v); LIMIT ||= v; end` was marked parseable and then raised `SyntaxError` when evaluated, landing in `errors`. It is now `unparseable` there (PR #1839, GH #1821)
48
+ - **The to-nil operators leave the receiver of a definition or a compound write alone** — `def self.build` -> `def nil.build` survived whatever the tests did, one false survivor per `def self.x`; `counter.total += 1` -> `nil.total += 1` was killed by any test reaching the line (PRs #1837, #1840; GH #1831, #1838)
49
+ - **A rooted value-object definition inside a scope is named after the top level** — `module Outer; ::Point = Data.define(...)` gave `Outer::Point`, so `--target Point` missed it (PR #1836, GH #1835)
50
+
51
+ ### Documentation
52
+
53
+ - **README: how to read a verdict when a test process died, and the `error` messages that are no verdict at all** — the statuses table, a new table under "Diagnosing errored mutations", and what `--isolation in_process` cannot contain (PR #1867, GH #1864)
54
+
5
55
  ## [1.4.0] - 2026-10-07
6
56
 
7
57
  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.
data/README.md CHANGED
@@ -132,7 +132,7 @@ Every command, subcommand, and flag listed in this section is part of evilution'
132
132
  | `--no-progress` | Boolean | _(enabled)_ | Disable the TTY progress bar. |
133
133
  | `--quiet-children` | Boolean | false | Redirect each forked worker's stdout/stderr to per-pid files under `tmp/evilution_children/<pid>.{out,err}` so noisy app initializers (Datadog, Bullet, etc.) don't merge with parent output. Trade-off: live worker errors only appear in the side files, not the terminal — `tail -f tmp/evilution_children/*.err` to watch them. |
134
134
  | `--quiet-children-dir DIR` | String | `tmp/evilution_children` | Override the directory used by `--quiet-children`. |
135
- | `--isolation MODE` | String | `auto` | Isolation strategy: `auto`, `fork`, or `in_process`. `auto` selects `fork` for Rails projects and packaged gems (`*.gemspec`), `in_process` otherwise. See [docs/isolation.md](docs/isolation.md). |
135
+ | `--isolation MODE` | String | `auto` | Isolation strategy: `auto`, `fork`, or `in_process`. `auto` selects `fork` for Rails projects and packaged gems (`*.gemspec`), `in_process` otherwise. Under `in_process` a mutation that takes the Ruby process down ends the run ([what `in_process` cannot contain](docs/isolation.md#what-in_process-cannot-contain)). See [docs/isolation.md](docs/isolation.md). |
136
136
  | `--preload FILE` | String | _(auto)_ | File to require in parent before forking workers. Auto-detect chain for Rails projects: `spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb`. For non-Rails gems: `spec/spec_helper.rb` → `test/test_helper.rb` → `test/helper.rb`, falling back to the gem entry `lib/<gem>.rb` (with a warning naming the searched paths). Pass `--no-preload` to opt out. |
137
137
  | `--no-preload` | Boolean | _(enabled)_ | Disable parent-process preload. |
138
138
  | `--warmup NAME` | String | `none` | After preload, warm lazily-initialised framework state once in the parent: `none` or `rails`. See [Warming up Rails before forking](#warming-up-rails-before-forking). |
@@ -165,12 +165,13 @@ 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 136 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
168
+ - **`default`** — the 155 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
169
169
  - **`strict`** — adds extra aggressive mutators on top of `default`:
170
170
  - `PredicateToNil` replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values.
171
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
172
 
173
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
+ - `SafeNavigationInsertion` replaces a plain call with safe navigation (`user.name` -> `user&.name`) to surface calls no test reaches with a nil receiver. With a nil receiver the mutant skips the call and gives `nil`, where the plain call raises `NoMethodError` or runs a method `nil` has (`nil.to_a`). It touches nearly every call with a receiver and survives wherever that receiver is never nil. It skips `self`, constants and literal receivers other than `nil`, and `::` calls. The opposite direction, `safe_navigation_removal`, is in the default profile.
174
175
 
175
176
  Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
176
177
 
@@ -182,7 +183,7 @@ Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.e
182
183
  |------|-----------------------------------------------|---------------------------------------|
183
184
  | 0 | Mutation score meets or exceeds `--min-score` | Success. No action needed. |
184
185
  | 1 | Mutation score below `--min-score`, or a target file resolved to no spec | Parse output, fix surviving mutants. |
185
- | 2 | Tool error (bad config, parse failure, etc.) | Check stderr, fix invocation. |
186
+ | 2 | Tool error (bad config, parse failure, a run under `--isolation in_process` that a mutation took down, etc.) | Check stderr, fix invocation. |
186
187
 
187
188
  `min_score` defaults to `0.0`, so **no score gate is armed unless you set one**. The `Result:` line says so rather than implying a threshold nobody configured:
188
189
 
@@ -194,6 +195,8 @@ $ evilution run lib/half_tested.rb --min-score 0.8
194
195
  Result: FAIL (score 66.67% < 80.00%) # exit 1
195
196
  ```
196
197
 
198
+ A run under `--isolation in_process` can end without a report: a mutation that takes the Ruby process down takes the run with it. It ends with 2, and a line on stderr names the mutation and points at `--isolation fork`. See [What `in_process` cannot contain](docs/isolation.md#what-in_process-cannot-contain).
199
+
197
200
  Evilution owns the exit status: `--preload` loads the project's own spec helper into the parent process, and an at-exit hook it installs (SimpleCov calls `exit` with its own status when coverage is below the minimum) would otherwise replace the status evilution computed (GH #1608).
198
201
 
199
202
  The printed verdict and the exit code always use the same threshold. Previously the line was printed against a hard-coded 80% that the exit code did not share, so a failing-looking run still exited 0 (GH #1604).
@@ -212,7 +215,7 @@ schema_version: 1 # opts into strict validation (rejects unknown keys
212
215
  # integration: rspec # test framework: rspec, minitest, test_unit
213
216
  # suggest_tests: false # concrete test code in suggestions (matches integration)
214
217
  # save_session: false # persist results under .evilution/results/
215
- # isolation: auto # auto | fork | in_process (auto selects fork for Rails + gems)
218
+ # isolation: auto # auto | fork | in_process (auto selects fork for Rails + gems, in_process otherwise; in_process cannot contain a mutation that takes the Ruby process down, see docs/isolation.md)
216
219
  # canary: true # proof-of-life synthetic mutation at session start (false to skip)
217
220
  # preload: null # path to preload before forking; false to disable; auto-detects for Rails + gems
218
221
  # warmup: none # rails: warm I18n, routes, assets and templates once after preload (fork isolation)
@@ -261,7 +264,7 @@ All keys recognised under `schema_version: 1`:
261
264
  | `jobs` | Integer | `1` | Number of parallel workers. |
262
265
  | `fail_fast` | Integer / null | `null` | Stop after N surviving mutants. `null` = disabled. |
263
266
  | `baseline` | Boolean | `true` | Run baseline test suite to detect pre-existing failures (a mutation failing only on those is `:neutral`). |
264
- | `isolation` | String | `auto` | Isolation strategy: `auto`, `fork`, `in_process`. `auto` selects `fork` for Rails projects and packaged gems (`*.gemspec`). |
267
+ | `isolation` | String | `auto` | Isolation strategy: `auto`, `fork`, `in_process`. `auto` selects `fork` for Rails projects and packaged gems (`*.gemspec`), `in_process` otherwise. Under `in_process` a mutation that takes the Ruby process down ends the run: see [What `in_process` cannot contain](docs/isolation.md#what-in_process-cannot-contain). |
265
268
  | `incremental` | Boolean | `false` | Cache killed/timeout results across runs. |
266
269
  | `suggest_tests` | Boolean | `false` | Generate concrete test code in survivor suggestions (matches `integration`). |
267
270
  | `progress` | Boolean | `true` | TTY progress bar. |
@@ -432,10 +435,10 @@ Compatibility policy for the `1.x` gem line:
432
435
 
433
436
  | Status | Meaning | Counted in score? |
434
437
  |--------------|-----------------------------------------------------------------------|-------------------|
435
- | `killed` | A test failed when the mutation was applied — test suite caught it | numerator + denominator |
438
+ | `killed` | A test failed when the mutation was applied — test suite caught it. Also a mutation that made its own file raise while loading, before any test ran, when the original file still loads (`error_message` starts with `load failed:`). Also a mutation whose test process died on an exception no handler saw, after the mutated source loaded and while the tests ran, as a crashing test would: under fork isolation `error_message` starts with `child died` and names the exception, under in-process isolation it starts with `test run ended by` | numerator + denominator |
436
439
  | `survived` | No test failed — gap in coverage | denominator only |
437
440
  | `timeout` | Test run exceeded `--timeout` — treated like survived for scoring | denominator only |
438
- | `error` | Mutation caused an unexpected error (syntax error, boot failure, etc.) | excluded from denominator |
441
+ | `error` | Mutation caused an unexpected error (syntax error, boot failure, a load failure the original file shares, etc.). Also a test process that died on an unhandled exception before the tests ran: under fork isolation `error_message` starts with `child died`, says `before the tests ran` and `error_class` is the exception's own class. Also a result that never arrived or could not be read, which is no verdict on the mutation (see [Diagnosing errored mutations](#7-diagnosing-errored-mutations)) | excluded from denominator |
439
442
  | `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 |
440
443
  | `equivalent` | Mutation is provably identical to the original (e.g. no-op replacement) | excluded |
441
444
  | `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 |
@@ -512,7 +515,7 @@ Subjects needing attention (2 subjects in 1 file):
512
515
 
513
516
  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`.
514
517
 
515
- ## Mutation Operators (136 total)
518
+ ## Mutation Operators (155 total)
516
519
 
517
520
  Each operator name is stable and appears in JSON output under `survived[].operator`.
518
521
 
@@ -521,16 +524,19 @@ Each operator name is stable and appears in JSON output under `survived[].operat
521
524
  | `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
522
525
  | `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
523
526
  | `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
527
+ | `short_circuit_to_eager` | Evaluate both operands of `&&` / `\|\|` / `and` / `or` (an operand that would regroup is parenthesized; an operand that only jumps, such as `return`, is skipped) | `a && b` -> `a & b`, `a \|\| b` -> `a \| b` |
524
528
  | `boolean_operand_promotion` | Drop one side of `&&` / `\|\|` | `a && b` -> `a`, `b` |
525
529
  | `boolean_expression_to_nil` | Replace a whole compound boolean with `nil` | `a && b` -> `nil` |
526
530
  | `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
527
531
  | `nil_replacement` | Replace `nil` with `true`, `false`, `0`, `""` | `nil` -> `true` |
528
- | `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
529
- | `float_literal` | Boundary-value float mutations | `f` -> `0.0`, `1.0` |
530
- | `string_literal` | Empty the string | `"str"` -> `""` |
531
- | `array_literal` | Empty the array | `[a, b]` -> `[]` |
532
- | `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
533
- | `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
532
+ | `integer_literal` | Boundary-value integer mutations, plus one sentinel past the next integer width (int8, uint8, int16, uint16, int32, uint32, int64) above the literal's magnitude | `n` -> `0`, `1`, `n+1`, `n-1`; `42` -> `167` |
533
+ | `float_literal` | Boundary-value float mutations, plus NaN and both infinities (not inside a pattern, where they do not parse) | `f` -> `0.0`, `1.0`, `Float::NAN`, `Float::INFINITY`, `-Float::INFINITY` |
534
+ | `complex_literal` | Replace a complex literal with `0i`, `1i`, its neighbours along the imaginary axis and `nil` (a rational part such as `3ri` gets no neighbours) | `5i` -> `0i`, `1i`, `6i`, `4i`, `nil` |
535
+ | `rational_literal` | Replace a rational literal with `0r`, `1r`, its two neighbours and `nil` (the rational part of a complex literal such as `3ri` is left to `complex_literal`) | `1.5r` -> `0r`, `1r`, `2.5r`, `0.5r`, `nil` |
536
+ | `string_literal` | Empty the string or replace it with `nil`; an interpolated string is also replaced as a whole; a backtick or `%x` command is replaced with `nil` only, its text never rewritten | `"str"` -> `""`, `nil`; `"a #{x}"` -> `""`, `nil`; `` `ls` `` -> `nil` |
537
+ | `array_literal` | Empty the array, replace it with `nil`, delete each element in turn (two or more elements; a heredoc element is kept), and replace a single-element array with its element (not a splat, bare keywords, a `%w` / `%i` word or a heredoc) | `[a, b]` -> `[]`, `nil`, `[b]`, `[a]`; `[x]` -> `x` |
538
+ | `hash_literal` | Empty the hash, replace it with `nil`, delete each key/value pair in turn (two or more elements; a `**splat` and a pair holding a heredoc are kept), and rename each label key in turn (a key written with `=>` is left to `symbol_literal` / `string_literal`) | `{a: 1, b: 2}` -> `{}`, `nil`, `{b: 2}`, `{a: 1}`, `{__evilution_mutated__: 1, b: 2}` |
539
+ | `symbol_literal` | Replace with sentinel symbol or `nil`; an interpolated symbol is replaced as a whole with the empty symbol or `nil` | `:foo` -> `:__evilution_mutated__`, `nil`; `:"a_#{x}"` -> `:""`, `nil` |
534
540
  | `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
535
541
  | `conditional_branch` | Remove if/unless/else branch | Deletes branch body |
536
542
  | `if_branch_swap` | Replace the if-branch with the else body, drop the else | `if c; x; else; y; end` -> `if c; y; end` |
@@ -547,6 +553,10 @@ Each operator name is stable and appears in JSON output under `survived[].operat
547
553
  | `argument_removal` | Remove individual arguments | `foo(a, b)` -> `foo(b)` |
548
554
  | `argument_nil_substitution` | Replace arguments with `nil` | `foo(a, b)` -> `foo(nil, b)` |
549
555
  | `call_to_nil` | Replace a method call with `nil` (skips void statements, receivers of another call, and attribute or index writes) | `user.name` -> `nil` |
556
+ | `constant_read_to_nil` | Replace a constant reference with `nil`, a path as a whole (skips call receivers, void statements and `rescue` exception classes) | `MAX` -> `nil`, `Config::MAX` -> `nil` |
557
+ | `variable_read_to_nil` | Replace a read of a local, instance, class or global variable, or `self`, with `nil` (skips call receivers, void statements, `if` / `unless` / `while` / `until` conditions, left operands of `&&` / `\|\|`, block-pass and double-splat arguments) | `total = count` -> `total = nil`, `build(name: @name)` -> `build(name: nil)` |
558
+ | `constant_namespace_strip` | Strip the namespace off a constant path, at each level of a longer one (skips top-level `::A` paths and operator-write targets such as `A::B ||= x`) | `A::B` -> `B`, `A::B::C` -> `C` and `B::C` |
559
+ | `constant_write_to_nil` | Assign `nil` to a constant instead of its value. A constant assignment outside a method is its own subject, named after the constant, and the literal operators mutate its value too; code that read the constant while the file loaded keeps the old value (skips a constant already `nil`; `Data.define` / `Struct.new` assignments are left to `data_struct_member`) | `LIMIT = 10` -> `LIMIT = nil` |
550
560
  | `safe_navigation_removal` | Replace `&.` with a plain call (skips `self` and literal receivers) | `user&.name` -> `user.name` |
551
561
  | `attribute_write_to_read` | Replace an attribute or index write with the matching read (skips writes `statement_deletion` already removes) | `a.foo = b` -> `a.foo`, `a[i] = b` -> `a[i]` |
552
562
  | `argument_propagation` | Replace a call with its only positional argument (skips operator methods, attribute writes and void statements) | `normalize(value)` -> `value` |
@@ -580,9 +590,14 @@ Each operator name is stable and appears in JSON output under `survived[].operat
580
590
  | `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
591
  | `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
592
  | `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)` |
593
+ | `round_half_mode_swap` | Swap the tie-breaking mode of `round` for the other two, so a value sitting exactly on a tie rounds differently (literal `:up` / `:even` / `:down` only; skips a mode held in a variable; `symbol_literal` leaves these modes alone) | `amount.round(2, half: :even)` -> `half: :up`, `half: :down` |
583
594
  | `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
595
  | `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
596
  | `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)_ |
597
+ | `freeze_removal` | Drop `.freeze` from a constant defined in a class or module body, attributed to the first method of that scope (skips values that are frozen anyway: symbols, numbers, `nil`, booleans, ranges, regexps, and string literals under `# frozen_string_literal: true`; a `freeze` inside a method is left to `method_call_removal`) | `LIST = %w[a b].freeze` -> `LIST = %w[a b]` |
598
+ | `visibility_removal` | Drop a `private`, `protected` or `module_function` declaration from a class, module or `class << self` body, attributed to the first method of that scope; a definition it wraps is kept (skips `public`, `private_constant`, `private_class_method`, and declarations guarded by a modifier) | `private` -> _(removed)_, `private def a; end` -> `def a; end` |
599
+ | `attr_accessor_swap` | Widen an `attr_reader` or `attr_writer` to `attr_accessor`, and remove an `attr_reader` / `attr_writer` / `attr_accessor` declaration, in a class, module or `class << self` body, attributed to the first method of that scope (skips a declaration wrapped in `private` / `protected`, or guarded by a modifier) | `attr_reader :name` -> `attr_accessor :name`, _(removed)_ |
600
+ | `sleep_to_zero` | Take the wait out of a `sleep` / `Kernel.sleep` whose duration is a variable or an expression (a literal duration is left to `integer_literal` / `float_literal`, which already emit `sleep 0`; skips a `sleep` sent to another receiver) | `sleep delay` -> `sleep 0` |
586
601
  | `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
602
  | `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
603
  | `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/` |
@@ -599,7 +614,9 @@ Each operator name is stable and appears in JSON output under `survived[].operat
599
614
  | `regex_simplification` | Simplify regex quantifiers, anchors, ranges | `/\d+/` -> `/\d/`, `/[a-z]/` -> `/[az]/` |
600
615
  | `receiver_replacement` | Drop explicit `self` receiver | `self.foo` -> `foo` |
601
616
  | `send_mutation` | Swap semantically related methods | `detect` -> `find`, `map` -> `flat_map`, `round` -> `floor` / `ceil` |
602
- | `compound_assignment` | Swap compound assignment operators | `+=` -> `-=`, `&&=` -> `\|\|=` |
617
+ | `compound_assignment` | Swap compound assignment operators, or replace the whole assignment with `nil`; on variables, attributes (`obj.count += 1`, `obj.name \|\|= x`) and indexes (`counts[key] += 1`, `cache[key] \|\|= x`) and, for operator writes, on constants (`TOTAL += 1`, a subject of its own like a constant assignment) | `+=` -> `-=`, `&&=` -> `\|\|=` |
618
+ | `operator_write_receiver_promotion` | Retarget an operator write on an attribute at the instance variable it goes through (skips other receivers, logical writes and index writes) | `@account.balance += x` -> `@account += x` |
619
+ | `memoization_break` | Recompute a memoized instance variable on every call (instance variables only; a local, `@@x`, `$x` or `@cache[key] \|\|=` is left alone) | `@x \|\|= expr` -> `@x = expr` |
603
620
  | `local_variable_assignment` | Replace variable assignment with `nil` | `x = expr` -> `x = nil` |
604
621
  | `instance_variable_write` | Replace ivar assignment with `nil` | `@x = expr` -> `@x = nil` |
605
622
  | `class_variable_write` | Replace cvar assignment with `nil` | `@@x = expr` -> `@@x = nil` |
@@ -611,6 +628,7 @@ Each operator name is stable and appears in JSON output under `survived[].operat
611
628
  | `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
629
  | `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
630
  | `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` |
631
+ | `rescue_class_widening` | Widen what a rescue clause catches, so a sibling error that should escape is handled too: the listed classes become `StandardError`, then `Exception`; a bare rescue, `StandardError`, or a core class outside it (`Interrupt`, `LoadError`, ...) becomes `Exception` only (skips a clause already naming `Exception`, and rescue modifiers) | `rescue KeyError => e` -> `rescue StandardError => e`, `rescue Exception => e` |
614
632
  | `inline_rescue` | Remove inline rescue fallback | `expr rescue val` -> `expr` |
615
633
  | `ensure_removal` | Remove ensure blocks | Deletes ensure block |
616
634
  | `break_statement` | Remove break statements | `break` -> removed |
@@ -624,6 +642,10 @@ Each operator name is stable and appears in JSON output under `survived[].operat
624
642
  | `index_to_at` | Replace `[]` with `.at()` for arrays | `arr[0]` -> `arr.at(0)` |
625
643
  | `index_to_fetch` | Replace `[]` with `.fetch()` | `h[k]` -> `h.fetch(k)` |
626
644
  | `index_to_dig` | Replace `[]` chains with `.dig()` | `h[a][b]` -> `h.dig(a, b)` |
645
+ | `index_receiver_to_self` | Send an index read to `self` instead of its receiver (skips index writes and reads already on `self`) | `h[k]` -> `self[k]` |
646
+ | `index_to_key_predicate` | Replace an index read with a `key?` check (skips integer and range indexes, reads with several arguments, void statements and index writes) | `h[k]` -> `h.key?(k)` |
647
+ | `index_range_to_drop` | Replace a to-the-end range index with `drop` (also the endless `a[n..]` and `a[n...]`; skips a literal start of zero or below, ranges stopping short of the last element, void statements and index writes) | `a[n..-1]` -> `a.drop(n)` |
648
+ | `index_write_to_value` | Replace an index write with the value it assigns, dropping the store; the assignment syntax still evaluates to the same value, the `a.[]=(k, v)` / `a&.[]=(k, v)` spellings may not (skips void statements, operator writes and multiple-assignment targets) | `h[k] = v` -> `v` |
627
649
  | `index_assignment_removal` | Remove `[]=` assignments | `h[k] = v` -> removed |
628
650
  | `pattern_matching_guard` | Remove/negate pattern guards | `in x if cond` -> `in x` |
629
651
  | `pattern_matching_alternative` | Remove/reorder alternatives | `pat1 \| pat2` -> `pat1` |
@@ -684,7 +706,7 @@ The server exposes the following tools:
684
706
  |---|---|
685
707
  | `evilution-mutate` | Run mutation testing on target files with structured JSON results |
686
708
  | `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) |
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 |
709
+ | `evilution-info` | Discovery before mutation — `action: subjects` lists the mutation subjects (methods, and class-body subjects such as constant assignments, 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 |
688
710
 
689
711
  ### Verbosity Control
690
712
 
@@ -851,10 +873,10 @@ Methods whose body overlaps the requested range are included. Mix targeted and w
851
873
  evilution run lib/foo.rb:15-30 lib/bar.rb --format json
852
874
  ```
853
875
 
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`:
876
+ 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, `Data.define` / `Struct.new` definitions and constant assignments (see step 2 under [Internals](#internals-for-context-not-for-direct-use)). Other class-body code — 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
877
 
856
878
  ```
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.
879
+ [evilution] app/models/order.rb:12-14 holds code outside every subject (class-body code such as DSL calls is not mutated); no mutations target those lines.
858
880
  ```
859
881
 
860
882
  Read `0 mutations` next to that message as "not measured", not as "nothing to test".
@@ -891,7 +913,7 @@ A score describes only the mutations that got a verdict. Five fields say what it
891
913
  |---|---|---|
892
914
  | `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 |
893
915
  | `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[]` |
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 |
916
+ | `summary.uncovered_code` | Lines you targeted hold code outside every subject — class-body code such as 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
917
  | `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 |
896
918
  | `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` |
897
919
 
@@ -910,7 +932,27 @@ For each entry in `survived[]`:
910
932
 
911
933
  ### 7. Diagnosing errored mutations
912
934
 
913
- Entries in the JSON `errors[]` array represent mutations that raised an exception (syntax error, load failure, or runtime crash) rather than producing a test outcome. Each entry includes `error_class`, `error_message`, and the first 5 `error_backtrace` lines. Use these fields to decide whether the error is a bug in the mutation operator (file an issue), a load-time problem in the mutated source (often `NoMethodError: super called outside of method` or constant-redefinition issues), or a genuine crash that the original tests should have caught. Run with `--verbose` to stream the same error details to stderr during the run.
935
+ Entries in the JSON `errors[]` array represent mutations that raised an exception (syntax error, load failure, or runtime crash) rather than producing a test outcome. A mutation that makes its own file raise while loading is not among them when the original file still loads right after it: the mutation is what broke the load, so it is reported `killed` with an `error_message` that starts with `load failed:`. A load failure stays an error when the original fails to load as well, since that says nothing about the mutation. Each entry includes `error_class`, `error_message`, and the first 5 `error_backtrace` lines. Use these fields to decide whether the error is a bug in the mutation operator (file an issue), a load-time problem in the mutated source (often `NoMethodError: super called outside of method` or constant-redefinition issues), or a genuine crash that the original tests should have caught. Run with `--verbose` to stream the same error details to stderr during the run.
936
+
937
+ A test process that dies on an exception no handler saw (a stack overflow that does not unwind, for one) reports the exception before it goes. What the mutation gets depends on when it died:
938
+
939
+ | `error_message` starts with | Isolation | Status | Meaning |
940
+ |---|---|---|---|
941
+ | `child died`, then `while running the tests` | fork | `killed` | The mutated source loaded and the tests were running: what a crashing test does. |
942
+ | `test run ended by` | in-process | `killed` | The same, in the process of the run. |
943
+ | `child died`, then `before the tests ran` | fork | `error` | Nothing was tested yet. `error_class` is the exception's class. |
944
+
945
+ Three messages are not about the mutation at all. No verdict was reached, and the entry says only why:
946
+
947
+ | `error_message` | Meaning | What to do |
948
+ |---|---|---|
949
+ | `child exited 0; its result was unreadable (...)` (the status varies) | Fork isolation. The child finished, but what arrived on its result pipe does not decode to a result; the parentheses say what was found. Every process the child forks inherits that pipe, so code under test that writes to a descriptor it did not open can put bytes on it. | Re-run that one mutation (line-range targeting). If it repeats, look for code that writes to inherited descriptors. |
950
+ | `worker's result could not be read; the worker was replaced` | Worker pool (`--jobs` above 1). The same thing on the pipe between the run and a worker: the worker was killed and replaced, and the run went on. | Re-run that one mutation, with `--jobs 1` to take the pool out of the picture. |
951
+ | `child died on SIGABRT without writing a result` | Fork isolation. The Ruby VM aborted (a `[BUG]` report, typically), so nothing could be reported from inside the child. Other signals read the same way: a segfault is `SIGSEGV`, a child the kernel killed for memory is `SIGKILL`. | Re-run that one mutation. If it repeats, the mutant crashes the interpreter: that is Ruby's to fix, and the mutation can be disabled with a comment. |
952
+
953
+ The messages in the first column of this last table are examples: the exit status, the signal and the text in parentheses vary. Search `errors[]` for `was unreadable`, `could not be read` and `without writing a result`.
954
+
955
+ Under `--isolation in_process` there is no child to lose: a mutation that takes the Ruby process down takes the run with it, and no report is written. See [Exit Codes](#exit-codes).
914
956
 
915
957
  ### Unresolved mutations — behaviour-named spec layouts
916
958
 
@@ -1058,9 +1100,9 @@ For the full contributor architecture — module map, data flow, and extension
1058
1100
  points — see [docs/architecture.md](docs/architecture.md).
1059
1101
 
1060
1102
  1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
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)`)
1103
+ 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, every other constant assignment outside a method (`LIMIT = 10`, `LIMIT += 1`), named after the constant (`Config::LIMIT`); `constant_write_to_nil` mutates a plain assignment, `compound_assignment` an operator write, and the literal operators the value of either, 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` or `prepended do ... end` block, where they are named after the concern (`Publishable.published`). A method defined in a concern's `included do ... end` or `prepended do ... end` block is mutated on the classes that took the concern before the run as well as on those that take it later; 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` or `prepended 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` or `prepended 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)`)
1062
1104
  3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
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
1105
+ 4. **Mutate** — 155 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
1064
1106
  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
1065
1107
  6. **Test** — The configured test framework (RSpec, Minitest, or Test::Unit) executes against the mutated source
1066
1108
  7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention