evilution 0.6.0 → 0.7.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: 90bb10673d940d85a42206ae1b20de288321dfb4f61802216dc4e9b68175f46f
4
- data.tar.gz: a769255c8fd242d3432e811d36655eec1dae0ac6ef3c7288c0d68fe2c390182c
3
+ metadata.gz: c22da4a2f18a35a0947f708163505e560fef557fb50595cbb735676d220fb17c
4
+ data.tar.gz: a3427ee2af916eed4e59a73eb6431626e0089b7fb826136675e228ed712ddf08
5
5
  SHA512:
6
- metadata.gz: 3c06ec7f6e81c3f8427b4021dddd298d3111827896e0eae72ba1d764e076523d4ce63aac83438705f559199534b4408aac5683aeddf2c8566daa7ffd20015d88
7
- data.tar.gz: cca406c93807c560cc70c7a5183c14d0c15fe139e12fe35f733341d8db839f28e74a7b34557ad5dabfdf3b7cbb34a76f8a954e30e4b0d91d0a9b5fb60ffed04f
6
+ metadata.gz: ebf55a8e9623bededf357bb684ff2a3d3a7e3af2e743e91d5d0875488118184cd1a2699905df5752f4784c8c7e6bef8c24f766430adf325850690ed0cd70ed45
7
+ data.tar.gz: c06b6ecece15c2695bc86f0baaed67217887777336abeb6635b263d3aa68bc17f4d2b68be826ccc4fa8ae9ae2794d167174b24e380675e5660c19b759bd54c1c
@@ -1 +1 @@
1
- 1773676731
1
+ 1773850606
data/.beads/issues.jsonl CHANGED
@@ -62,7 +62,7 @@
62
62
  {"id":"EV-3.8","title":"Implement ArrayLiteral operator","description":"Targets ArrayNode with elements. Mutation: [a, b, c] -> []. File: lib/evilution/mutator/operator/array_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.734617709+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:10:56.397565925+07:00","closed_at":"2026-03-02T11:10:56.397565925+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.8","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.8","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
63
63
  {"id":"EV-3.9","title":"Implement HashLiteral operator","description":"Targets HashNode with pairs. Mutation: {k: v} -> {}. File: lib/evilution/mutator/operator/hash_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.840779748+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:10:56.397570989+07:00","closed_at":"2026-03-02T11:10:56.397570989+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.9","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.9","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
64
64
  {"id":"EV-30","title":"Epic: Smarter Suggestions","description":"Improve the suggestion field in mutation results to provide concrete, actionable test code that agents can use directly.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:03.590579846+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:03.590579846+07:00","dependencies":[{"issue_id":"EV-30","depends_on_id":"EV-31","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
65
- {"id":"EV-31","title":"Generate concrete RSpec test code in suggestions","description":"Replace prose suggestions like 'add a test that checks the boundary' with actual RSpec code snippets that would kill the mutant. Use the mutation's operator, file context, and method signature to generate a concrete test example. The suggestion should be a valid RSpec 'it' block that an agent can drop into a spec file.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:05.964542284+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:05.964542284+07:00"}
65
+ {"id":"EV-31","title":"Generate concrete RSpec test code in suggestions","description":"Replace prose suggestions like 'add a test that checks the boundary' with actual RSpec code snippets that would kill the mutant. Use the mutation's operator, file context, and method signature to generate a concrete test example. The suggestion should be a valid RSpec 'it' block that an agent can drop into a spec file.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:05.964542284+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:05.964542284+07:00","dependencies":[{"issue_id":"EV-31","depends_on_id":"EV-55","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-56","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-57","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-58","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-59","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-60","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-31","depends_on_id":"EV-61","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
66
66
  {"id":"EV-32","title":"Epic: Zero-friction Defaults","description":"Make evilution work well with zero configuration. Auto-detect what to mutate and which specs to run based on git state and file conventions.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:06.843446356+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311639679+07:00","closed_at":"2026-03-16T10:11:01.311639679+07:00","close_reason":"Already merged and released in v0.4.0","dependencies":[{"issue_id":"EV-32","depends_on_id":"EV-33","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-32","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
67
67
  {"id":"EV-33","title":"Auto-detect changed files from git merge base","description":"When evilution is run with no file arguments, default to mutating files changed since the merge base of the current branch (git merge-base HEAD main). This makes 'evilution run --format json' just work in a feature branch without specifying files. Should be skippable if explicit files are provided.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:08.235421922+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311622041+07:00","closed_at":"2026-03-16T10:11:01.311622041+07:00","close_reason":"Already merged and released in v0.4.0"}
68
68
  {"id":"EV-34","title":"Convention-based spec file resolution","description":"Implement a mapping from source files to their spec files using Ruby/RSpec conventions: lib/foo/bar.rb -> spec/foo/bar_spec.rb, app/models/user.rb -> spec/models/user_spec.rb. This is a foundational piece used by both zero-friction defaults (auto-detect specs) and per-mutation spec targeting (run only relevant specs). Should handle common Rails and gem layouts.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:09.380269033+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311627178+07:00","closed_at":"2026-03-16T10:11:01.311627178+07:00","close_reason":"Already merged and released in v0.4.0"}
@@ -86,7 +86,7 @@
86
86
  {"id":"EV-42","title":"Expand mutation operators (method call removal, receiver replacement, etc.)","description":"Currently 18 operators generating ~19 mutations vs mutant's 78 for the same method. Missing operators: method call removal, self receiver replacement, additional boolean logic mutations, nil substitutions. Higher operator count catches more subtle bugs and produces a more meaningful mutation score.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:03.039944039+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T09:57:03.039944039+07:00"}
87
87
  {"id":"EV-43","title":"Fix --version flag (returns 'version unknown')","description":"Running 'evilution --version' returns 'version unknown' instead of the actual version. The 'evilution version' subcommand works, but --version as a flag does not. Users expect --version to work.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:05.048211823+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:31:01.045381336+07:00","closed_at":"2026-03-16T10:31:01.045381336+07:00","close_reason":"Fixed and merged"}
88
88
  {"id":"EV-44","title":"Re-introduce parallel execution","description":"Bring back parallel mutation execution. Consider whether two-level fork model is still right, temp-file isolation needs, and auto-detecting when parallelism is worthwhile. GH #35.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T10:15:42.050318104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T16:26:30.341909415+07:00","closed_at":"2026-03-16T16:26:30.341909415+07:00","close_reason":"PR #90 merged — process-based parallel pool with --jobs flag"}
89
- {"id":"EV-45","title":"Epic: Close mutation operator gap with mutant","description":"Track all missing mutation operators to approach mutant's ~78 mutations per method. Currently at ~19 with 18 operators. This is a long-term effort — prioritize high-value operators first.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:07.597313227+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T21:50:07.597313227+07:00","dependencies":[{"issue_id":"EV-45","depends_on_id":"EV-46","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-47","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-48","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-49","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-50","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-51","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-52","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-53","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-54","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-42","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
89
+ {"id":"EV-45","title":"Epic: Close mutation operator gap with mutant","description":"Track all missing mutation operators to approach mutant's ~78 mutations per method. Currently at ~19 with 18 operators. This is a long-term effort — prioritize high-value operators first.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:07.597313227+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T10:14:41.815885279+07:00","dependencies":[{"issue_id":"EV-45","depends_on_id":"EV-46","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-47","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-48","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-49","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-50","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-51","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-52","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-53","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-54","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-45","depends_on_id":"EV-42","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
90
90
  {"id":"EV-46","title":"Operator: MethodCallRemoval","description":"Remove method call, keep receiver. e.g. obj.foo(x) → obj. High bug-finding value — catches untested side effects.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:22.890467537+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:29:15.588415782+07:00","closed_at":"2026-03-17T09:29:15.588415782+07:00","close_reason":"PR merged — MethodCallRemoval operator (19th operator)"}
91
91
  {"id":"EV-47","title":"Operator: BlockRemoval","description":"Remove block from method call. e.g. items.map { |x| x * 2 } → items.map. Catches untested block logic.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.067266925+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T21:50:23.067266925+07:00"}
92
92
  {"id":"EV-48","title":"Operator: RangeReplacement","description":"Swap inclusive/exclusive ranges. e.g. 1..10 → 1...10 and vice versa. Trivial to implement.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.172934653+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T21:50:23.172934653+07:00"}
@@ -100,10 +100,40 @@
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":"open","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-16T21:50:23.379431887+07:00"}
102
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-16T21:50:23.483414117+07:00"}
103
- {"id":"EV-52","title":"Operator: ArgumentRemoval","description":"Remove individual arguments from method calls. e.g. foo(a, b) → foo(a). More complex AST handling.","status":"open","priority":4,"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-16T21:50:23.587291445+07:00"}
104
- {"id":"EV-53","title":"Operator: ReceiverReplacement","description":"Drop explicit self receiver. e.g. self.foo → foo. Low bug-finding value.","status":"open","priority":4,"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-16T21:50:23.692244528+07:00"}
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":4,"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-16T21:50:23.797359784+07:00"}
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-17T23:16:28.920737471+07:00","closed_at":"2026-03-17T23:16:28.920737471+07:00","close_reason":"Merged"}
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-17T10:14:41.617278357+07:00"}
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-17T10:14:41.719145856+07:00"}
106
+ {"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
+ {"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
+ {"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"}
109
+ {"id":"EV-58","title":"Concrete suggestions: Collection operators","description":"Generate concrete RSpec it blocks for ArrayLiteral, HashLiteral, and CollectionReplacement mutations. Test should assert contents or size.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.577189348+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.577189348+07:00"}
110
+ {"id":"EV-59","title":"Concrete suggestions: Conditional operators","description":"Generate concrete RSpec it blocks for ConditionalNegation and ConditionalBranch mutations. Test should exercise both branches.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.676644345+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.676644345+07:00"}
106
111
  {"id":"EV-6","title":"Fix pool fork tests hanging in CI and WSL2","description":"The spec/evilution/parallel/pool_spec.rb fork-based tests hang on both WSL2 and GitHub Actions. CI currently excludes them via --exclude-pattern. Root cause is likely double-fork (Pool forks workers, each Worker uses Isolation::Fork which forks again) causing pipe/process management issues. Needs investigation and fix so pool tests run reliably in CI.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:43.62587758+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.831163433+07:00","closed_at":"2026-03-05T12:31:51.831163433+07:00","close_reason":"Closed"}
112
+ {"id":"EV-60","title":"Concrete suggestions: Structural operators","description":"Generate concrete RSpec it blocks for StatementDeletion, MethodBodyReplacement, ReturnValueRemoval, and MethodCallRemoval mutations. Test should depend on side effect or return value.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-17T09:58:33.777078104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-17T09:58:33.777078104+07:00"}
113
+ {"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
+ {"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
+ {"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"}
121
+ {"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"}
107
122
  {"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
+ {"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"]}
124
+ {"id":"EV-70.1","title":"Add RSS memory measurement utility","description":"Create a lightweight Memory module that reads RSS from /proc/$$/status (Linux) with no gem dependencies. Provide Memory.rss_mb method returning current process RSS in MB. This is the foundation for all other memory instrumentation.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:45.894741371+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T19:02:42.603502332+07:00","closed_at":"2026-03-18T19:02:42.603502332+07:00","close_reason":"Implemented Memory module with rss_kb, rss_mb, rss_kb_for(pid), and delta methods. 8 specs passing.","external_ref":"gh-125","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.1","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
125
+ {"id":"EV-70.10","title":"Release Prism AST nodes after mutation generation","description":"Subject holds @node (Prism AST) which is only needed during registry.mutations_for(subject). After mutations are generated, the AST node is never used again but stays retained through the results chain. Clear or release the node reference after mutation generation.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:10.923467438+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T12:19:13.508955682+07:00","closed_at":"2026-03-19T12:19:13.508955682+07:00","close_reason":"Added release_node! to Subject, wired into Runner generate_mutations lazy pipeline to release Prism AST nodes after mutation generation.","external_ref":"gh-134","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.10","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
126
+ {"id":"EV-70.11","title":"Reduce Marshal payload size in Parallel::Pool","description":"Pool workers Marshal.dump full MutationResult (including Mutation with source strings and Subject with AST) through pipes. Serialize only the minimal result data (status, duration, test_command, diff) to reduce memory pressure on both sides of the pipe.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:14.224229894+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T12:40:38.91378273+07:00","closed_at":"2026-03-19T12:40:38.91378273+07:00","close_reason":"Worker block now returns compact hash instead of full MutationResult through Marshal pipe. Parent rebuilds MutationResult from batch mutations and compact data.","external_ref":"gh-135","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.11","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
127
+ {"id":"EV-70.12","title":"Drain StringIO buffers in InProcess isolation","description":"InProcess isolation captures stdout/stderr into StringIO objects that grow unbounded during test execution. Either drain/truncate them periodically, redirect to File::NULL like Fork does, or cap buffer size.","status":"closed","priority":3,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:17.052097022+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T12:49:51.334565974+07:00","closed_at":"2026-03-19T12:49:51.334565974+07:00","close_reason":"Replaced StringIO buffers with File::NULL in InProcess isolation suppress_output to prevent unbounded memory growth.","external_ref":"gh-136","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.12","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
128
+ {"id":"EV-70.13","title":"Add memory budget RSpec specs","description":"Add specs that run N mutations in a loop and assert RSS doesn't grow linearly. Test sequential, parallel, fork, and in-process paths. Use a custom RSpec matcher like expect { block }.to not_leak_memory(iterations: 20, max_growth_mb: 10). These catch memory regressions in CI.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:20.033283488+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-19T13:53:32.627830139+07:00","closed_at":"2026-03-19T13:53:32.627830139+07:00","close_reason":"Added rake memory:check task with LeakCheck class. Runs 4 checks (InProcess, Fork, mutation gen+strip, parallel pool) in a separate process, samples RSS at intervals, exits non-zero on regression. Configurable via MEMORY_CHECK_ITERATIONS and MEMORY_CHECK_MAX_GROWTH_KB env vars.","external_ref":"gh-137","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.13","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.13","depends_on_id":"EV-70.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
129
+ {"id":"EV-70.14","title":"Add GC stats tracking to verbose output","description":"When verbose mode is enabled, include GC.stat metrics (heap_live_slots, total_allocated_objects, total_freed_objects) at mutation boundaries. Helps distinguish real leaks from deferred GC.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:52:22.258742699+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T23:57:52.062251033+07:00","closed_at":"2026-03-18T23:57:52.062251033+07:00","close_reason":"Added total_allocated_objects and total_freed_objects to both phase logs and per-mutation diagnostics via shared gc_stats_string helper","external_ref":"gh-138","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.14","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.14","depends_on_id":"EV-70.5","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
130
+ {"id":"EV-70.2","title":"Instrument Runner with per-phase memory snapshots","description":"Add memory measurements at key points in Runner#call: after parse_subjects, after generate_mutations, per-batch in parallel mode, and at completion. Emit to stderr when config.verbose is true. Report peak RSS in Summary.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:48.544642382+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T19:56:39.228340976+07:00","closed_at":"2026-03-18T19:56:39.228340976+07:00","close_reason":"Added per-phase memory snapshots to Runner (parse_subjects, generate_mutations, run_mutations, per-batch). Gated on config.verbose && !config.quiet. 7 new specs.","external_ref":"gh-126","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.2","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.2","depends_on_id":"EV-70.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
131
+ {"id":"EV-70.3","title":"Instrument fork isolation with child memory reporting","description":"In Isolation::Fork, measure child process RSS after test execution and pipe it back alongside the test result via Marshal. Parent collects per-mutation memory stats. Enables detecting COW collapse and anomalous child memory usage.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:51.289345346+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T22:48:01.087933624+07:00","closed_at":"2026-03-18T22:48:01.087933624+07:00","close_reason":"Child process measures RSS after test execution via Memory.rss_kb, pipes it back in Marshal result. MutationResult gains child_rss_kb attribute. 4 new specs.","external_ref":"gh-127","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.3","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.3","depends_on_id":"EV-70.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
132
+ {"id":"EV-70.4","title":"Instrument in-process isolation with memory deltas","description":"In Isolation::InProcess, measure RSS before and after each mutation execution. Track cumulative delta to detect linear memory growth (leak signature). Report via verbose output.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:53.296371954+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T23:02:52.770787172+07:00","closed_at":"2026-03-18T23:02:52.770787172+07:00","close_reason":"InProcess isolation measures RSS before/after each mutation, computes memory_delta_kb. MutationResult gains memory_delta_kb attribute. Nil on timeout or when RSS unavailable. 4 new specs.","external_ref":"gh-128","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.4","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.4","depends_on_id":"EV-70.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
133
+ {"id":"EV-70.5","title":"Wire up config.verbose flag for memory diagnostics","description":"The verbose flag is declared in Config but unused. Wire it up so that when --verbose is passed, memory instrumentation output is emitted to stderr. Include per-mutation RSS, delta, and GC.stat[:heap_live_slots].","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:56.498046888+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T23:28:36.076408766+07:00","closed_at":"2026-03-18T23:28:36.076408766+07:00","close_reason":"Verbose flag now emits per-mutation diagnostics: child_rss (fork), memory delta (in-process), and GC heap_live_slots. Output gated on --verbose && !--quiet. 4 new specs (11 total in runner_memory_spec).","external_ref":"gh-129","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.5","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.5","depends_on_id":"EV-70.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
134
+ {"id":"EV-70.6","title":"Add memory stats to JSON and CLI reporters","description":"Extend Reporter::JSON and Reporter::CLI to include memory stats in output when available: peak RSS, per-mutation memory, total memory delta. Add peak_memory_mb and memory_deltas fields to Summary result object.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-18T18:51:59.909768892+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-18T23:38:38.026960498+07:00","closed_at":"2026-03-18T23:38:38.026960498+07:00","close_reason":"Added peak_memory_mb to Summary, memory fields to JSON/CLI reporters, 10 new specs","external_ref":"gh-130","labels":["v0.7.0"],"dependencies":[{"issue_id":"EV-70.6","depends_on_id":"EV-70","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.6","depends_on_id":"EV-70.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-70.6","depends_on_id":"EV-70.5","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
135
+ {"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
+ {"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
+ {"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"}]}
108
138
  {"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."}
109
139
  {"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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0] - 2026-03-19
4
+
5
+ ### Added
6
+
7
+ - **ArgumentRemoval operator** — new mutation operator that removes individual arguments from method calls with 2+ positional args (e.g. `foo(a, b, c)` → `foo(b, c)`, `foo(a, c)`, `foo(a, b)`)
8
+ - **Memory observability** — verbose mode (`-v`) now logs RSS and GC stats (heap_live_slots, allocated, freed) after each phase and per-mutation; includes child_rss and memory delta when available
9
+ - **Peak memory reporting** — text and JSON output include peak memory usage across all mutations
10
+ - **`rake memory:check`** — standalone memory leak detection task for pre-release validation; runs 4 checks (InProcess, Fork, mutation generation, parallel pool) and exits non-zero on regression; configurable via `MEMORY_CHECK_ITERATIONS` and `MEMORY_CHECK_MAX_GROWTH_KB` env vars
11
+ - **Neutral mutation detection** — baseline test suite run detects pre-existing failures; mutations in already-failing code are marked `neutral` instead of `survived`
12
+
13
+ ### Fixed
14
+
15
+ - **Memory leak: source string retention** — `Mutation#strip_sources!` caches the diff then nils out original/mutated source strings after execution, allowing GC to reclaim them
16
+ - **Memory leak: AST node retention** — `Subject#release_node!` releases Prism AST nodes after mutation generation; nodes are no longer retained through the results chain
17
+ - **Memory leak: StringIO buffer growth** — InProcess isolation now redirects output to `/dev/null` instead of accumulating in unbounded StringIO buffers
18
+ - **Memory leak: Marshal payload bloat** — parallel pool workers now serialize only compact result hashes (status, duration, metrics) instead of full MutationResult objects with embedded Mutation/Subject/AST trees
19
+ - **Memory leak: double forking** — parallel mode uses InProcess isolation inside pool workers to avoid fork-inside-fork; sequential mode continues using Fork isolation
20
+
3
21
  ## [0.6.0] - 2026-03-17
4
22
 
5
23
  ### Added
data/README.md CHANGED
@@ -46,7 +46,10 @@ evilution [command] [options] [files...]
46
46
  | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
47
47
  | `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `spec/`. |
48
48
  | `--no-coverage` | Boolean | false | **DEPRECATED, NO-OP**: Kept for backward compatibility. Will be removed. |
49
- | `-v`, `--verbose` | Boolean | false | Verbose output. |
49
+ | `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Pool forks per batch; mutations run in-process inside workers. |
50
+ | `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
51
+ | `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
52
+ | `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation. |
50
53
  | `-q`, `--quiet` | Boolean | false | Suppress output. |
51
54
 
52
55
  ### Exit Codes
@@ -87,7 +90,8 @@ Use `--format json` for machine-readable output. Schema:
87
90
  "timed_out": "integer — mutations that exceeded timeout",
88
91
  "errors": "integer — mutations that caused unexpected errors",
89
92
  "score": "float — killed / (total - errors), range 0.0-1.0, rounded to 4 decimals",
90
- "duration": "float — total wall-clock seconds, rounded to 4 decimals"
93
+ "duration": "float — total wall-clock seconds, rounded to 4 decimals",
94
+ "peak_memory_mb": "float (optional) — peak RSS across all mutation child processes, in MB"
91
95
  },
92
96
  "survived": [
93
97
  {
@@ -133,6 +137,33 @@ Each operator name is stable and appears in JSON output under `survived[].operat
133
137
  | `return_value_removal` | Strip return values | `return x` -> `return` |
134
138
  | `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
135
139
 
140
+ ## MCP Server (AI Agent Integration)
141
+
142
+ Evilution includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server for direct tool invocation by AI agents (Claude Code, VS Code Copilot, etc.).
143
+
144
+ ### Setup
145
+
146
+ Create a `.mcp.json` file in your project root:
147
+
148
+ ```json
149
+ {
150
+ "mcpServers": {
151
+ "evilution": {
152
+ "type": "stdio",
153
+ "command": "evilution",
154
+ "args": ["mcp"],
155
+ "env": {}
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ If using Bundler, set the command to `bundle` and args to `["exec", "evilution", "mcp"]`.
162
+
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.
164
+
165
+ > **Note**: `.mcp.json` is gitignored by default since it is a local editor/agent configuration file.
166
+
136
167
  ## Recommended Workflows for AI Agents
137
168
 
138
169
  ### 1. Full project scan
@@ -208,14 +239,30 @@ bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
208
239
 
209
240
  Note: `--quiet` suppresses all stdout output (including JSON). Use it in CI only when you care about the exit code and do not need JSON output.
210
241
 
242
+ ## Development
243
+
244
+ ### Memory leak check
245
+
246
+ Run before releasing to verify no memory regressions:
247
+
248
+ ```bash
249
+ bundle exec rake memory:check
250
+ ```
251
+
252
+ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripping, parallel pool) by running repeated iterations and asserting RSS stays flat. Configurable via environment variables:
253
+
254
+ - `MEMORY_CHECK_ITERATIONS` — number of iterations per check (default: 50)
255
+ - `MEMORY_CHECK_MAX_GROWTH_KB` — maximum allowed RSS growth in KB (default: 10240 = 10 MB)
256
+
211
257
  ## Internals (for context, not for direct use)
212
258
 
213
259
  1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
214
260
  2. **Extract** — Methods are identified as mutation subjects
215
261
  3. **Mutate** — Operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing)
216
- 4. **Isolate** — Each mutation runs in a `fork()`-ed child process (no test pollution)
262
+ 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
217
263
  5. **Test** — RSpec executes against the mutated source
218
- 6. **Report** — Results aggregated into text or JSON
264
+ 6. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
265
+ 7. **Report** — Results aggregated into text or JSON, including peak memory usage
219
266
 
220
267
  ## Repository
221
268
 
data/Rakefile CHANGED
@@ -9,4 +9,6 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
+ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
13
+
12
14
  task default: %i[spec rubocop]
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_resolver"
4
+
5
+ module Evilution
6
+ class Baseline
7
+ Result = Struct.new(:failed_spec_files, :duration) do
8
+ def initialize(**)
9
+ super
10
+ freeze
11
+ end
12
+
13
+ def failed?
14
+ !failed_spec_files.empty?
15
+ end
16
+ end
17
+
18
+ def initialize(spec_resolver: SpecResolver.new, timeout: 30)
19
+ @spec_resolver = spec_resolver
20
+ @timeout = timeout
21
+ end
22
+
23
+ def call(subjects)
24
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ spec_files = resolve_unique_spec_files(subjects)
26
+ failed = Set.new
27
+
28
+ spec_files.each do |spec_file|
29
+ failed.add(spec_file) unless run_spec_file(spec_file)
30
+ end
31
+
32
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
33
+ Result.new(failed_spec_files: failed, duration: duration)
34
+ end
35
+
36
+ def run_spec_file(spec_file)
37
+ read_io, write_io = IO.pipe
38
+ pid = fork_spec_runner(spec_file, read_io, write_io)
39
+ write_io.close
40
+ read_result(read_io, pid)
41
+ rescue StandardError
42
+ false
43
+ ensure
44
+ read_io&.close
45
+ write_io&.close
46
+ end
47
+
48
+ def fork_spec_runner(spec_file, read_io, write_io)
49
+ Process.fork do
50
+ read_io.close
51
+ $stdout.reopen(File::NULL, "w")
52
+ $stderr.reopen(File::NULL, "w")
53
+
54
+ require "rspec/core"
55
+ ::RSpec.reset
56
+ status = ::RSpec::Core::Runner.run(
57
+ ["--format", "progress", "--no-color", "--order", "defined", spec_file]
58
+ )
59
+ Marshal.dump({ passed: status.zero? }, write_io)
60
+ write_io.close
61
+ exit!(status.zero? ? 0 : 1)
62
+ end
63
+ end
64
+
65
+ GRACE_PERIOD = 0.5
66
+
67
+ def read_result(read_io, pid)
68
+ if read_io.wait_readable(@timeout)
69
+ data = read_io.read
70
+ Process.wait(pid)
71
+ return false if data.empty?
72
+
73
+ result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
74
+ result[:passed]
75
+ else
76
+ terminate_child(pid)
77
+ false
78
+ end
79
+ end
80
+
81
+ def terminate_child(pid)
82
+ Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
83
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
84
+ return if status
85
+
86
+ sleep(GRACE_PERIOD)
87
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
88
+ return if status
89
+
90
+ Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
91
+ Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
92
+ end
93
+
94
+ private
95
+
96
+ def resolve_unique_spec_files(subjects)
97
+ subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
98
+ end
99
+ end
100
+ end
data/lib/evilution/cli.rb CHANGED
@@ -98,6 +98,12 @@ module Evilution
98
98
  end
99
99
 
100
100
  def add_options(opts)
101
+ add_core_options(opts)
102
+ add_filter_options(opts)
103
+ add_flag_options(opts)
104
+ end
105
+
106
+ def add_core_options(opts)
101
107
  opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
102
108
  opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
103
109
  opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
@@ -106,6 +112,9 @@ module Evilution
106
112
  "Use line-range targeting instead: evilution run lib/foo.rb:15-30")
107
113
  @options[:diff_base] = b
108
114
  end
115
+ end
116
+
117
+ def add_filter_options(opts)
109
118
  opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
110
119
  opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
111
120
  opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
@@ -113,8 +122,13 @@ module Evilution
113
122
  warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
114
123
  @options[:coverage] = false
115
124
  end
125
+ end
126
+
127
+ def add_flag_options(opts)
116
128
  opts.on("--fail-fast", "Stop after N surviving mutants " \
117
129
  "(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
130
+ opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
131
+ opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
118
132
  opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
119
133
  opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
120
134
  opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
@@ -18,32 +18,21 @@ module Evilution
18
18
  quiet: false,
19
19
  jobs: 1,
20
20
  fail_fast: nil,
21
+ baseline: true,
22
+ isolation: :auto,
21
23
  line_ranges: {},
22
24
  spec_files: []
23
25
  }.freeze
24
26
 
25
27
  attr_reader :target_files, :timeout, :format, :diff_base,
26
28
  :target, :min_score, :integration, :coverage, :verbose, :quiet,
27
- :jobs, :fail_fast, :line_ranges, :spec_files
29
+ :jobs, :fail_fast, :baseline, :isolation, :line_ranges, :spec_files
28
30
 
29
31
  def initialize(**options)
30
32
  file_options = options.delete(:skip_config_file) ? {} : load_config_file
31
33
  merged = DEFAULTS.merge(file_options).merge(options)
32
34
  warn_removed_options(merged, file_options)
33
- @target_files = Array(merged[:target_files])
34
- @timeout = merged[:timeout]
35
- @format = merged[:format].to_sym
36
- @diff_base = merged[:diff_base]
37
- @target = merged[:target]
38
- @min_score = merged[:min_score].to_f
39
- @integration = merged[:integration].to_sym
40
- @coverage = merged[:coverage]
41
- @verbose = merged[:verbose]
42
- @quiet = merged[:quiet]
43
- @jobs = validate_jobs(merged[:jobs])
44
- @fail_fast = validate_fail_fast(merged[:fail_fast])
45
- @line_ranges = merged[:line_ranges] || {}
46
- @spec_files = Array(merged[:spec_files])
35
+ assign_attributes(merged)
47
36
  freeze
48
37
  end
49
38
 
@@ -71,6 +60,10 @@ module Evilution
71
60
  !fail_fast.nil?
72
61
  end
73
62
 
63
+ def baseline?
64
+ baseline
65
+ end
66
+
74
67
  def self.file_options
75
68
  CONFIG_FILES.each do |path|
76
69
  next unless File.exist?(path)
@@ -128,6 +121,34 @@ module Evilution
128
121
  raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
129
122
  end
130
123
 
124
+ def assign_attributes(merged)
125
+ @target_files = Array(merged[:target_files])
126
+ @timeout = merged[:timeout]
127
+ @format = merged[:format].to_sym
128
+ @diff_base = merged[:diff_base]
129
+ @target = merged[:target]
130
+ @min_score = merged[:min_score].to_f
131
+ @integration = merged[:integration].to_sym
132
+ @coverage = merged[:coverage]
133
+ @verbose = merged[:verbose]
134
+ @quiet = merged[:quiet]
135
+ @jobs = validate_jobs(merged[:jobs])
136
+ @fail_fast = validate_fail_fast(merged[:fail_fast])
137
+ @baseline = merged[:baseline]
138
+ @isolation = validate_isolation(merged[:isolation])
139
+ @line_ranges = merged[:line_ranges] || {}
140
+ @spec_files = Array(merged[:spec_files])
141
+ end
142
+
143
+ def validate_isolation(value)
144
+ raise ConfigError, "isolation must be auto, fork, or in_process, got nil" if value.nil?
145
+
146
+ value = value.to_sym
147
+ raise ConfigError, "isolation must be auto, fork, or in_process, got #{value.inspect}" unless %i[auto fork in_process].include?(value)
148
+
149
+ value
150
+ end
151
+
131
152
  def validate_jobs(value)
132
153
  raise ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
133
154
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "tmpdir"
5
+ require_relative "../memory"
5
6
 
6
7
  module Evilution
7
8
  module Isolation
@@ -51,7 +52,8 @@ module Evilution
51
52
  end
52
53
 
53
54
  def execute_in_child(mutation, test_command)
54
- test_command.call(mutation)
55
+ result = test_command.call(mutation)
56
+ { child_rss_kb: Memory.rss_kb }.merge(result)
55
57
  rescue StandardError => e
56
58
  { passed: false, error: e.message }
57
59
  end
@@ -98,7 +100,8 @@ module Evilution
98
100
  mutation: mutation,
99
101
  status: status,
100
102
  duration: duration,
101
- test_command: result[:test_command]
103
+ test_command: result[:test_command],
104
+ child_rss_kb: result[:child_rss_kb]
102
105
  )
103
106
  end
104
107
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "../memory"
5
+ require_relative "../result/mutation_result"
6
+
7
+ module Evilution
8
+ module Isolation
9
+ class InProcess
10
+ def call(mutation:, test_command:, timeout:)
11
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
+ rss_before = Memory.rss_kb
13
+ result = execute_with_timeout(mutation, test_command, timeout)
14
+ rss_after = Memory.rss_kb
15
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
16
+ delta = compute_memory_delta(rss_before, rss_after, result)
17
+
18
+ build_mutation_result(mutation, result, duration, rss_after, delta)
19
+ end
20
+
21
+ private
22
+
23
+ def execute_with_timeout(mutation, test_command, timeout)
24
+ result = Timeout.timeout(timeout) do
25
+ suppress_output { test_command.call(mutation) }
26
+ end
27
+ { timeout: false }.merge(result)
28
+ rescue Timeout::Error
29
+ { timeout: true }
30
+ rescue StandardError => e
31
+ { timeout: false, passed: false, error: e.message }
32
+ end
33
+
34
+ def suppress_output
35
+ saved_stdout = $stdout
36
+ saved_stderr = $stderr
37
+ File.open(File::NULL, "w") do |null_out|
38
+ File.open(File::NULL, "w") do |null_err|
39
+ $stdout = null_out
40
+ $stderr = null_err
41
+ yield
42
+ end
43
+ end
44
+ ensure
45
+ $stdout = saved_stdout
46
+ $stderr = saved_stderr
47
+ end
48
+
49
+ def compute_memory_delta(rss_before, rss_after, result)
50
+ return nil if result[:timeout]
51
+ return nil unless rss_before && rss_after
52
+
53
+ rss_after - rss_before
54
+ end
55
+
56
+ def build_mutation_result(mutation, result, duration, rss_after, memory_delta_kb)
57
+ status = if result[:timeout]
58
+ :timeout
59
+ elsif result[:error]
60
+ :error
61
+ elsif result[:passed]
62
+ :survived
63
+ else
64
+ :killed
65
+ end
66
+
67
+ Result::MutationResult.new(
68
+ mutation: mutation,
69
+ status: status,
70
+ duration: duration,
71
+ test_command: result[:test_command],
72
+ child_rss_kb: rss_after,
73
+ memory_delta_kb: memory_delta_kb
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end