evilution 0.8.0 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 134f193b4bce22007d6af782bd885129aafb9104a763cfec5e9a70c322be25ec
4
- data.tar.gz: 7031e18d9eacd96886200b1c598b346d4256177045c7d639c54c3020c3f61f63
3
+ metadata.gz: a4753225c2b9795143ecd0e37a355a2503e2a467ee9f79d1be82841141102a3d
4
+ data.tar.gz: 760527bdb4c437124ed4ec26e35d72aff297bdc519636cb04b12eee408e5a4e4
5
5
  SHA512:
6
- metadata.gz: ab72d51bfae90aca49ccca456a4d576a3caf30ceaa6c1d51b6f489548458f56cf86c7038fd9bfb69aa7452fe4ec81efbebbddaf0e4e5961f82262f31bc501690
7
- data.tar.gz: 3ca5fa2951e70df43e4e56b611115ae1d4c2611132c53e5a26af92a90737be231a0bb3c62d3bab7444fc0de22f1210284ec8365d7134353eddf550ddbb417522
6
+ metadata.gz: bc9b9d5d7922ecb4f6dd7fd7a9dd62dcf292f8c5ba1b8d6b71b01d3850a2565afaecd04d30fc18bdef9f8d586cf5559ce97ac7188b5c0e9e99d3d5bbfb4c91e4
7
+ data.tar.gz: c8ec937012b8e910c61c42b650150de6144f59e49a731e903d59973858bca71e8fbd63a29988acb749f3e6ecfaa0ecc749d907195f81449bf6c3aa68a3872fe4
@@ -1 +1 @@
1
- 1773850606
1
+ 1774024848
data/.beads/issues.jsonl CHANGED
@@ -99,10 +99,17 @@
99
99
  {"id":"EV-5.5","title":"Write README.md","description":"Replace placeholder README with: gem description, installation, quick start, CLI usage, configuration, output formats (JSON schema), operator list, comparison with mutant, contributing guide, license.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.454635118+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.226280116+07:00","closed_at":"2026-03-02T11:54:29.226280116+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.5","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-4.9","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-5.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
100
100
  {"id":"EV-5.6","title":"Update CHANGELOG.md for v0.1.0","description":"Document all features in the initial release: operator list, RSpec integration, JSON/CLI output, parallel execution, coverage-based selection, diff-based targeting.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.571499415+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.22634981+07:00","closed_at":"2026-03-02T11:54:29.22634981+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.6","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.6","depends_on_id":"EV-5.5","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
101
101
  {"id":"EV-50","title":"Operator: ConditionalFlip","description":"Flip if/unless. e.g. if cond → unless cond. Catches single-branch conditional testing.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.379431887+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T16:25:00.912700206+07:00","closed_at":"2026-03-19T16:25:00.912700206+07:00","close_reason":"Implemented ConditionalFlip operator with 10 specs","external_ref":"gh-98","dependencies":[{"issue_id":"EV-50","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
102
- {"id":"EV-51","title":"Operator: SendMutation","description":"Replace known method families. e.g. flat_map → map, public_send → send, gsub → sub. Catches untested method semantics.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.483414117+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T15:45:33.737392246+07:00","external_ref":"gh-99","dependencies":[{"issue_id":"EV-51","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
102
+ {"id":"EV-51","title":"Operator: SendMutation","description":"Replace known method families. e.g. flat_map → map, public_send → send, gsub → sub. Catches untested method semantics.","status":"closed","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.483414117+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-20T23:14:12.811639741+07:00","closed_at":"2026-03-20T23:14:12.811639741+07:00","close_reason":"Implemented SendMutation operator with 17 method family replacements (flat_map/map, public_send/send, gsub/sub, detect/find, collect/map, each_with_object/inject, reverse_each/each, length/size, values_at/fetch_values). 19 specs passing.","external_ref":"gh-99","dependencies":[{"issue_id":"EV-51","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
103
103
  {"id":"EV-52","title":"Operator: ArgumentRemoval","description":"Remove, permute, and replace individual method call arguments. e.g. foo(a, b) → foo(a), foo(b, a), foo(a, nil). This is the #1 cited gap vs mutant — where mutant catches the most bugs evilution misses. Includes: removing each argument individually, swapping argument order, replacing with nil.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.587291445+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T15:45:33.832675896+07:00","closed_at":"2026-03-17T23:16:28.920737471+07:00","close_reason":"Merged","external_ref":"gh-100","dependencies":[{"issue_id":"EV-52","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
104
- {"id":"EV-53","title":"Operator: ReceiverReplacement","description":"Drop explicit self receiver. e.g. self.foo → foo. Low bug-finding value.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.692244528+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T15:45:33.9293301+07:00","external_ref":"gh-101","dependencies":[{"issue_id":"EV-53","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
104
+ {"id":"EV-53","title":"Operator: ReceiverReplacement","description":"Drop explicit self receiver. e.g. self.foo → foo. Low bug-finding value.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.692244528+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T20:56:21.943424181+07:00","closed_at":"2026-03-19T20:56:21.943424181+07:00","close_reason":"Implemented ReceiverReplacement operator with 8 specs, 100% mutation score","external_ref":"gh-101","dependencies":[{"issue_id":"EV-53","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
105
105
  {"id":"EV-54","title":"Deepen existing operators with more replacement variants","description":"Existing operators generate limited variants per node. E.g. integer 5 only produces 0 and 1, but could also produce -1 and nil. Expanding variant sets across all 18 operators would significantly increase mutation coverage.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.797359784+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T15:45:34.022534916+07:00","external_ref":"gh-102","dependencies":[{"issue_id":"EV-54","depends_on_id":"EV-45","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
106
+ {"id":"EV-54.1","title":"Add nil variants to literal operators","description":"Add nil as a mutation variant to IntegerLiteral, FloatLiteral, StringLiteral, BooleanLiteralReplacement, ArrayLiteral, HashLiteral, and SymbolLiteral operators. Nil-safety is one of the most common sources of production bugs in Ruby, making this the highest-ROI expansion.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:06.464726693+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:01.743744776+07:00","external_ref":"gh-167","dependencies":[{"issue_id":"EV-54.1","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
107
+ {"id":"EV-54.2","title":"Expand NilReplacement with false, 0, and empty string variants","description":"Currently nil only mutates to true. Add false, 0, and empty string variants since nil is commonly used in boolean, numeric, and string contexts.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:20.731673229+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:01.844521612+07:00","external_ref":"gh-168","dependencies":[{"issue_id":"EV-54.2","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
108
+ {"id":"EV-54.3","title":"Expand CollectionReplacement with more method swaps","description":"Add sort↔sort_by, find↔detect, any?↔all?, count↔length swaps. These are very common collection methods with subtle behavioral differences.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:20.842448904+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:01.9421689+07:00","external_ref":"gh-169","dependencies":[{"issue_id":"EV-54.3","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
109
+ {"id":"EV-54.4","title":"Expand ComparisonReplacement with opposite direction flips","description":"Currently > only mutates to >= and ==. Add full opposite flips: >→<, >=→<=, <→>, <=→>=. Catches inverted comparison bugs.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:20.944147572+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:02.036010574+07:00","external_ref":"gh-170","dependencies":[{"issue_id":"EV-54.4","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
110
+ {"id":"EV-54.5","title":"Add always-matching regexp variant","description":"Currently regexp only mutates to never-matching /a\\A/. Add always-matching /.*/ variant to test both match and no-match code paths.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:21.049402671+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:02.124434713+07:00","external_ref":"gh-171","dependencies":[{"issue_id":"EV-54.5","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
111
+ {"id":"EV-54.6","title":"Expand ArithmeticReplacement with bitwise shift operators","description":"Add <<→>> swap. Niche but catches bitwise shift bugs. Lowest priority in this epic.","status":"open","priority":4,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T21:21:21.152296396+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:22:02.210556018+07:00","external_ref":"gh-172","dependencies":[{"issue_id":"EV-54.6","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
112
+ {"id":"EV-54.7","title":"Operator: ArgumentNilSubstitution","description":"Replace each method argument with nil, one at a time. E.g. foo(a, b) → foo(nil, b) and foo(a, nil). Works for single-arg calls too (unlike ArgumentRemoval which requires 2+). This catches 'default value' bugs where a method has a sensible fallback for nil — e.g. I18n.with_locale(user.locale) → I18n.with_locale(nil) silently falls back to default locale. Identified from real-world feedback where mutant caught a locale test gap that evilution missed (v0.7.0).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T22:09:42.995246472+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-20T23:43:12.098455761+07:00","closed_at":"2026-03-20T23:43:12.098455761+07:00","close_reason":"Implemented ArgumentNilSubstitution operator. Replaces each argument with nil one at a time, works for single-arg calls too (unlike ArgumentRemoval). Skips splat, keyword, block, and forwarding args. 12 specs passing, 705 total.","external_ref":"gh-175","dependencies":[{"issue_id":"EV-54.7","depends_on_id":"EV-54","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
106
113
  {"id":"EV-55","title":"Concrete suggestions: Comparison & Arithmetic operators","description":"Generate concrete RSpec it blocks for ComparisonReplacement and ArithmeticReplacement mutations. Test should assert exact return value at boundary conditions.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.26716126+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.26716126+07:00"}
107
114
  {"id":"EV-56","title":"Concrete suggestions: Boolean operators","description":"Generate concrete RSpec it blocks for BooleanOperatorReplacement, BooleanLiteralReplacement, and NegationInsertion mutations. Test should assert true/false explicitly.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.370014645+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.370014645+07:00"}
108
115
  {"id":"EV-57","title":"Concrete suggestions: Literal operators","description":"Generate concrete RSpec it blocks for IntegerLiteral, FloatLiteral, StringLiteral, and SymbolLiteral mutations. Test should assert exact value returned.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.473990949+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.473990949+07:00"}
@@ -113,11 +120,11 @@
113
120
  {"id":"EV-61","title":"Concrete suggestions: Nil operator","description":"Generate concrete RSpec it block for NilReplacement mutations. Test should assert non-nil return value.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.882878252+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.882878252+07:00"}
114
121
  {"id":"EV-62","title":"Neutral mutation detection","description":"Run the original unmutated code once before mutation testing. Flag any tests that already fail, so surviving mutants caused by pre-broken tests are reported as neutral rather than gaps. Eliminates false positives in mutation score.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.670730866+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T23:16:28.920679692+07:00","closed_at":"2026-03-17T23:16:28.920679692+07:00","close_reason":"Merged"}
115
122
  {"id":"EV-63","title":"In-process mutation for performance","description":"Replace fork+file-write isolation with in-memory mutation (constant replacement or load-path override) for the common case. Current approach: 3.0 mutations/s vs mutant's 17.6 mutations/s (6x slower). Fork overhead is the main bottleneck. Keep fork as fallback for safety.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.765021194+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T18:31:15.344270883+07:00","closed_at":"2026-03-18T18:31:15.344270883+07:00","close_reason":"GH issue #114 closed, PR #123 merged"}
116
- {"id":"EV-64","title":"Class-level --target filtering","description":"Extend --target to accept class names without method (e.g. --target Game) to mutate all methods in the class. Currently only supports fully-qualified method names like Game#method.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.858147999+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:03.858147999+07:00"}
117
- {"id":"EV-65","title":"Incremental mode with result caching","description":"Cache mutation results and only re-run mutations on changed lines/methods. Big CI speed win for large codebases. Could key cache on file content hash + mutation fingerprint.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.95132569+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:03.95132569+07:00"}
118
- {"id":"EV-66","title":"Scope-aware spec resolution","description":"Improve convention-based spec resolution for nested modules. When a method is nested in a module, prefer spec/models/game_spec.rb over a generic match. Handle edge cases in app/ directory structures.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.04579177+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:04.04579177+07:00"}
119
- {"id":"EV-67","title":"HTML report with visual mutation map","description":"Generate an HTML report showing which lines are well-tested vs vulnerable. Visual mutation map per file with color-coded coverage.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.13552276+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:04.13552276+07:00"}
120
- {"id":"EV-68","title":"Equivalent mutation detection","description":"Add heuristics to skip mutations that produce provably identical behavior (dead code paths, no-op transformations). Reduces noise in mutation results.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.226297798+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:04.226297798+07:00"}
123
+ {"id":"EV-64","title":"Class-level --target filtering","description":"Extend --target to accept class names without method (e.g. --target Game) to mutate all methods in the class. Currently only supports fully-qualified method names like Game#method.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.858147999+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:31:38.601679951+07:00","closed_at":"2026-03-19T21:31:38.601679951+07:00","close_reason":"Implemented class-level --target filtering with 3 new specs, 100% mutation score"}
124
+ {"id":"EV-65","title":"Incremental mode with result caching","description":"Cache mutation results and only re-run mutations on changed lines/methods. Big CI speed win for large codebases. Could key cache on file content hash + mutation fingerprint.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:03.95132569+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T21:47:19.95617067+07:00","closed_at":"2026-03-19T21:47:19.95617067+07:00","close_reason":"Implemented incremental mode with result caching. Cache class (9 specs), runner integration (4 specs), CLI --incremental flag, config support. 100% mutation score on cache."}
125
+ {"id":"EV-66","title":"Scope-aware spec resolution","description":"Improve convention-based spec resolution for nested modules. When a method is nested in a module, prefer spec/models/game_spec.rb over a generic match. Handle edge cases in app/ directory structures.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.04579177+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T22:15:01.15206232+07:00","closed_at":"2026-03-19T22:15:01.15206232+07:00","close_reason":"Implemented parent-path fallback in SpecResolver for nested module spec resolution"}
126
+ {"id":"EV-67","title":"HTML report with visual mutation map","description":"Generate an HTML report showing which lines are well-tested vs vulnerable. Visual mutation map per file with color-coded coverage.","status":"closed","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.13552276+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-20T23:58:29.045246978+07:00","closed_at":"2026-03-20T23:58:29.045246978+07:00","close_reason":"Implemented HTML reporter with visual mutation map. Self-contained HTML with inline CSS (dark theme), per-file mutation maps with color-coded lines (green=killed, red=survived, yellow=timeout), expandable survived mutation details with diffs and suggestions. Writes to evilution-report.html. 17 reporter specs, 724 total passing."}
127
+ {"id":"EV-68","title":"Equivalent mutation detection","description":"Add heuristics to skip mutations that produce provably identical behavior (dead code paths, no-op transformations). Reduces noise in mutation results.","status":"closed","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.226297798+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-21T00:22:25.796367972+07:00","closed_at":"2026-03-21T00:22:25.796367972+07:00","close_reason":"Implemented equivalent mutation detection with 4 heuristics: NoopSource (identical source), MethodBodyNil (empty/nil method bodies), AliasSwap (detect/find, length/size, collect/map), DeadCode (unreachable code after return/raise). Integrated into runner pipeline, reporters (CLI/JSON/HTML), and MCP tool. Equivalents excluded from score denominator. 100% mutation score (211/211). 753 total specs passing."}
121
128
  {"id":"EV-69","title":"Minitest integration","description":"Add Minitest support alongside existing RSpec integration. Broader ecosystem support for non-RSpec projects.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T10:15:04.313129974+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:15:04.313129974+07:00"}
122
129
  {"id":"EV-7","title":"Enable true per-mutation isolation with temp file copies","description":"Currently all mutations for the same file go to one worker (PR #4 fix). This means single-file projects get no parallelism benefit. Implement temp file copy approach: each worker copies the source file to a temp location, mutates the copy, and runs tests against it. This enables safe parallel mutation of the same file across multiple workers.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:46.820210376+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:46:55.740292929+07:00","closed_at":"2026-03-06T11:46:55.740292929+07:00","close_reason":"Already merged in PR #20"}
123
130
  {"id":"EV-70","title":"Epic: Fix memory leaks and add memory observability","description":"Evilution processes consume up to 17.2 GB each due to memory leaks. This epic covers: (1) identifying and fixing root causes — double forking, upfront mutation array, source string retention in results, Marshal round-trips; (2) adding runtime memory instrumentation behind --verbose; (3) adding memory budget specs to catch regressions in CI.","status":"closed","priority":1,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:39.345459861+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T14:34:58.765114612+07:00","closed_at":"2026-03-19T14:34:58.765114612+07:00","close_reason":"All 14 sub-issues complete. Memory leaks fixed, observability added, rake memory:check in place for regression detection.","external_ref":"gh-124","labels":["v0.7.0"]}
@@ -135,5 +142,8 @@
135
142
  {"id":"EV-70.7","title":"Eliminate double forking in parallel mode","description":"When jobs>1 with auto isolation, Pool already forks workers. Using Isolation::Fork inside each worker creates a redundant second fork. Fix: use in-process isolation inside pool workers (they already have process isolation from the pool fork). This is likely the single biggest contributor to 17 GB memory usage.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:02.309469841+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T00:06:16.517207415+07:00","closed_at":"2026-03-19T00:06:16.517207415+07:00","close_reason":"Eliminated double forking: parallel pool workers now use InProcess isolation since Pool already provides fork-based process isolation. Simplified resolve_isolation to never auto-select Fork.","external_ref":"gh-131","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.7","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
136
143
  {"id":"EV-70.8","title":"Lazy/streaming mutation generation","description":"Currently generate_mutations builds ALL mutations into an array before any are executed (runner.rb:38). For large codebases this holds thousands of Mutation objects (each with 2x full source strings + AST node) in memory simultaneously. Change to lazy enumeration so mutations are generated per-subject as needed.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:05.552816007+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T00:26:32.161184282+07:00","closed_at":"2026-03-19T00:26:32.161184282+07:00","close_reason":"Changed generate_mutations to lazy enumerator (subjects.lazy.flat_map). Mutations generated per-subject on demand, not all materialized upfront. Baseline now receives subjects instead of mutations. Pre-computed mutation_count passed to run methods for progress/fail_fast.","external_ref":"gh-132","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.8","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
137
144
  {"id":"EV-70.9","title":"Strip source strings from Mutation after result creation","description":"Each MutationResult retains its Mutation which holds original_source and mutated_source (full file contents). These are only needed for diff generation in reporters. Compute and cache the diff eagerly, then release the source strings to allow GC. Or store only the diff in MutationResult.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:08.1904857+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T10:31:19.673577645+07:00","closed_at":"2026-03-19T10:31:19.673577645+07:00","close_reason":"Implemented strip_sources! on Mutation (lazy diff caching, nil out source strings after execution). Wired into Runner sequential and parallel paths.","external_ref":"gh-133","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.9","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
145
+ {"id":"EV-71","title":"Trim MCP tool response to reduce context window usage","description":"The evilution MCP tool returns full JSON report with all killed mutation diffs (~10k tokens). Only survived mutations are actionable. Trim killed/neutral/timed_out/errors detail arrays in MutateTool.call, keeping summary counts and survived details only.","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T23:23:32.672265145+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T23:31:18.511115181+07:00","closed_at":"2026-03-19T23:31:18.511115181+07:00","close_reason":"Trim killed/neutral/timed_out/errors detail arrays from MCP tool response, keeping only summary and survived details","external_ref":"marinazzio/evilution#178"}
146
+ {"id":"EV-72","title":"Add verbosity control to MCP tool response","description":"Add a verbosity parameter (full/summary/minimal) to the MCP tool to control response size. Default to summary for best context efficiency. Full keeps all entries without diffs, summary omits killed/neutral arrays, minimal keeps only summary + survived.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-19T23:41:41.837080367+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T23:41:41.837080367+07:00","external_ref":"marinazzio/evilution#179"}
147
+ {"id":"EV-73","title":"Clean up deprecated CLI switches and config options","description":"Remove long-deprecated --diff, --no-coverage flags and coverage config option. GH #176.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-21T00:37:09.266487789+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-21T00:49:24.827479374+07:00","closed_at":"2026-03-21T00:49:24.827479374+07:00","close_reason":"Removed deprecated --diff, --no-coverage CLI flags; removed diff_base, coverage, diff? from Config; removed filter_by_diff from Runner; deleted diff/parser, diff/file_filter, coverage/collector, coverage/test_map and their specs; cleaned up requires. 712 specs passing.","external_ref":"gh-176"}
138
148
  {"id":"EV-8","title":"Investigate RSpec state accumulation across mutation runs","description":"RSpec.reset is called between mutations but loaded spec files persist in memory. Long mutation runs may accumulate state from previously loaded specs, potentially causing false positives/negatives. Investigate whether RSpec::Core::Runner.run properly isolates between runs or if additional cleanup is needed.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:48.618669476+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T11:57:56.965910759+07:00","closed_at":"2026-03-05T11:57:56.965910759+07:00","close_reason":"Investigation complete: fork isolation already prevents state accumulation. Added documentation explaining the guarantee and tests verifying independent consecutive runs."}
139
149
  {"id":"EV-9","title":"Add multi-Ruby CI test matrix (3.2, 3.3, 4.0)","description":"CI currently only tests Ruby 4.0.1. Add a matrix strategy testing Ruby 3.2, 3.3, and 4.0 to ensure compatibility across supported Ruby versions. Prism ships with Ruby 3.3+ so 3.2 may need the prism gem as a dependency.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:51.239774764+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.83181612+07:00","closed_at":"2026-03-05T12:31:51.83181612+07:00","close_reason":"Closed"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0] - 2026-03-21
4
+
5
+ ### Added
6
+
7
+ - **SendMutation operator** — new mutation operator that replaces method calls with semantically related alternatives (e.g. `detect` ↔ `find`, `map` ↔ `flat_map`, `length` ↔ `size`, `gsub` ↔ `sub`, `send` ↔ `public_send`, and more); 17 replacement pairs covering common Ruby method families
8
+ - **ArgumentNilSubstitution operator** — new mutation operator that replaces each positional argument with `nil` one at a time (e.g. `foo(a, b)` → `foo(nil, b)`, `foo(a, nil)`); skips splat, keyword, block, and forwarding arguments
9
+ - **HTML report** (`--format html`) — self-contained HTML mutation report with dark theme, color-coded mutation map, survived mutation diffs with suggestions, and score badge; written to `evilution-report.html`
10
+ - **Equivalent mutation detection** — automatically identifies mutations that produce semantically identical behavior using four heuristics: noop source (identical before/after), method body nil (empty/nil methods), alias swap (detect↔find, length↔size, collect↔map), and dead code (unreachable statements after return/raise); equivalent mutations are excluded from the mutation score denominator
11
+ - **MCP tool equivalent trimming** — diffs are stripped from equivalent mutation entries in MCP responses alongside killed and neutral entries
12
+
13
+ ### Removed
14
+
15
+ - **`--diff` CLI flag** — deprecated since v0.2.0; use line-range targeting instead (e.g. `evilution run lib/foo.rb:15-30`)
16
+ - **`--no-coverage` CLI flag** — deprecated since v0.2.0; had no effect
17
+ - **`diff_base` and `coverage` config keys** — no longer recognized in `.evilution.yml`; config file warnings removed
18
+ - **`Diff::Parser` and `Diff::FileFilter` modules** — dead code removed along with specs
19
+ - **`Coverage::Collector` and `Coverage::TestMap` modules** — dead code removed along with specs
20
+
21
+ ## [0.9.0] - 2026-03-19
22
+
23
+ ### Added
24
+
25
+ - **ReceiverReplacement operator** — new mutation operator that drops explicit `self` receiver from method calls (e.g. `self.foo` → `foo`); catches untested self-dispatch semantics
26
+ - **Class-level `--target` filtering** — `--target Foo` now matches all methods in the `Foo` class, not just `Foo#method`; instance method targeting (`Foo#bar`) continues to work as before
27
+ - **Incremental mode** (`--incremental`) — caches killed/timeout results keyed by file content SHA256 + mutation fingerprint; skips re-running unchanged mutations on subsequent runs; atomic file-based cache in `tmp/evilution_cache/`
28
+ - **Scope-aware spec resolution** — `SpecResolver` now walks up the directory tree when an exact spec file isn't found (e.g. `app/models/game/round.rb` → `spec/models/game_spec.rb`); works with both stripped (`spec/`) and kept (`spec/lib/`) layouts
29
+
30
+ ### Changed
31
+
32
+ - **MCP tool response trimming** — diffs are stripped from killed and neutral mutation entries to reduce context window usage (~36% smaller responses); survived, timed_out, and errors retain full diffs for actionability
33
+
3
34
  ## [0.8.0] - 2026-03-19
4
35
 
5
36
  ### Added
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Evilution
8
+ class Cache
9
+ DEFAULT_DIR = "tmp/evilution_cache"
10
+
11
+ def initialize(cache_dir: DEFAULT_DIR)
12
+ @cache_dir = cache_dir
13
+ end
14
+
15
+ def fetch(mutation)
16
+ return nil if mutation.original_source.nil?
17
+
18
+ file_key = file_key(mutation)
19
+ entry_key = entry_key(mutation)
20
+ data = read_file(file_key)
21
+ return nil unless data
22
+
23
+ entry = data[entry_key]
24
+ return nil unless entry.is_a?(Hash) && entry["status"].is_a?(String)
25
+
26
+ { status: entry["status"].to_sym, duration: entry["duration"],
27
+ killing_test: entry["killing_test"], test_command: entry["test_command"] }
28
+ end
29
+
30
+ def store(mutation, result_data)
31
+ file_key = file_key(mutation)
32
+ entry_key = entry_key(mutation)
33
+ data = read_file(file_key) || {}
34
+
35
+ data[entry_key] = {
36
+ "status" => result_data[:status].to_s,
37
+ "duration" => result_data[:duration],
38
+ "killing_test" => result_data[:killing_test],
39
+ "test_command" => result_data[:test_command]
40
+ }
41
+
42
+ write_file(file_key, data)
43
+ end
44
+
45
+ def clear
46
+ FileUtils.rm_rf(@cache_dir)
47
+ end
48
+
49
+ private
50
+
51
+ def file_key(mutation)
52
+ content_hash = Digest::SHA256.hexdigest(mutation.original_source)
53
+ "#{safe_filename(mutation.file_path)}_#{content_hash[0, 16]}"
54
+ end
55
+
56
+ def entry_key(mutation)
57
+ "#{mutation.operator_name}:#{mutation.line}:#{mutation.column}"
58
+ end
59
+
60
+ def safe_filename(path)
61
+ path.gsub(%r{[/\\]}, "_").gsub(/[^a-zA-Z0-9._-]/, "")
62
+ end
63
+
64
+ def read_file(file_key)
65
+ path = cache_path(file_key)
66
+ return nil unless File.exist?(path)
67
+
68
+ JSON.parse(File.read(path))
69
+ rescue JSON::ParserError
70
+ nil
71
+ end
72
+
73
+ def write_file(file_key, data)
74
+ FileUtils.mkdir_p(@cache_dir)
75
+ path = cache_path(file_key)
76
+ tmp = "#{path}.#{Process.pid}.tmp"
77
+ File.write(tmp, JSON.generate(data))
78
+ File.rename(tmp, path)
79
+ end
80
+
81
+ def cache_path(file_key)
82
+ File.join(@cache_dir, "#{file_key}.json")
83
+ end
84
+ end
85
+ end
data/lib/evilution/cli.rb CHANGED
@@ -106,28 +106,20 @@ module Evilution
106
106
  def add_core_options(opts)
107
107
  opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
108
108
  opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
109
- opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
110
- opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
111
- warn("Warning: --diff is deprecated and will be removed in a future version. " \
112
- "Use line-range targeting instead: evilution run lib/foo.rb:15-30")
113
- @options[:diff_base] = b
114
- end
109
+ opts.on("-f", "--format FORMAT", "Output format: text, json, html") { |f| @options[:format] = f.to_sym }
115
110
  end
116
111
 
117
112
  def add_filter_options(opts)
118
113
  opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
119
114
  opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
120
115
  opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
121
- opts.on("--no-coverage", "DEPRECATED: Has no effect and will be removed in a future version") do
122
- warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
123
- @options[:coverage] = false
124
- end
125
116
  end
126
117
 
127
118
  def add_flag_options(opts)
128
119
  opts.on("--fail-fast", "Stop after N surviving mutants " \
129
120
  "(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
130
121
  opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
122
+ opts.on("--incremental", "Cache killed/timeout results; skip re-running them on unchanged files") { @options[:incremental] = true }
131
123
  opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
132
124
  opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
133
125
  opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
@@ -9,29 +9,27 @@ module Evilution
9
9
  DEFAULTS = {
10
10
  timeout: 30,
11
11
  format: :text,
12
- diff_base: nil,
13
12
  target: nil,
14
13
  min_score: 0.0,
15
14
  integration: :rspec,
16
- coverage: true,
17
15
  verbose: false,
18
16
  quiet: false,
19
17
  jobs: 1,
20
18
  fail_fast: nil,
21
19
  baseline: true,
22
20
  isolation: :auto,
21
+ incremental: false,
23
22
  line_ranges: {},
24
23
  spec_files: []
25
24
  }.freeze
26
25
 
27
- attr_reader :target_files, :timeout, :format, :diff_base,
28
- :target, :min_score, :integration, :coverage, :verbose, :quiet,
29
- :jobs, :fail_fast, :baseline, :isolation, :line_ranges, :spec_files
26
+ attr_reader :target_files, :timeout, :format,
27
+ :target, :min_score, :integration, :verbose, :quiet,
28
+ :jobs, :fail_fast, :baseline, :isolation, :incremental, :line_ranges, :spec_files
30
29
 
31
30
  def initialize(**options)
32
31
  file_options = options.delete(:skip_config_file) ? {} : load_config_file
33
32
  merged = DEFAULTS.merge(file_options).merge(options)
34
- warn_removed_options(merged, file_options)
35
33
  assign_attributes(merged)
36
34
  freeze
37
35
  end
@@ -44,8 +42,8 @@ module Evilution
44
42
  format == :text
45
43
  end
46
44
 
47
- def diff?
48
- !diff_base.nil?
45
+ def html?
46
+ format == :html
49
47
  end
50
48
 
51
49
  def line_ranges?
@@ -64,6 +62,10 @@ module Evilution
64
62
  baseline
65
63
  end
66
64
 
65
+ def incremental?
66
+ incremental
67
+ end
68
+
67
69
  def self.file_options
68
70
  CONFIG_FILES.each do |path|
69
71
  next unless File.exist?(path)
@@ -102,9 +104,6 @@ module Evilution
102
104
 
103
105
  # Stop after N surviving mutants (default: disabled)
104
106
  # fail_fast: 1
105
-
106
- # DEPRECATED: Coverage filtering is deprecated and will be removed
107
- # coverage: true
108
107
  YAML
109
108
  end
110
109
 
@@ -125,17 +124,16 @@ module Evilution
125
124
  @target_files = Array(merged[:target_files])
126
125
  @timeout = merged[:timeout]
127
126
  @format = merged[:format].to_sym
128
- @diff_base = merged[:diff_base]
129
127
  @target = merged[:target]
130
128
  @min_score = merged[:min_score].to_f
131
129
  @integration = merged[:integration].to_sym
132
- @coverage = merged[:coverage]
133
130
  @verbose = merged[:verbose]
134
131
  @quiet = merged[:quiet]
135
132
  @jobs = validate_jobs(merged[:jobs])
136
133
  @fail_fast = validate_fail_fast(merged[:fail_fast])
137
134
  @baseline = merged[:baseline]
138
135
  @isolation = validate_isolation(merged[:isolation])
136
+ @incremental = merged[:incremental]
139
137
  @line_ranges = merged[:line_ranges] || {}
140
138
  @spec_files = Array(merged[:spec_files])
141
139
  end
@@ -160,18 +158,6 @@ module Evilution
160
158
  raise ConfigError, "jobs must be a positive integer, got #{value.inspect}"
161
159
  end
162
160
 
163
- def warn_removed_options(_merged, file_options)
164
- if file_options.key?(:coverage)
165
- warn("Warning: 'coverage' in config file is deprecated and ignored. " \
166
- "This option will be removed in a future version.")
167
- end
168
-
169
- return unless file_options[:diff_base]
170
-
171
- warn("Warning: 'diff_base' in config file is deprecated and will be removed in a future version. " \
172
- "Use line-range targeting instead: evilution run lib/foo.rb:15-30")
173
- end
174
-
175
161
  def load_config_file
176
162
  self.class.file_options
177
163
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "heuristic/noop_source"
4
+ require_relative "heuristic/method_body_nil"
5
+ require_relative "heuristic/alias_swap"
6
+ require_relative "heuristic/dead_code"
7
+
8
+ module Evilution
9
+ module Equivalent
10
+ class Detector
11
+ def initialize(heuristics: nil)
12
+ @heuristics = heuristics || default_heuristics
13
+ end
14
+
15
+ def call(mutations)
16
+ equivalent = []
17
+ remaining = []
18
+
19
+ mutations.each do |mutation|
20
+ if @heuristics.any? { |h| h.match?(mutation) }
21
+ equivalent << mutation
22
+ else
23
+ remaining << mutation
24
+ end
25
+ end
26
+
27
+ [equivalent, remaining]
28
+ end
29
+
30
+ private
31
+
32
+ def default_heuristics
33
+ [
34
+ Heuristic::NoopSource.new,
35
+ Heuristic::MethodBodyNil.new,
36
+ Heuristic::AliasSwap.new,
37
+ Heuristic::DeadCode.new
38
+ ]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Equivalent
5
+ module Heuristic
6
+ class AliasSwap
7
+ ALIAS_PAIRS = Set[
8
+ Set[:detect, :find],
9
+ Set[:length, :size],
10
+ Set[:collect, :map]
11
+ ].freeze
12
+
13
+ def match?(mutation)
14
+ return false unless mutation.operator_name == "send_mutation"
15
+
16
+ diff = mutation.diff
17
+ removed = extract_method(diff, "- ")
18
+ added = extract_method(diff, "+ ")
19
+ return false unless removed && added
20
+
21
+ pair = Set[removed.to_sym, added.to_sym]
22
+ ALIAS_PAIRS.include?(pair)
23
+ end
24
+
25
+ private
26
+
27
+ def extract_method(diff, prefix)
28
+ line = diff.split("\n").find { |l| l.start_with?(prefix) }
29
+ return nil unless line
30
+
31
+ match = line.match(/\.(\w+)(?:[\s(]|$)/)
32
+ match && match[1]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Equivalent
5
+ module Heuristic
6
+ class DeadCode
7
+ def match?(mutation)
8
+ return false unless mutation.operator_name == "statement_deletion"
9
+
10
+ node = mutation.subject.node
11
+ return false unless node
12
+
13
+ body = node.body
14
+ return false unless body.is_a?(Prism::StatementsNode)
15
+
16
+ statements = body.body
17
+ unreachable_lines = find_unreachable_lines(statements)
18
+ unreachable_lines.include?(mutation.line)
19
+ end
20
+
21
+ private
22
+
23
+ def find_unreachable_lines(statements)
24
+ lines = Set.new
25
+ found_unconditional_return = false
26
+
27
+ statements.each do |stmt|
28
+ if found_unconditional_return
29
+ collect_lines(stmt, lines)
30
+ elsif unconditional_return?(stmt)
31
+ found_unconditional_return = true
32
+ end
33
+ end
34
+
35
+ lines
36
+ end
37
+
38
+ def unconditional_return?(node)
39
+ node.is_a?(Prism::ReturnNode) ||
40
+ (node.is_a?(Prism::CallNode) && node.name == :raise)
41
+ end
42
+
43
+ def collect_lines(node, lines)
44
+ start_line = node.location.start_line
45
+ end_line = node.location.end_line
46
+ (start_line..end_line).each { |l| lines.add(l) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Equivalent
5
+ module Heuristic
6
+ class MethodBodyNil
7
+ def match?(mutation)
8
+ return false unless mutation.operator_name == "method_body_replacement"
9
+
10
+ node = mutation.subject.node
11
+ return false unless node
12
+
13
+ body = node.body
14
+ return true if body.nil? || body.is_a?(Prism::NilNode)
15
+
16
+ return body.body.first.is_a?(Prism::NilNode) if body.is_a?(Prism::StatementsNode) && body.body.length == 1
17
+
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Equivalent
5
+ module Heuristic
6
+ class NoopSource
7
+ def match?(mutation)
8
+ mutation.original_source == mutation.mutated_source
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -50,8 +50,9 @@ module Evilution
50
50
  runner = Runner.new(config: config)
51
51
  summary = runner.call
52
52
  report = Reporter::JSON.new.call(summary)
53
+ compact = trim_report(report)
53
54
 
54
- ::MCP::Tool::Response.new([{ type: "text", text: report }])
55
+ ::MCP::Tool::Response.new([{ type: "text", text: compact }])
55
56
  rescue Evilution::Error => e
56
57
  error_payload = build_error_payload(e)
57
58
  ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(error_payload) }], error: true)
@@ -98,6 +99,20 @@ module Evilution
98
99
  opts
99
100
  end
100
101
 
102
+ def trim_report(json_string)
103
+ data = ::JSON.parse(json_string)
104
+ strip_diffs(data, "killed")
105
+ strip_diffs(data, "neutral")
106
+ strip_diffs(data, "equivalent")
107
+ ::JSON.generate(data)
108
+ end
109
+
110
+ def strip_diffs(data, key)
111
+ return unless data[key]
112
+
113
+ data[key].each { |entry| entry.delete("diff") }
114
+ end
115
+
101
116
  def build_error_payload(error)
102
117
  error_type = case error
103
118
  when ConfigError then "config_error"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ArgumentNilSubstitution < Base
7
+ SKIP_TYPES = [
8
+ Prism::SplatNode,
9
+ Prism::KeywordHashNode,
10
+ Prism::BlockArgumentNode,
11
+ Prism::ForwardingArgumentsNode
12
+ ].freeze
13
+
14
+ def visit_call_node(node)
15
+ args = node.arguments&.arguments
16
+
17
+ if args && args.length >= 1 && positional_only?(args)
18
+ args.each_index do |i|
19
+ parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
20
+ replacement = parts.join(", ")
21
+
22
+ add_mutation(
23
+ offset: node.arguments.location.start_offset,
24
+ length: node.arguments.location.length,
25
+ replacement: replacement,
26
+ node: node
27
+ )
28
+ end
29
+ end
30
+
31
+ super
32
+ end
33
+
34
+ private
35
+
36
+ def positional_only?(args)
37
+ args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end