evilution 0.17.0 → 0.19.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +103 -33
  4. data/CHANGELOG.md +50 -0
  5. data/README.md +144 -50
  6. data/lib/evilution/ast/sorbet_sig_detector.rb +52 -0
  7. data/lib/evilution/baseline.rb +9 -1
  8. data/lib/evilution/cli.rb +398 -23
  9. data/lib/evilution/config.rb +10 -2
  10. data/lib/evilution/disable_comment.rb +90 -0
  11. data/lib/evilution/integration/rspec.rb +74 -5
  12. data/lib/evilution/isolation/fork.rb +10 -6
  13. data/lib/evilution/isolation/in_process.rb +14 -10
  14. data/lib/evilution/mcp/session_diff_tool.rb +5 -35
  15. data/lib/evilution/mutator/operator/collection_return.rb +33 -0
  16. data/lib/evilution/mutator/operator/defined_check.rb +16 -0
  17. data/lib/evilution/mutator/operator/keyword_argument.rb +91 -0
  18. data/lib/evilution/mutator/operator/multiple_assignment.rb +47 -0
  19. data/lib/evilution/mutator/operator/regex_capture.rb +43 -0
  20. data/lib/evilution/mutator/operator/scalar_return.rb +37 -0
  21. data/lib/evilution/mutator/operator/splat_operator.rb +46 -0
  22. data/lib/evilution/mutator/operator/yield_statement.rb +51 -0
  23. data/lib/evilution/mutator/registry.rb +9 -1
  24. data/lib/evilution/parallel/pool.rb +7 -53
  25. data/lib/evilution/parallel/work_queue.rb +265 -0
  26. data/lib/evilution/reporter/cli.rb +21 -1
  27. data/lib/evilution/reporter/html.rb +69 -3
  28. data/lib/evilution/reporter/json.rb +23 -2
  29. data/lib/evilution/reporter/suggestion.rb +29 -1
  30. data/lib/evilution/result/mutation_result.rb +5 -2
  31. data/lib/evilution/result/summary.rb +19 -2
  32. data/lib/evilution/runner.rb +123 -12
  33. data/lib/evilution/session/diff.rb +85 -0
  34. data/lib/evilution/spec_resolver.rb +13 -1
  35. data/lib/evilution/version.rb +1 -1
  36. data/lib/evilution.rb +11 -0
  37. data/script/memory_check +22 -0
  38. metadata +14 -2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0] - 2026-04-07
