evilution 0.5.0 → 0.6.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 +4 -4
- data/.beads/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +13 -3
- data/CHANGELOG.md +8 -0
- data/lib/evilution/cli.rb +34 -2
- data/lib/evilution/mcp/mutate_tool.rb +115 -0
- data/lib/evilution/mcp/server.rb +19 -0
- data/lib/evilution/mutator/operator/method_call_removal.rb +22 -0
- data/lib/evilution/mutator/registry.rb +2 -1
- data/lib/evilution/reporter/suggestion.rb +2 -1
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- metadata +25 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90bb10673d940d85a42206ae1b20de288321dfb4f61802216dc4e9b68175f46f
|
|
4
|
+
data.tar.gz: a769255c8fd242d3432e811d36655eec1dae0ac6ef3c7288c0d68fe2c390182c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c06ec7f6e81c3f8427b4021dddd298d3111827896e0eae72ba1d764e076523d4ce63aac83438705f559199534b4408aac5683aeddf2c8566daa7ffd20015d88
|
|
7
|
+
data.tar.gz: cca406c93807c560cc70c7a5183c14d0c15fe139e12fe35f733341d8db839f28e74a7b34557ad5dabfdf3b7cbb34a76f8a954e30e4b0d91d0a9b5fb60ffed04f
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1773676731
|
data/.beads/issues.jsonl
CHANGED
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
{"id":"EV-24","title":"Epic: JSON Output Improvements","description":"Make JSON output fully machine-parseable in all scenarios, including errors. Add diagnostic fields that help agents debug failures.","status":"closed","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:37.450686472+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T11:15:24.900944562+07:00","closed_at":"2026-03-16T11:15:24.900944562+07:00","close_reason":"All children complete: structured errors, test_command in JSON, noise suppression","dependencies":[{"issue_id":"EV-24","depends_on_id":"EV-25","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-24","depends_on_id":"EV-26","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-24","depends_on_id":"EV-40","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
37
37
|
{"id":"EV-25","title":"Structured error responses in JSON mode","description":"When --format json is used and exit code is 2 (error), output a JSON object with error details instead of unstructured stderr text. Schema: { \"error\": { \"type\": \"config_error|parse_error|runtime_error\", \"message\": \"...\", \"file\": \"...\" } }. Agents currently have to regex-parse stderr which is fragile.","status":"closed","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:38.283715502+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-15T22:41:54.370789377+07:00","closed_at":"2026-03-15T22:41:54.370789377+07:00","close_reason":"Merged PR #74 — structured JSON error output in CLI"}
|
|
38
38
|
{"id":"EV-26","title":"Include test command in mutation result JSON","description":"Add a 'test_command' field to each mutation result in JSON output showing the exact command that was run to test that mutation. Helps agents debug when a mutation errors out or times out.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:39.227881462+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311631114+07:00","closed_at":"2026-03-16T10:11:01.311631114+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
39
|
-
{"id":"EV-27","title":"Epic: Workflow Integration","description":"Make evilution easier to invoke from AI agent toolchains — MCP server for direct tool calls, stdin mode for piping.","status":"
|
|
40
|
-
{"id":"EV-28","title":"MCP server for direct tool invocation","description":"Implement a Model Context Protocol (MCP) server that exposes evilution as a tool. Agents could call evilution directly instead of shelling out and parsing output. The server should expose a 'mutate' tool that accepts target files, options, and returns structured results.","status":"
|
|
41
|
-
{"id":"EV-29","title":"Add --stdin flag to accept file list from stdin","description":"Add a --stdin flag that reads target file paths (one per line) from stdin. Enables workflows like: git diff --name-only | evilution run --stdin --format json. Each line can include line-range syntax (e.g. lib/foo.rb:15-30).","status":"
|
|
39
|
+
{"id":"EV-27","title":"Epic: Workflow Integration","description":"Make evilution easier to invoke from AI agent toolchains — MCP server for direct tool calls, stdin mode for piping.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:43.883767452+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T22:59:12.241444308+07:00","closed_at":"2026-03-16T22:59:12.241444308+07:00","close_reason":"Both child issues done: MCP server (EV-28) and --stdin (EV-29)","dependencies":[{"issue_id":"EV-27","depends_on_id":"EV-28","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-27","depends_on_id":"EV-29","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
40
|
+
{"id":"EV-28","title":"MCP server for direct tool invocation","description":"Implement a Model Context Protocol (MCP) server that exposes evilution as a tool. Agents could call evilution directly instead of shelling out and parsing output. The server should expose a 'mutate' tool that accepts target files, options, and returns structured results.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:45.29866593+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T22:58:51.734461132+07:00","closed_at":"2026-03-16T22:58:51.734461132+07:00","close_reason":"PR #103 merged — MCP server with evilution-mutate tool via stdio transport"}
|
|
41
|
+
{"id":"EV-29","title":"Add --stdin flag to accept file list from stdin","description":"Add a --stdin flag that reads target file paths (one per line) from stdin. Enables workflows like: git diff --name-only | evilution run --stdin --format json. Each line can include line-range syntax (e.g. lib/foo.rb:15-30).","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:46.306306092+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T18:03:28.998559073+07:00","closed_at":"2026-03-16T18:03:28.998559073+07:00","close_reason":"PR #92 merged — --stdin flag for piped file list workflows"}
|
|
42
42
|
{"id":"EV-3","title":"Phase 2: Mutation Operators & CLI","description":"Implement remaining 17 mutation operators, build CLI with OptionParser, exe/evilution executable, human-readable reporter. Milestone: bundle exec evilution run lib/user.rb --format json","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:00.492971295+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:21:32.168384165+07:00","closed_at":"2026-03-02T11:21:32.168384165+07:00","close_reason":"Phase 2 complete: all 18 operators, CLI, Reporter::CLI, Registry registration, executable","dependencies":[{"issue_id":"EV-3","depends_on_id":"EV-2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
43
43
|
{"id":"EV-3.1","title":"Implement ArithmeticReplacement operator","description":"Targets CallNode where name is :+, :-, :*, :/, :%, :**. Replacements: + <-> -, * <-> /, % -> /, ** -> *. File: lib/evilution/mutator/operator/arithmetic_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.025649082+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:06:10.922412304+07:00","closed_at":"2026-03-02T11:06:10.922412304+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.1","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.1","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
44
44
|
{"id":"EV-3.10","title":"Implement SymbolLiteral operator","description":"Targets SymbolNode. Mutation: :foo -> :__evilution_mutated__. File: lib/evilution/mutator/operator/symbol_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.935877816+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541040613+07:00","closed_at":"2026-03-02T11:13:36.541040613+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.10","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.10","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -86,6 +86,11 @@
|
|
|
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"}]}
|
|
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
|
+
{"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
|
+
{"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"}
|
|
93
|
+
{"id":"EV-49","title":"Operator: RegexpMutation","description":"Replace regexp with always-failing pattern. e.g. /pattern/ → /a\\A/. Catches untested regex matching.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T21:50:23.274989762+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T21:50:23.274989762+07:00"}
|
|
89
94
|
{"id":"EV-5","title":"Phase 4: Polish","description":"Suggestion generator, .evilution.yml config file loading, evilution init subcommand, error handling, README","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:03.497091872+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:32.782883746+07:00","closed_at":"2026-03-02T11:54:32.782883746+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5","depends_on_id":"EV-4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
90
95
|
{"id":"EV-5.1","title":"Implement Evilution::Reporter::Suggestion","description":"Generates actionable fix suggestions per surviving mutant. Each operator type has a suggestion template. E.g., ComparisonReplacement >= -> > suggests 'Add a test for the boundary case where value equals exactly [threshold]'. File: lib/evilution/reporter/suggestion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.038411009+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:21.518470747+07:00","closed_at":"2026-03-02T11:54:21.518470747+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.1","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.1","depends_on_id":"EV-2.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.1","depends_on_id":"EV-3.21","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
91
96
|
{"id":"EV-5.2","title":"Implement .evilution.yml config file loading","description":"Load config from .evilution.yml / config/evilution.yml if present. YAML keys map to Config fields. CLI flags override file values. File: update lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.142538793+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:21.518473527+07:00","closed_at":"2026-03-02T11:54:21.518473527+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.2","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.2","depends_on_id":"EV-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -93,6 +98,11 @@
|
|
|
93
98
|
{"id":"EV-5.4","title":"Add error handling and edge cases","description":"Handle: no test files found, no subjects found, fork failures, marshal errors, invalid config, files that fail to parse. Use Evilution::Error subclasses. Ensure clean exit codes (2 for tool errors).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.353265504+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:25.59628785+07:00","closed_at":"2026-03-02T11:54:25.59628785+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.4","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.4","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
94
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"}]}
|
|
95
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
|
+
{"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
|
+
{"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"}
|
|
96
106
|
{"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"}
|
|
97
107
|
{"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"}
|
|
98
108
|
{"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."}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`--stdin` flag** — read target file paths from stdin (one per line), enabling piped workflows like `git diff --name-only | evilution run --stdin --format json`; supports line-range syntax (e.g. `lib/foo.rb:15-30`); errors if combined with positional file arguments
|
|
8
|
+
- **MCP server** (`evilution mcp`) — Model Context Protocol server for direct AI agent integration via stdio; exposes an `evilution-mutate` tool that accepts target files, options, and returns structured JSON results
|
|
9
|
+
- **MethodCallRemoval operator** — new mutation operator that removes method calls while keeping the receiver (e.g. `obj.foo(x)` → `obj`); catches untested side effects and return values
|
|
10
|
+
|
|
3
11
|
## [0.5.0] - 2026-03-16
|
|
4
12
|
|
|
5
13
|
### Added
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -8,14 +8,16 @@ require_relative "runner"
|
|
|
8
8
|
|
|
9
9
|
module Evilution
|
|
10
10
|
class CLI
|
|
11
|
-
def initialize(argv)
|
|
11
|
+
def initialize(argv, stdin: $stdin)
|
|
12
12
|
@options = {}
|
|
13
13
|
@command = :run
|
|
14
|
+
@stdin = stdin
|
|
14
15
|
argv = argv.dup
|
|
15
16
|
argv = extract_command(argv)
|
|
16
17
|
argv = preprocess_flags(argv)
|
|
17
18
|
raw_args = build_option_parser.parse!(argv)
|
|
18
19
|
@files, @line_ranges = parse_file_args(raw_args)
|
|
20
|
+
read_stdin_files if @options.delete(:stdin) && @command == :run
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def call
|
|
@@ -25,6 +27,8 @@ module Evilution
|
|
|
25
27
|
0
|
|
26
28
|
when :init
|
|
27
29
|
run_init
|
|
30
|
+
when :mcp
|
|
31
|
+
run_mcp
|
|
28
32
|
when :run
|
|
29
33
|
run_mutations
|
|
30
34
|
end
|
|
@@ -40,6 +44,9 @@ module Evilution
|
|
|
40
44
|
when "init"
|
|
41
45
|
@command = :init
|
|
42
46
|
argv.shift
|
|
47
|
+
when "mcp"
|
|
48
|
+
@command = :mcp
|
|
49
|
+
argv.shift
|
|
43
50
|
when "run"
|
|
44
51
|
argv.shift
|
|
45
52
|
end
|
|
@@ -85,7 +92,7 @@ module Evilution
|
|
|
85
92
|
opts.separator ""
|
|
86
93
|
opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
|
|
87
94
|
opts.separator ""
|
|
88
|
-
opts.separator "Commands: run (default), init, version"
|
|
95
|
+
opts.separator "Commands: run (default), init, mcp, version"
|
|
89
96
|
opts.separator ""
|
|
90
97
|
opts.separator "Options:"
|
|
91
98
|
end
|
|
@@ -108,6 +115,7 @@ module Evilution
|
|
|
108
115
|
end
|
|
109
116
|
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
110
117
|
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
118
|
+
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
111
119
|
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
112
120
|
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
113
121
|
end
|
|
@@ -124,6 +132,28 @@ module Evilution
|
|
|
124
132
|
0
|
|
125
133
|
end
|
|
126
134
|
|
|
135
|
+
def run_mcp
|
|
136
|
+
require_relative "mcp/server"
|
|
137
|
+
server = MCP::Server.build
|
|
138
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(server)
|
|
139
|
+
transport.open
|
|
140
|
+
0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def read_stdin_files
|
|
144
|
+
@stdin_error = "--stdin cannot be combined with positional file arguments" unless @files.empty?
|
|
145
|
+
return if @stdin_error
|
|
146
|
+
|
|
147
|
+
lines = []
|
|
148
|
+
@stdin.each_line do |line|
|
|
149
|
+
line = line.strip
|
|
150
|
+
lines << line unless line.empty?
|
|
151
|
+
end
|
|
152
|
+
stdin_files, stdin_ranges = parse_file_args(lines)
|
|
153
|
+
@files = stdin_files
|
|
154
|
+
@line_ranges = @line_ranges.merge(stdin_ranges)
|
|
155
|
+
end
|
|
156
|
+
|
|
127
157
|
def parse_file_args(raw_args)
|
|
128
158
|
files = []
|
|
129
159
|
ranges = {}
|
|
@@ -152,6 +182,8 @@ module Evilution
|
|
|
152
182
|
end
|
|
153
183
|
|
|
154
184
|
def run_mutations
|
|
185
|
+
raise ConfigError, @stdin_error if @stdin_error
|
|
186
|
+
|
|
155
187
|
file_options = Config.file_options
|
|
156
188
|
config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
157
189
|
runner = Runner.new(config: config)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mcp"
|
|
5
|
+
require_relative "../config"
|
|
6
|
+
require_relative "../runner"
|
|
7
|
+
require_relative "../reporter/json"
|
|
8
|
+
|
|
9
|
+
module Evilution
|
|
10
|
+
module MCP
|
|
11
|
+
class MutateTool < ::MCP::Tool
|
|
12
|
+
tool_name "evilution-mutate"
|
|
13
|
+
description "Run mutation testing on Ruby source files"
|
|
14
|
+
input_schema(
|
|
15
|
+
properties: {
|
|
16
|
+
files: {
|
|
17
|
+
type: "array",
|
|
18
|
+
items: { type: "string" },
|
|
19
|
+
description: "Target files, supports line-range syntax (e.g. lib/foo.rb:15-30)"
|
|
20
|
+
},
|
|
21
|
+
target: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Only mutate the named method (e.g. Foo#bar)"
|
|
24
|
+
},
|
|
25
|
+
timeout: {
|
|
26
|
+
type: "integer",
|
|
27
|
+
description: "Per-mutation timeout in seconds (default: 30)"
|
|
28
|
+
},
|
|
29
|
+
jobs: {
|
|
30
|
+
type: "integer",
|
|
31
|
+
description: "Number of parallel workers (default: 1)"
|
|
32
|
+
},
|
|
33
|
+
fail_fast: {
|
|
34
|
+
type: "integer",
|
|
35
|
+
description: "Stop after N surviving mutants"
|
|
36
|
+
},
|
|
37
|
+
spec: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: { type: "string" },
|
|
40
|
+
description: "Spec files to run (overrides auto-detection)"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def call(server_context:, files: [], target: nil, timeout: nil, jobs: nil, fail_fast: nil, spec: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
47
|
+
parsed_files, line_ranges = parse_files(Array(files))
|
|
48
|
+
config_opts = build_config_opts(parsed_files, line_ranges, target, timeout, jobs, fail_fast, spec)
|
|
49
|
+
config = Config.new(**config_opts)
|
|
50
|
+
runner = Runner.new(config: config)
|
|
51
|
+
summary = runner.call
|
|
52
|
+
report = Reporter::JSON.new.call(summary)
|
|
53
|
+
|
|
54
|
+
::MCP::Tool::Response.new([{ type: "text", text: report }])
|
|
55
|
+
rescue Evilution::Error => e
|
|
56
|
+
error_payload = build_error_payload(e)
|
|
57
|
+
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(error_payload) }], error: true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def parse_files(raw_files)
|
|
63
|
+
files = []
|
|
64
|
+
ranges = {}
|
|
65
|
+
|
|
66
|
+
raw_files.each do |arg|
|
|
67
|
+
file, range_str = arg.split(":", 2)
|
|
68
|
+
files << file
|
|
69
|
+
next unless range_str
|
|
70
|
+
|
|
71
|
+
ranges[file] = parse_line_range(range_str)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
[files, ranges]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_line_range(str)
|
|
78
|
+
if str.include?("-")
|
|
79
|
+
start_str, end_str = str.split("-", 2)
|
|
80
|
+
start_line = Integer(start_str)
|
|
81
|
+
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
82
|
+
start_line..end_line
|
|
83
|
+
else
|
|
84
|
+
line = Integer(str)
|
|
85
|
+
line..line
|
|
86
|
+
end
|
|
87
|
+
rescue ArgumentError, TypeError
|
|
88
|
+
raise ParseError, "invalid line range: #{str.inspect}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_config_opts(files, line_ranges, target, timeout, jobs, fail_fast, spec)
|
|
92
|
+
opts = { target_files: files, line_ranges: line_ranges, format: :json, quiet: true, skip_config_file: true }
|
|
93
|
+
opts[:target] = target if target
|
|
94
|
+
opts[:timeout] = timeout if timeout
|
|
95
|
+
opts[:jobs] = jobs if jobs
|
|
96
|
+
opts[:fail_fast] = fail_fast if fail_fast
|
|
97
|
+
opts[:spec_files] = spec if spec
|
|
98
|
+
opts
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_error_payload(error)
|
|
102
|
+
error_type = case error
|
|
103
|
+
when ConfigError then "config_error"
|
|
104
|
+
when ParseError then "parse_error"
|
|
105
|
+
else "runtime_error"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
payload = { type: error_type, message: error.message }
|
|
109
|
+
payload[:file] = error.file if error.file
|
|
110
|
+
{ error: payload }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
require_relative "../version"
|
|
5
|
+
require_relative "mutate_tool"
|
|
6
|
+
|
|
7
|
+
module Evilution
|
|
8
|
+
module MCP
|
|
9
|
+
class Server
|
|
10
|
+
def self.build
|
|
11
|
+
::MCP::Server.new(
|
|
12
|
+
name: "evilution",
|
|
13
|
+
version: Evilution::VERSION,
|
|
14
|
+
tools: [MutateTool]
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Evilution
|
|
4
|
+
module Mutator
|
|
5
|
+
module Operator
|
|
6
|
+
class MethodCallRemoval < Base
|
|
7
|
+
def visit_call_node(node)
|
|
8
|
+
if node.receiver
|
|
9
|
+
add_mutation(
|
|
10
|
+
offset: node.location.start_offset,
|
|
11
|
+
length: node.location.length,
|
|
12
|
+
replacement: node.receiver.slice,
|
|
13
|
+
node: node
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -23,7 +23,8 @@ module Evilution
|
|
|
23
23
|
Operator::MethodBodyReplacement,
|
|
24
24
|
Operator::NegationInsertion,
|
|
25
25
|
Operator::ReturnValueRemoval,
|
|
26
|
-
Operator::CollectionReplacement
|
|
26
|
+
Operator::CollectionReplacement,
|
|
27
|
+
Operator::MethodCallRemoval
|
|
27
28
|
].each { |op| registry.register(op) }
|
|
28
29
|
registry
|
|
29
30
|
end
|
|
@@ -21,7 +21,8 @@ module Evilution
|
|
|
21
21
|
"method_body_replacement" => "Add a test that checks the method's return value or side effects",
|
|
22
22
|
"negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
|
|
23
23
|
"return_value_removal" => "Add a test that uses the return value of this method",
|
|
24
|
-
"collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects"
|
|
24
|
+
"collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
|
|
25
|
+
"method_call_removal" => "Add a test that depends on the return value or side effect of this method call"
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
28
|
DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -25,6 +25,7 @@ require_relative "evilution/mutator/operator/statement_deletion"
|
|
|
25
25
|
require_relative "evilution/mutator/operator/method_body_replacement"
|
|
26
26
|
require_relative "evilution/mutator/operator/return_value_removal"
|
|
27
27
|
require_relative "evilution/mutator/operator/collection_replacement"
|
|
28
|
+
require_relative "evilution/mutator/operator/method_call_removal"
|
|
28
29
|
require_relative "evilution/mutator/registry"
|
|
29
30
|
require_relative "evilution/isolation/fork"
|
|
30
31
|
require_relative "evilution/parallel/pool"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -30,6 +30,26 @@ dependencies:
|
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '3'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: mcp
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.8'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0.8'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2'
|
|
33
53
|
description: Evilution is a mutation testing tool for Ruby. It validates test suite
|
|
34
54
|
quality by making small code changes and checking if tests catch them. AI-agent-first
|
|
35
55
|
design with JSON output, diff-based targeting, and coverage-based filtering.
|
|
@@ -70,6 +90,8 @@ files:
|
|
|
70
90
|
- lib/evilution/integration/base.rb
|
|
71
91
|
- lib/evilution/integration/rspec.rb
|
|
72
92
|
- lib/evilution/isolation/fork.rb
|
|
93
|
+
- lib/evilution/mcp/mutate_tool.rb
|
|
94
|
+
- lib/evilution/mcp/server.rb
|
|
73
95
|
- lib/evilution/mutation.rb
|
|
74
96
|
- lib/evilution/mutator/base.rb
|
|
75
97
|
- lib/evilution/mutator/operator/arithmetic_replacement.rb
|
|
@@ -84,6 +106,7 @@ files:
|
|
|
84
106
|
- lib/evilution/mutator/operator/hash_literal.rb
|
|
85
107
|
- lib/evilution/mutator/operator/integer_literal.rb
|
|
86
108
|
- lib/evilution/mutator/operator/method_body_replacement.rb
|
|
109
|
+
- lib/evilution/mutator/operator/method_call_removal.rb
|
|
87
110
|
- lib/evilution/mutator/operator/negation_insertion.rb
|
|
88
111
|
- lib/evilution/mutator/operator/nil_replacement.rb
|
|
89
112
|
- lib/evilution/mutator/operator/return_value_removal.rb
|