4
+
5
+ ### Added
6
+
7
+ - **Smart spec auto-detection** — `SpecResolver` maps source files to the closest matching spec using Rails conventions: controllers to request specs (`app/controllers/foo_controller.rb` → `spec/requests/foo_spec.rb`), models, services, Avo resources, and lib/ paths; falls back through parent directory patterns when exact match doesn't exist; warns when falling back to full suite so users know to use `--spec` (#530, #555)
8
+ - **`--spec-dir DIR` CLI flag** — include all `*_spec.rb` files in a directory recursively; composable with `--spec` for combining explicit files and directories (#513)
9
+ - **RSS tracking per mutation** — JSON output includes per-mutation RSS memory measurements for profiling memory behavior across mutations (#532)
10
+ - **Memory budget CI gate** — dedicated benchmark workflow with memory check step; uses realistic project classes as fixtures (#533, #567)
11
+ - **Worker hang protection** — `WorkQueue` item timeout prevents indefinite worker hangs; timeout handling for worker timing collection (#558, #559)
12
+
13
+ ### Fixed
14
+
15
+ - **InProcess `suppress_output` closing `/dev/null` handle** — prevent closing the shared `/dev/null` file descriptor which caused subsequent output suppression to fail (#569)
16
+ - **Double `Process.wait` in Fork isolation** — handle empty child processes without raising (#561)
17
+
18
+ ### Changed
19
+
20
+ - **RSpec integration memory management** — use clear hooks to release AST nodes and source strings between mutations, preventing memory retention across mutation runs (#543)
21
+
22
+ ## [0.18.0] - 2026-04-03
23
+
24
+ ### Added
25
+
26
+ - **Disable comments** — `# evilution:disable` comments to suppress mutations on specific lines, methods, or regions; inline disable for single lines, standalone disable before `def` for entire methods, range disable/enable pairs for arbitrary regions; `--show-disabled` flag reports skipped mutations in CLI, JSON, and HTML output (#321, #323, #325)
27
+ - **Sorbet `sig` filtering** — automatically detects and excludes mutations inside Sorbet `sig { ... }` blocks; cached per file for performance (#330, #334)
28
+ - **Session diff engine** — `Evilution::Session::Diff` compares two saved sessions, reporting fixed mutations, new survivors, persistent survivors, and score delta; identity matching by `[operator, file, line, subject]` (#333)
29
+ - **`session diff` CLI command** — `evilution session diff <base> <head>` with color-coded text output (green=fixed, red=new survivors, yellow=persistent) and `--format json` support (#336)
30
+ - **HTML report baseline comparison** — `--baseline-session PATH` overlays a saved session on the HTML report, highlighting regressions with badges and showing score delta (#339)
31
+ - **`util mutation` CLI command** — `evilution util mutation [-e CODE | FILE]` previews all mutations for a source file or inline Ruby snippet; supports `--format json` (#328)
32
+ - **`subjects` CLI command** — `evilution subjects [files...]` lists all mutation subjects (methods) with file locations and mutation counts; supports `--stdin` (#322)
33
+ - **`tests list` CLI command** — `evilution tests list [files...]` lists spec files mapped to source files via `SpecResolver` (#326)
34
+ - **`environment show` CLI command** — `evilution environment show` displays runtime environment: version, Ruby version, config path, and all active settings (#319)
35
+ - **Type-aware return mutation operators** — `CollectionReturn` replaces collection return values with type-aware alternatives (`[]`, `{}`); `ScalarReturn` replaces scalar return values with type-aware alternatives (`0`, `""`, `nil`) (#300, #304)
36
+ - **Keyword argument mutations** — `KeywordArgument` operator removes default values, removes optional keywords entirely, and removes `**kwargs` rest parameters (#345)
37
+ - **Multiple assignment mutations** — `MultipleAssignment` operator removes individual assignment targets and swaps 2-element order (#346)
38
+ - **Yield statement mutations** — `YieldStatement` operator removes yield, removes yield arguments, and replaces yield value with `nil` (#347)
39
+ - **Splat operator mutations** — `SplatOperator` operator removes `*` (splat) and `**` (double-splat) from method calls and array literals (#348)
40
+ - **`defined?` check mutations** — `DefinedCheck` operator replaces `defined?(expr)` with `true` (#356)
41
+ - **Regex capture reference mutations** — `RegexCapture` operator swaps numbered capture references (`$1`↔`$2`) and replaces with `nil` (#357)
42
+ - **Suggestion templates** — concrete RSpec suggestions for `collection_return` and `scalar_return` operators (#308)
43
+ - **Efficiency metrics** — summary output includes `efficiency` (killtime/wall-clock ratio), `mutations_per_second` throughput, and `killtime` aggregate; reported in CLI, JSON, and HTML (#313)
44
+ - **Parallel execution metrics** — worker statistics tracking with `busy_time`, `wall_time`, `idle_time`, and `utilization` per worker (#314)
45
+ - **Demand-driven work distribution** — `Parallel::Pool` uses pipe-based shared work queue with demand-driven dispatch and configurable prefetch; replaces batch-based distribution (#303, #307, #311)
46
+
47
+ ### Changed
48
+
49
+ - **Operator count** — 60 operators (up from 52), with new return-type, keyword, assignment, yield, splat, defined?, and regex capture operators
50
+ - **CLI reporter** — survived mutations now include subject name and code diffs (#341)
51
+ - **Dependency updates** — Ruby 3.3.10 → 3.3.11 in CI (#447), ruby/setup-ruby 1.295.0 → 1.299.0, rubygems/release-gem 1.1.4 → 1.2.0
52
+
3
53
  ## [0.17.0] - 2026-03-30
4
54
 
5
55
  ### Added
data/README.md CHANGED
@@ -29,27 +29,43 @@ evilution [command] [options] [files...]
29
29
 
30
30
  ### Commands
31
31
 
32
- | Command | Description | Default |
33
- |-----------|------------------------------------------|---------|
34
- | `run` | Execute mutation testing against files | Yes |
35
- | `init` | Generate `.evilution.yml` config file | |
36
- | `version` | Print version string | |
32
+ | Command | Description | Default |
33
+ |----------------------|----------------------------------------------------|---------|
34
+ | `run` | Execute mutation testing against files | Yes |
35
+ | `init` | Generate `.evilution.yml` config file | |
36
+ | `version` | Print version string | |
37
+ | `subjects [files]` | List mutation subjects with locations and counts | |
38
+ | `tests list [files]` | List spec files mapped to source files | |
39
+ | `session list` | List saved session results | |
40
+ | `session show FILE` | Display detailed session results | |
41
+ | `session diff A B` | Compare two sessions (fixed/new/persistent) | |
42
+ | `session gc --older-than D` | Garbage-collect sessions older than D (e.g. 30d) | |
43
+ | `util mutation` | Preview mutations for a file or inline code | |
44
+ | `environment show` | Display runtime environment and settings | |
37
45
 
38
46
  ### Options (for `run` command)
39
47
 
40
- | Flag | Type | Default | Description |
41
- |-------------------------|---------|--------------|---------------------------------------------------|
42
- | `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
43
- | `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
44
- | `--target METHOD` | String | _(none)_ | Only mutate the named method (e.g. `Foo::Bar#calculate`). |
45
- | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
46
- | `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `spec/`. |
47
- | `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Pool forks per batch; mutations run in-process inside workers. |
48
- | `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
49
- | `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
50
- | `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation. |
51
- | `--suggest-tests` | Boolean | false | Generate concrete RSpec test code in suggestions instead of static descriptions. |
52
- | `-q`, `--quiet` | Boolean | false | Suppress output. |
48
+ | Flag | Type | Default | Description |
49
+ |------------------------------|---------|--------------|---------------------------------------------------|
50
+ | `-t`, `--timeout N` | Integer | 30 | Per-mutation timeout in seconds. |
51
+ | `-f`, `--format FORMAT` | String | `text` | Output format: `text`, `json`, or `html`. |
52
+ | `--target EXPR` | String | _(none)_ | Only mutate matching methods. Supports method name (`Foo::Bar#calculate`), class (`Foo`), namespace wildcards (`Foo::Bar*`), method-type selectors (`Foo#`, `Foo.`), descendants (`descendants:Foo`), and source globs (`source:lib/**/*.rb`). |
53
+ | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
54
+ | `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to auto-detection via `SpecResolver`. |
55
+ | `--spec-dir DIR` | String | _(none)_ | Include all `*_spec.rb` files in DIR recursively. Composable with `--spec`. |
56
+ | `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Uses demand-driven work distribution with pipe-based IPC. |
57
+ | `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
58
+ | `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
59
+ | `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation. |
60
+ | `--suggest-tests` | Boolean | false | Generate concrete RSpec test code in suggestions instead of static descriptions. |
61
+ | `-q`, `--quiet` | Boolean | false | Suppress output. |
62
+ | `--stdin` | Boolean | false | Read target file paths from stdin (one per line). |
63
+ | `--incremental` | Boolean | false | Cache killed/timeout results; skip unchanged mutations on re-runs. |
64
+ | `--save-session` | Boolean | false | Persist results as timestamped JSON under `.evilution/results/`. |
65
+ | `--no-progress` | Boolean | _(enabled)_ | Disable the TTY progress bar. |
66
+ | `--show-disabled` | Boolean | false | Report mutations skipped by `# evilution:disable` comments. |
67
+ | `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
68
+ | `-e CODE`, `--eval CODE` | String | _(none)_ | Inline Ruby code for `util mutation` command. |
53
69
 
54
70
  ### Exit Codes
55
71
 
@@ -66,15 +82,43 @@ Generate default config: `bundle exec evilution init`
66
82
  Creates `.evilution.yml`:
67
83
 
68
84
  ```yaml
69
- # timeout: 10 # seconds per mutation
70
- # format: text # text | json
71
- # min_score: 0.0 # 0.0–1.0
72
- # integration: rspec # test framework
73
- # suggest_tests: false # concrete RSpec test code in suggestions
85
+ # timeout: 30 # seconds per mutation
86
+ # format: text # text | json | html
87
+ # min_score: 0.0 # 0.0–1.0
88
+ # integration: rspec # test framework
89
+ # suggest_tests: false # concrete RSpec test code in suggestions
90
+ # save_session: false # persist results under .evilution/results/
91
+ # show_disabled: false # report mutations skipped by disable comments
92
+ # baseline_session: null # path to session file for HTML comparison
93
+ # ignore_patterns: [] # AST patterns to exclude (see docs/ast_pattern_syntax.md)
94
+ # progress: true # TTY progress bar
74
95
  ```
75
96
 
76
97
  **Precedence**: CLI flags override `.evilution.yml` values.
77
98
 
99
+ ## Disable Comments
100
+
101
+ Suppress mutations on specific code with inline comments:
102
+
103
+ ```ruby
104
+ # Disable a single line
105
+ log(message) # evilution:disable
106
+
107
+ # Disable an entire method (place comment immediately before def)
108
+ # evilution:disable
109
+ def infrastructure_method
110
+ # no mutations generated for this method body
111
+ end
112
+
113
+ # Disable a region
114
+ # evilution:disable
115
+ setup_logging
116
+ configure_metrics
117
+ # evilution:enable
118
+ ```
119
+
120
+ Use `--show-disabled` to see which mutations were skipped.
121
+
78
122
  ## JSON Output Schema
79
123
 
80
124
  Use `--format json` for machine-readable output. Schema:
@@ -112,30 +156,72 @@ Use `--format json` for machine-readable output. Schema:
112
156
 
113
157
  **Key metric**: `summary.score` — the mutation score. Higher is better. 1.0 means all mutations were caught.
114
158
 
115
- ## Mutation Operators (18 total)
159
+ ## Mutation Operators (60 total)
116
160
 
117
161
  Each operator name is stable and appears in JSON output under `survived[].operator`.
118
162
 
119
- | Operator | What it does | Example |
120
- |---------------------------|-------------------------------------------|------------------------------------|
121
- | `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
122
- | `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
123
- | `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
124
- | `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
125
- | `nil_replacement` | Replace expression with `nil` | `expr` -> `nil` |
126
- | `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
127
- | `float_literal` | Boundary-value float mutations | `f` -> `0.0`, `1.0` |
128
- | `string_literal` | Empty the string | `"str"` -> `""` |
129
- | `array_literal` | Empty the array | `[a, b]` -> `[]` |
130
- | `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
131
- | `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
132
- | `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
133
- | `conditional_branch` | Remove if/else branch | Deletes branch body |
134
- | `statement_deletion` | Remove statements from method bodies | Deletes a statement |
135
- | `method_body_replacement` | Replace entire method body with `nil` | Method body -> `nil` |
136
- | `negation_insertion` | Negate predicate methods | `x.empty?` -> `!x.empty?` |
137
- | `return_value_removal` | Strip return values | `return x` -> `return` |
138
- | `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
163
+ | Operator | What it does | Example |
164
+ |---|---|---|
165
+ | `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
166
+ | `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
167
+ | `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
168
+ | `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
169
+ | `nil_replacement` | Replace `nil` with `true`, `false`, `0`, `""` | `nil` -> `true` |
170
+ | `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
171
+ | `float_literal` | Boundary-value float mutations | `f` -> `0.0`, `1.0` |
172
+ | `string_literal` | Empty the string | `"str"` -> `""` |
173
+ | `array_literal` | Empty the array | `[a, b]` -> `[]` |
174
+ | `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
175
+ | `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
176
+ | `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
177
+ | `conditional_branch` | Remove if/else branch | Deletes branch body |
178
+ | `conditional_flip` | Flip `if` to `unless` and vice versa | `if cond` -> `unless cond` |
179
+ | `statement_deletion` | Remove statements from method bodies | Deletes a statement |
180
+ | `method_body_replacement` | Replace entire method body with `nil` | Method body -> `nil` |
181
+ | `negation_insertion` | Negate predicate methods | `x.empty?` -> `!x.empty?` |
182
+ | `return_value_removal` | Strip return values | `return x` -> `return` |
183
+ | `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
184
+ | `collection_return` | Replace collection return values | `return [1]` -> `return []` |
185
+ | `scalar_return` | Replace scalar return values | `return 42` -> `return 0` |
186
+ | `method_call_removal` | Remove method calls, keep receiver | `obj.foo(x)` -> `obj` |
187
+ | `argument_removal` | Remove individual arguments | `foo(a, b)` -> `foo(b)` |
188
+ | `argument_nil_substitution` | Replace arguments with `nil` | `foo(a, b)` -> `foo(nil, b)` |
189
+ | `keyword_argument` | Remove keyword defaults/params | `def foo(bar: 42)` -> `def foo(bar:)` |
190
+ | `multiple_assignment` | Remove targets or swap order | `a, b = 1, 2` -> `b, a = 1, 2` |
191
+ | `block_removal` | Remove blocks from method calls | `items.map { \|x\| x * 2 }` -> `items.map` |
192
+ | `range_replacement` | Swap inclusive/exclusive ranges | `1..10` -> `1...10` |
193
+ | `regexp_mutation` | Replace regexp with always/never matching | `/pat/` -> `/a\A/` |
194
+ | `receiver_replacement` | Drop explicit `self` receiver | `self.foo` -> `foo` |
195
+ | `send_mutation` | Swap semantically related methods | `detect` -> `find`, `map` -> `flat_map` |
196
+ | `compound_assignment` | Swap compound assignment operators | `+=` -> `-=`, `&&=` -> `\|\|=` |
197
+ | `local_variable_assignment` | Replace variable assignment with `nil` | `x = expr` -> `x = nil` |
198
+ | `instance_variable_write` | Replace ivar assignment with `nil` | `@x = expr` -> `@x = nil` |
199
+ | `class_variable_write` | Replace cvar assignment with `nil` | `@@x = expr` -> `@@x = nil` |
200
+ | `global_variable_write` | Replace gvar assignment with `nil` | `$x = expr` -> `$x = nil` |
201
+ | `mixin_removal` | Remove include/extend/prepend | `include Foo` -> removed |
202
+ | `superclass_removal` | Remove class inheritance | `class Foo < Bar` -> `class Foo` |
203
+ | `rescue_removal` | Remove rescue clauses | Deletes rescue block |
204
+ | `rescue_body_replacement` | Replace rescue body with `nil` | Rescue body -> `nil` |
205
+ | `inline_rescue` | Remove inline rescue fallback | `expr rescue val` -> `expr` |
206
+ | `ensure_removal` | Remove ensure blocks | Deletes ensure block |
207
+ | `break_statement` | Remove break statements | `break` -> removed |
208
+ | `next_statement` | Remove next statements | `next` -> removed |
209
+ | `redo_statement` | Remove redo statements | `redo` -> removed |
210
+ | `bang_method` | Swap bang with non-bang methods | `sort!` -> `sort` |
211
+ | `bitwise_replacement` | Swap bitwise operators | `a & b` -> `a \| b` |
212
+ | `bitwise_complement` | Remove or swap `~` | `~x` -> `x`, `~x` -> `-x` |
213
+ | `zsuper_removal` | Replace implicit `super` with `nil` | `super` -> `nil` |
214
+ | `explicit_super_mutation` | Mutate explicit super arguments | `super(a, b)` -> `super` |
215
+ | `index_to_fetch` | Replace `[]` with `.fetch()` | `h[k]` -> `h.fetch(k)` |
216
+ | `index_to_dig` | Replace `[]` chains with `.dig()` | `h[a][b]` -> `h.dig(a, b)` |
217
+ | `index_assignment_removal` | Remove `[]=` assignments | `h[k] = v` -> removed |
218
+ | `pattern_matching_guard` | Remove/negate pattern guards | `in x if cond` -> `in x` |
219
+ | `pattern_matching_alternative` | Remove/reorder alternatives | `pat1 \| pat2` -> `pat1` |
220
+ | `pattern_matching_array` | Remove/wildcard array elements | `[a, b]` -> `[a, _]` |
221
+ | `yield_statement` | Remove yield or its arguments | `yield(x)` -> `yield` |
222
+ | `splat_operator` | Remove splat/double-splat | `foo(*args)` -> `foo(args)` |
223
+ | `defined_check` | Replace `defined?` with `true` | `defined?(x)` -> `true` |
224
+ | `regex_capture` | Swap or nil-ify capture refs | `$1` -> `$2`, `$1` -> `nil` |
139
225
 
140
226
  ## MCP Server (AI Agent Integration)
141
227
 
@@ -160,7 +246,14 @@ Create a `.mcp.json` file in your project root:
160
246
 
161
247
  If using Bundler, set the command to `bundle` and args to `["exec", "evilution", "mcp"]`.
162
248
 
163
- The server exposes an `evilution-mutate` tool that accepts target files, method targets, spec overrides, parallelism, and timeout options — returning structured JSON results directly to the agent.
249
+ The server exposes the following tools:
250
+
251
+ | Tool | Description |
252
+ |---|---|
253
+ | `evilution-mutate` | Run mutation testing on target files with structured JSON results |
254
+ | `evilution-session-list` | Browse saved session history |
255
+ | `evilution-session-show` | Display detailed session results |
256
+ | `evilution-session-diff` | Compare two sessions (fixed/new/persistent survivors, score delta) |
164
257
 
165
258
  ### Verbosity Control
166
259
 
@@ -274,11 +367,12 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
274
367
 
275
368
  1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
276
369
  2. **Extract** — Methods are identified as mutation subjects
277
- 3. **Mutate** — Operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing)
278
- 4. **Isolate** — Default isolation is in-process; `--isolation fork` uses forked child processes. Parallel mode (`--jobs N`) always uses in-process isolation inside pool workers to avoid double forking
279
- 5. **Test** — RSpec executes against the mutated source
280
- 6. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
281
- 7. **Report** — Results aggregated into text or JSON, including peak memory usage
370
+ 3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
371
+ 4. **Mutate** — 60 operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing)
372
+ 5. **Isolate** — Default isolation is in-process; `--isolation fork` uses forked child processes. Parallel mode (`--jobs N`) always uses in-process isolation inside pool workers to avoid double forking
373
+ 6. **Test** — RSpec executes against the mutated source
374
+ 7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
375
+ 8. **Report** — Results aggregated into text, JSON, or HTML, including efficiency metrics and peak memory usage
282
376
 
283
377
  ## Repository
284
378
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Evilution::AST::SorbetSigDetector
6
+ def call(source)
7
+ return [] if source.empty?
8
+
9
+ result = Prism.parse(source)
10
+ return [] if result.failure?
11
+
12
+ ranges = []
13
+ collect_sig_ranges(result.value, ranges, :byte)
14
+ ranges
15
+ end
16
+
17
+ def line_ranges(source)
18
+ return [] if source.empty?
19
+
20
+ result = Prism.parse(source)
21
+ return [] if result.failure?
22
+
23
+ ranges = []
24
+ collect_sig_ranges(result.value, ranges, :line)
25
+ ranges
26
+ end
27
+
28
+ private
29
+
30
+ def collect_sig_ranges(node, ranges, mode)
31
+ if sig_block?(node)
32
+ loc = node.location
33
+ ranges << if mode == :byte
34
+ (loc.start_offset...loc.end_offset)
35
+ else
36
+ (loc.start_line..loc.end_line)
37
+ end
38
+ end
39
+
40
+ node.child_nodes.each do |child|
41
+ collect_sig_ranges(child, ranges, mode) if child
42
+ end
43
+ end
44
+
45
+ def sig_block?(node)
46
+ node.is_a?(Prism::CallNode) &&
47
+ node.name == :sig &&
48
+ node.receiver.nil? &&
49
+ node.arguments.nil? &&
50
+ !node.block.nil?
51
+ end
52
+ end
@@ -93,6 +93,14 @@ class Evilution::Baseline
93
93
  private
94
94
 
95
95
  def resolve_unique_spec_files(subjects)
96
- subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
96
+ warned = Set.new
97
+ subjects.map do |s|
98
+ resolved = @spec_resolver.call(s.file_path)
99
+ if resolved.nil? && warned.add?(s.file_path)
100
+ warn "[evilution] No matching spec found for #{s.file_path}, running full suite. " \
101
+ "Use --spec to specify the spec file."
102
+ end
103
+ resolved || "spec"
104
+ end.uniq
97
105
  end
98
106
  end