evilution 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67bff7d0366e8fe092534c14669ec3637ba1d95934a003f595519dca8f16fe68
4
- data.tar.gz: b4545412acaedb471187e9522cb8858c07b9a83b748a58ffbd8aa2a1f0ec7a29
3
+ metadata.gz: 90bb10673d940d85a42206ae1b20de288321dfb4f61802216dc4e9b68175f46f
4
+ data.tar.gz: a769255c8fd242d3432e811d36655eec1dae0ac6ef3c7288c0d68fe2c390182c
5
5
  SHA512:
6
- metadata.gz: 15c2460a72fc5822ddecc3e38c4e60424bc96a24274542ce2492c55e9a847ca02484ec103513791f91c285f3ee18b8fb3017009a146fc517822c9a1749f2cd93
7
- data.tar.gz: 98f93f7b5bbe2baf2172bee631a44fa8cde99e5913e3484b91c06d780ea2f0d7f462e6a030187d3179dfbe0b0bd9d78afdeff29dd28e38e7d7b9f6efa90763c1
6
+ metadata.gz: 3c06ec7f6e81c3f8427b4021dddd298d3111827896e0eae72ba1d764e076523d4ce63aac83438705f559199534b4408aac5683aeddf2c8566daa7ffd20015d88
7
+ data.tar.gz: cca406c93807c560cc70c7a5183c14d0c15fe139e12fe35f733341d8db839f28e74a7b34557ad5dabfdf3b7cbb34a76f8a954e30e4b0d91d0a9b5fb60ffed04f
@@ -1 +1 @@
1
- 1773589314
1
+ 1773676731
data/.beads/issues.jsonl CHANGED
@@ -13,7 +13,7 @@
13
13
  {"id":"EV-15","title":"Add line-range targeting (file:line-line syntax)","description":"Parse line-range syntax in CLI, store in Config, filter subjects in Runner. GH #29.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T00:36:29.164188342+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-08T00:38:02.739833414+07:00","closed_at":"2026-03-08T00:38:02.739833414+07:00","close_reason":"Implemented line-range targeting in CLI, Config, and Runner with full test coverage"}
14
14
  {"id":"EV-16","title":"Remove file-discovery logic from Integration::RSpec (GH #33)","description":"Integration::RSpec has detect_test_files, spec_file_candidates, and fallback_spec_dir that guess which specs to run. With precise targeting, agents can pass spec files directly. Simplify or remove this guessing logic, possibly adding a --spec flag.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T19:17:27.268579626+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T18:13:46.899195957+07:00","closed_at":"2026-03-09T18:13:46.899195957+07:00","close_reason":"Completed via PR #38 (tracked as EV-17)"}
15
15
  {"id":"EV-17","title":"Remove file-discovery logic from Integration::RSpec","description":"Add --spec flag, simplify RSpec integration by removing detect_test_files heuristics. GH #33","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T00:41:29.880486333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T00:43:21.229172712+07:00","closed_at":"2026-03-09T00:43:21.229172712+07:00","close_reason":"Removed ~60 lines of file-discovery logic from Integration::RSpec, added --spec CLI flag, spec_files config, and wired through Runner"}
16
- {"id":"EV-18","title":"Add method-name targeting (Class#method) (GH #30)","description":"Allow specifying a fully-qualified method name: evilution run Foo::Bar#calculate. Resolve name to file and line range, then mutate only that method. Lower priority since it requires name-to-file resolution which can be ambiguous with reopened classes or metaprogramming. Update README, --help, and CHANGELOG.","status":"in_progress","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T23:49:31.827723147+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:05:22.381962628+07:00"}
16
+ {"id":"EV-18","title":"Add method-name targeting (Class#method) (GH #30)","description":"Allow specifying a fully-qualified method name: evilution run Foo::Bar#calculate. Resolve name to file and line range, then mutate only that method. Lower priority since it requires name-to-file resolution which can be ambiguous with reopened classes or metaprogramming. Update README, --help, and CHANGELOG.","status":"closed","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T23:49:31.827723147+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311634237+07:00","closed_at":"2026-03-16T10:11:01.311634237+07:00","close_reason":"Already merged and released in v0.4.0"}
17
17
  {"id":"EV-19","title":"Add method-name targeting (--target flag)","description":"Users can target a specific method for mutation testing via --target METHOD flag. Matches against Subject.name.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:09:57.091870734+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:10:02.637447023+07:00","closed_at":"2026-03-10T00:10:02.637447023+07:00","close_reason":"Implemented --target flag in CLI, Config.target? predicate, Runner.filter_by_target, README docs, and specs"}
18
18
  {"id":"EV-2","title":"Phase 1: Foundation — End-to-End Single Mutation","description":"Build the core pipeline: parse Ruby with Prism, generate mutations, fork-based isolation, RSpec integration, JSON reporting. Milestone: Runner.new(files: ['lib/user.rb']).call produces JSON output.","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:04:58.737191467+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:02:00.342745637+07:00","closed_at":"2026-03-02T11:02:00.342745637+07:00","close_reason":"Phase 1 Foundation complete: Config, AST::Parser, Subject, SourceSurgeon, Mutation, Mutator::Base+Registry, ComparisonReplacement, Isolation::Fork, Integration::RSpec, Result objects, Reporter::JSON, Runner — all 13 tasks done"}
19
19
  {"id":"EV-2.1","title":"Implement Evilution::Config","description":"Immutable configuration value object. Fields: target_files, jobs (default: Etc.nprocessors), timeout (default: 10s), format (:json/:text), diff_base (nil), min_score (0.0), integration (:rspec), config_file path. Merge from defaults + YAML + CLI flags. File: lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.275297792+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:43:14.688620834+07:00","closed_at":"2026-03-02T10:43:14.688620834+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.1","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
@@ -30,15 +30,15 @@
30
30
  {"id":"EV-2.8","title":"Implement Evilution::Result::MutationResult and Summary","description":"MutationResult: value object with fields: mutation, status (:killed/:survived/:timeout/:error), duration, killing_test (optional). Summary: aggregates MutationResult array into total/killed/survived/timed_out/errors/score/duration. Files: lib/evilution/result/mutation_result.rb, lib/evilution/result/summary.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.022249568+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:51:08.482621898+07:00","closed_at":"2026-03-02T10:51:08.482621898+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.8","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
31
31
  {"id":"EV-2.9","title":"Implement Evilution::Isolation::Fork","description":"Fork-based process isolation. Method: run(mutation, test_command, timeout:) -> MutationResult. Flow: create pipe, fork child, child applies mutation via eval, child runs test command, marshal result back via pipe, parent reads with IO.select timeout, SIGKILL on deadline. File: lib/evilution/isolation/fork.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.142167275+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:54:08.819310594+07:00","closed_at":"2026-03-02T10:54:08.819310594+07:00","close_reason":"Fork isolation implemented with 6 passing specs","dependencies":[{"issue_id":"EV-2.9","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.9","depends_on_id":"EV-2.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
32
32
  {"id":"EV-20","title":"Deprecate coverage filtering","description":"With line-range and method-name targeting, coverage collection adds overhead for little benefit. Deprecate --no-coverage flag, coverage config key, and remove coverage logic from runner.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:30:58.837659684+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:33:06.453669976+07:00","closed_at":"2026-03-10T00:33:06.453669976+07:00","close_reason":"Deprecated coverage filtering: CLI warns on --no-coverage, config warns on coverage key, runner no longer uses coverage logic"}
33
- {"id":"EV-21","title":"Epic: Speed & Performance","description":"Reduce mutation testing wall-clock time for fast agent feedback loops. Includes fail-fast, parallel execution, and per-mutation spec targeting.","status":"open","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:26.608316104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:26.608316104+07:00","dependencies":[{"issue_id":"EV-21","depends_on_id":"EV-22","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-21","depends_on_id":"EV-23","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
34
- {"id":"EV-22","title":"Add --fail-fast flag with survivor threshold","description":"Add --fail-fast [N] CLI flag and fail_fast config option to stop mutation testing after N surviving mutants (default N=1 when flag given without value). Agents fix gaps iteratively, so discovering all survivors upfront is wasted work. A threshold gives control over the speed/thoroughness tradeoff: --fail-fast 1 for quick checks, --fail-fast 5 for CI gates, omit for full scans. Runner#call should stop running mutations and return a partial Summary once the threshold is reached. JSON output should include a 'truncated: true' field when fail-fast triggered.","status":"in_progress","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.018733235+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T08:24:05.534981769+07:00"}
35
- {"id":"EV-23","title":"Per-mutation spec targeting","description":"Instead of running the full spec suite for every mutation, map each mutated source file to its relevant spec file(s) using convention-based resolution (e.g. lib/foo/bar.rb -> spec/foo/bar_spec.rb) and only run those. This dramatically reduces per-mutation test time. Depends on convention-based spec file resolution being implemented first.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.98620973+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:28.98620973+07:00","dependencies":[{"issue_id":"EV-23","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
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":"open","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-13T09:57:11.531255221+07:00","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"}]}
33
+ {"id":"EV-21","title":"Epic: Speed & Performance","description":"Reduce mutation testing wall-clock time for fast agent feedback loops. Includes fail-fast, parallel execution, and per-mutation spec targeting.","status":"closed","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:26.608316104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T14:49:23.063583958+07:00","closed_at":"2026-03-16T14:49:23.063583958+07:00","close_reason":"All children complete: fail-fast, per-mutation spec targeting","dependencies":[{"issue_id":"EV-21","depends_on_id":"EV-22","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-21","depends_on_id":"EV-23","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
34
+ {"id":"EV-22","title":"Add --fail-fast flag with survivor threshold","description":"Add --fail-fast [N] CLI flag and fail_fast config option to stop mutation testing after N surviving mutants (default N=1 when flag given without value). Agents fix gaps iteratively, so discovering all survivors upfront is wasted work. A threshold gives control over the speed/thoroughness tradeoff: --fail-fast 1 for quick checks, --fail-fast 5 for CI gates, omit for full scans. Runner#call should stop running mutations and return a partial Summary once the threshold is reached. JSON output should include a 'truncated: true' field when fail-fast triggered.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.018733235+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:11:01.311500554+07:00","closed_at":"2026-03-16T10:11:01.311500554+07:00","close_reason":"Already merged and released in v0.4.0"}
35
+ {"id":"EV-23","title":"Per-mutation spec targeting","description":"Instead of running the full spec suite for every mutation, map each mutated source file to its relevant spec file(s) using convention-based resolution (e.g. lib/foo/bar.rb -> spec/foo/bar_spec.rb) and only run those. This dramatically reduces per-mutation test time. Depends on convention-based spec file resolution being implemented first.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.98620973+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T14:49:13.616876819+07:00","closed_at":"2026-03-16T14:49:13.616876819+07:00","close_reason":"Fixed and merged","dependencies":[{"issue_id":"EV-23","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
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
- {"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":"open","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-10T06:17:39.227881462+07:00"}
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":"open","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-10T06:17:43.883767452+07:00","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":"open","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-10T06:17:45.29866593+07:00"}
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":"open","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-10T06:17:46.306306092+07:00"}
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":"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"}]}
@@ -63,9 +63,9 @@
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
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"}
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":"open","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-10T06:18:06.843446356+07:00","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
- {"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":"in_progress","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-15T23:20:23.413703926+07:00"}
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":"in_progress","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-15T22:50:31.462784114+07:00"}
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
+ {"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
+ {"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"}
69
69
  {"id":"EV-35","title":"File left in mutated state on timeout","description":"Epic: File left in mutated state on timeout. Root cause: SIGKILL (fork.rb:45) is untrappable — child's ensure block (rspec.rb:24-26) never runs, so restore_original never executes. Affects Strategy B (direct file write) with data corruption and Strategy A (temp dir) with resource leaks. Decomposed into EV-37, EV-38, EV-39.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:27:59.54425065+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T00:34:44.130203912+07:00","closed_at":"2026-03-13T00:34:44.130203912+07:00","close_reason":"All sub-issues (EV-37, EV-38, EV-39) completed","dependencies":[{"issue_id":"EV-35","depends_on_id":"EV-37","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-35","depends_on_id":"EV-38","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-35","depends_on_id":"EV-39","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
70
70
  {"id":"EV-36","title":"Raise default timeout from 10s to 30s","description":"Default 10s timeout is too low when running the full test suite per mutation (no smart test selection yet). Agent testing showed 30s was needed. Raising the default prevents confusing timeout failures for new users.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:27:59.761882257+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-12T14:45:27.102490973+07:00","closed_at":"2026-03-12T14:45:27.102490973+07:00","close_reason":"Closed"}
71
71
  {"id":"EV-37","title":"Parent-side backup & restore for direct-write mutations","description":"Root cause: SIGKILL is untrappable, so when a mutation times out the child's ensure block (rspec.rb:24-26) never runs. If Strategy B (direct file write, rspec.rb:50-58) was used, the original file is left in a mutated state on disk. Fix: before forking, the parent must save a backup of the original file content. After the child exits (normally or via kill), the parent verifies the file is restored and restores it if not. This is the primary fix for EV-35.","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:32:49.590919391+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-12T23:04:21.990924732+07:00","closed_at":"2026-03-12T23:04:21.990924732+07:00","close_reason":"Implemented parent-side restore in Fork#call ensure block with read-before-write guard"}
@@ -81,10 +81,16 @@
81
81
  {"id":"EV-4.7","title":"Integrate parallel execution into Runner","description":"Update Runner to use Parallel::Pool when jobs > 1. Replace sequential mutation execution with pool.run. Verify results are identical. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.637953763+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:25.596270941+07:00","closed_at":"2026-03-02T11:54:25.596270941+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.7","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.7","depends_on_id":"EV-4.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.7","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
82
82
  {"id":"EV-4.8","title":"Integrate coverage-based filtering into Runner","description":"Update Runner to optionally collect coverage on first test suite run, build TestMap, and skip mutations on uncovered lines. Controlled by --no-coverage flag. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.745583233+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:35.638752306+07:00","closed_at":"2026-03-02T11:54:25.596274893+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.8","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.8","depends_on_id":"EV-4.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.8","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
83
83
  {"id":"EV-4.9","title":"Integrate diff-based targeting into Runner","description":"Update Runner to optionally use Diff::Parser + FileFilter when --diff flag is set. Filter subjects before mutation generation. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.851982561+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:25.596280246+07:00","closed_at":"2026-03-02T11:54:25.596280246+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.9","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.9","depends_on_id":"EV-4.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.9","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
84
- {"id":"EV-40","title":"Separate rspec noise from evilution output (stdout/stderr)","description":"RSpec warnings flood stdout and corrupt JSON output. When using --format json, the JSON gets buried in hundreds of lines of rspec warnings. Fix: redirect rspec subprocess output to stderr (or /dev/null), keep evilution results on stdout. Critical for piping JSON output.","status":"open","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:56:58.604909176+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T09:56:58.604909176+07:00"}
85
- {"id":"EV-41","title":"Progress indicator during mutation runs","description":"Zero output during multi-minute runs makes it look stuck. Add a simple 'mutation 3/19 killed...' progress line to stderr so users know work is happening. Only show in text mode or when stderr is a TTY.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:01.023293323+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T09:57:01.023293323+07:00"}
84
+ {"id":"EV-40","title":"Separate rspec noise from evilution output (stdout/stderr)","description":"RSpec warnings flood stdout and corrupt JSON output. When using --format json, the JSON gets buried in hundreds of lines of rspec warnings. Fix: redirect rspec subprocess output to stderr (or /dev/null), keep evilution results on stdout. Critical for piping JSON output.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:56:58.604909176+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T11:15:15.02691418+07:00","closed_at":"2026-03-16T11:15:15.02691418+07:00","close_reason":"Fixed and merged via PR #86"}
85
+ {"id":"EV-41","title":"Progress indicator during mutation runs","description":"Zero output during multi-minute runs makes it look stuck. Add a simple 'mutation 3/19 killed...' progress line to stderr so users know work is happening. Only show in text mode or when stderr is a TTY.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:01.023293323+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T11:38:40.674664201+07:00","closed_at":"2026-03-16T11:38:40.674664201+07:00","close_reason":"Fixed and merged via PR #87"}
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
- {"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":"open","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-13T09:57:05.048211823+07:00"}
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
+ {"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"}
88
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"}]}
89
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"}]}
90
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"}]}
@@ -92,6 +98,11 @@
92
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"}]}
93
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"}]}
94
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"}
95
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"}
96
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"}
97
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,52 @@
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
+
11
+ ## [0.5.0] - 2026-03-16
12
+
13
+ ### Added
14
+
15
+ - **Parallel execution** (`--jobs N` / `-j N`) — re-introduces parallel mutation execution using a process-based pool; each mutation runs in its own fork-isolated child process; fail-fast is checked between batches
16
+ - **Per-mutation spec targeting** — automatically resolves the matching spec file for each mutated source file using convention-based resolution; falls back to the full suite if no match; `--spec` flag overrides auto-detection
17
+ - **Progress indicator** — prints `mutation 3/19 killed` progress to stderr during text-mode runs so long-running sessions no longer appear stuck; only shown when stderr is a TTY, suppressed in quiet and JSON modes
18
+
19
+ ### Fixed
20
+
21
+ - **`--version` flag** — now correctly outputs the gem version instead of "version unknown"
22
+ - **RSpec noise suppression** — child process stdout/stderr is redirected to `/dev/null` so RSpec warnings no longer corrupt JSON output or flood the terminal
23
+
24
+ ## [0.4.0] - 2026-03-16
25
+
26
+ ### Added
27
+
28
+ - **`--fail-fast` flag** — stop after N surviving mutants (`--fail-fast`, `--fail-fast=3`, `--fail-fast 5`); defaults to 1 when given without a value
29
+ - **Structured JSON error responses** — errors in `--format json` mode now output structured JSON with `type`, `message`, and optional `file` fields
30
+ - **Convention-based spec file resolution** — automatically maps source files to their spec counterparts (`lib/` → `spec/`, `app/` → `spec/`)
31
+ - **`test_command` in mutation result JSON** — each mutation result now includes the RSpec command used, for easier debugging
32
+ - **Auto-detect changed files from git merge base** — when no explicit files are given, Evilution automatically finds changed `.rb` files under `lib/` and `app/` since the merge base with `main`/`master` (including `origin/` remotes for CI)
33
+
34
+ ### Changed
35
+
36
+ - Error classes (`ConfigError`, `ParseError`) now support a `file:` keyword for richer error context
37
+
38
+ ## [0.3.0] - 2026-03-13
39
+
40
+ ### Added
41
+
42
+ - **Sandbox-based temp directory cleanup** — leaked temp directories from timed-out children are now reliably cleaned up
43
+ - **Graceful timeout handling** — sends SIGTERM with a grace period before SIGKILL on child timeout
44
+
45
+ ### Changed
46
+
47
+ - Default per-mutation timeout increased from 10s to 30s
48
+ - Parent process now restores the original source file after each mutation (defense-in-depth)
49
+
3
50
  ## [0.2.0] - 2026-03-10
4
51
 
5
52
  ### 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
@@ -51,15 +58,7 @@ module Evilution
51
58
  i = 0
52
59
  while i < argv.length
53
60
  arg = argv[i]
54
- if %w[--jobs -j].include?(arg)
55
- warn("Warning: --jobs is no longer supported and will be ignored.")
56
- next_arg = argv[i + 1]
57
- numeric_next = next_arg && next_arg.match?(/\A-?\d+\z/)
58
- i += numeric_next ? 2 : 1
59
- elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
60
- warn("Warning: --jobs is no longer supported and will be ignored.")
61
- i += 1
62
- elsif arg == "--fail-fast"
61
+ if arg == "--fail-fast"
63
62
  next_arg = argv[i + 1]
64
63
 
65
64
  if next_arg && next_arg.match?(/\A-?\d+\z/)
@@ -83,6 +82,7 @@ module Evilution
83
82
  def build_option_parser
84
83
  OptionParser.new do |opts|
85
84
  opts.banner = "Usage: evilution [command] [options] [files...]"
85
+ opts.version = VERSION
86
86
  add_separators(opts)
87
87
  add_options(opts)
88
88
  end
@@ -92,12 +92,13 @@ module Evilution
92
92
  opts.separator ""
93
93
  opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
94
94
  opts.separator ""
95
- opts.separator "Commands: run (default), init, version"
95
+ opts.separator "Commands: run (default), init, mcp, version"
96
96
  opts.separator ""
97
97
  opts.separator "Options:"
98
98
  end
99
99
 
100
100
  def add_options(opts)
101
+ opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
101
102
  opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
102
103
  opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
103
104
  opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
@@ -114,6 +115,7 @@ module Evilution
114
115
  end
115
116
  opts.on("--fail-fast", "Stop after N surviving mutants " \
116
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 }
117
119
  opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
118
120
  opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
119
121
  end
@@ -130,6 +132,28 @@ module Evilution
130
132
  0
131
133
  end
132
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
+
133
157
  def parse_file_args(raw_args)
134
158
  files = []
135
159
  ranges = {}
@@ -158,6 +182,8 @@ module Evilution
158
182
  end
159
183
 
160
184
  def run_mutations
185
+ raise ConfigError, @stdin_error if @stdin_error
186
+
161
187
  file_options = Config.file_options
162
188
  config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
163
189
  runner = Runner.new(config: config)
@@ -16,6 +16,7 @@ module Evilution
16
16
  coverage: true,
17
17
  verbose: false,
18
18
  quiet: false,
19
+ jobs: 1,
19
20
  fail_fast: nil,
20
21
  line_ranges: {},
21
22
  spec_files: []
@@ -23,7 +24,7 @@ module Evilution
23
24
 
24
25
  attr_reader :target_files, :timeout, :format, :diff_base,
25
26
  :target, :min_score, :integration, :coverage, :verbose, :quiet,
26
- :fail_fast, :line_ranges, :spec_files
27
+ :jobs, :fail_fast, :line_ranges, :spec_files
27
28
 
28
29
  def initialize(**options)
29
30
  file_options = options.delete(:skip_config_file) ? {} : load_config_file
@@ -39,6 +40,7 @@ module Evilution
39
40
  @coverage = merged[:coverage]
40
41
  @verbose = merged[:verbose]
41
42
  @quiet = merged[:quiet]
43
+ @jobs = validate_jobs(merged[:jobs])
42
44
  @fail_fast = validate_fail_fast(merged[:fail_fast])
43
45
  @line_ranges = merged[:line_ranges] || {}
44
46
  @spec_files = Array(merged[:spec_files])
@@ -102,6 +104,9 @@ module Evilution
102
104
  # Test integration: rspec (default: rspec)
103
105
  # integration: rspec
104
106
 
107
+ # Number of parallel workers (default: 1)
108
+ # jobs: 1
109
+
105
110
  # Stop after N surviving mutants (default: disabled)
106
111
  # fail_fast: 1
107
112
 
@@ -123,12 +128,18 @@ module Evilution
123
128
  raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
124
129
  end
125
130
 
126
- def warn_removed_options(merged, file_options)
127
- if merged.key?(:jobs)
128
- warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
129
- "Remove it from your configuration or invocation.")
130
- end
131
+ def validate_jobs(value)
132
+ raise ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
133
+
134
+ value = Integer(value)
135
+ raise ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
136
+
137
+ value
138
+ rescue ::ArgumentError, ::TypeError
139
+ raise ConfigError, "jobs must be a positive integer, got #{value.inspect}"
140
+ end
131
141
 
142
+ def warn_removed_options(_merged, file_options)
132
143
  if file_options.key?(:coverage)
133
144
  warn("Warning: 'coverage' in config file is deprecated and ignored. " \
134
145
  "This option will be removed in a future version.")
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "stringio"
5
5
  require "tmpdir"
6
6
  require_relative "base"
7
+ require_relative "../spec_resolver"
7
8
 
8
9
  module Evilution
9
10
  module Integration
@@ -99,6 +100,7 @@ module Evilution
99
100
 
100
101
  out = StringIO.new
101
102
  err = StringIO.new
103
+ command = "rspec"
102
104
  args = build_args(mutation)
103
105
  command = "rspec #{args.join(" ")}"
104
106
 
@@ -109,10 +111,17 @@ module Evilution
109
111
  { passed: false, error: e.message, test_command: command }
110
112
  end
111
113
 
112
- def build_args(_mutation)
113
- files = test_files || ["spec"]
114
+ def build_args(mutation)
115
+ files = resolve_test_files(mutation)
114
116
  ["--format", "progress", "--no-color", "--order", "defined", *files]
115
117
  end
118
+
119
+ def resolve_test_files(mutation)
120
+ return test_files if test_files
121
+
122
+ resolved = SpecResolver.new.call(mutation.file_path)
123
+ resolved ? [resolved] : ["spec"]
124
+ end
116
125
  end
117
126
  end
118
127
  end
@@ -16,6 +16,7 @@ module Evilution
16
16
  pid = ::Process.fork do
17
17
  ENV["TMPDIR"] = sandbox_dir
18
18
  read_io.close
19
+ suppress_child_output
19
20
  result = execute_in_child(mutation, test_command)
20
21
  Marshal.dump(result, write_io)
21
22
  write_io.close
@@ -44,6 +45,11 @@ module Evilution
44
45
  warn("Warning: failed to restore #{mutation.file_path}: #{e.message}")
45
46
  end
46
47
 
48
+ def suppress_child_output
49
+ $stdout.reopen(File::NULL, "w")
50
+ $stderr.reopen(File::NULL, "w")
51
+ end
52
+
47
53
  def execute_in_child(mutation, test_command)
48
54
  test_command.call(mutation)
49
55
  rescue StandardError => e
@@ -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
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Parallel
5
+ class Pool
6
+ def initialize(size:)
7
+ raise ArgumentError, "pool size must be a positive integer, got #{size.inspect}" unless size.is_a?(Integer) && size >= 1
8
+
9
+ @size = size
10
+ end
11
+
12
+ def map(items, &block)
13
+ results = []
14
+
15
+ items.each_slice(@size) do |batch|
16
+ results.concat(run_batch(batch, &block))
17
+ end
18
+
19
+ results
20
+ end
21
+
22
+ private
23
+
24
+ def run_batch(items, &block)
25
+ entries = items.map do |item|
26
+ read_io, write_io = IO.pipe
27
+ pid = fork_worker(item, read_io, write_io, &block)
28
+ write_io.close
29
+ { pid: pid, read_io: read_io }
30
+ end
31
+
32
+ collect_results(entries)
33
+ end
34
+
35
+ def fork_worker(item, read_io, write_io, &block)
36
+ Process.fork do
37
+ read_io.close
38
+ result = block.call(item)
39
+ Marshal.dump(result, write_io)
40
+ rescue Exception => e # rubocop:disable Lint/RescueException
41
+ Marshal.dump(e, write_io)
42
+ ensure
43
+ write_io.close
44
+ exit!
45
+ end
46
+ end
47
+
48
+ def collect_results(entries)
49
+ entries.map do |entry|
50
+ data = entry[:read_io].read
51
+ entry[:read_io].close
52
+ Process.wait(entry[:pid])
53
+ raise Evilution::Error, "worker process failed with no result" if data.empty?
54
+
55
+ result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
56
+ raise result if result.is_a?(Exception)
57
+
58
+ result
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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"
@@ -13,6 +13,7 @@ require_relative "diff/file_filter"
13
13
  require_relative "git/changed_files"
14
14
  require_relative "result/mutation_result"
15
15
  require_relative "result/summary"
16
+ require_relative "parallel/pool"
16
17
 
17
18
  module Evilution
18
19
  class Runner
@@ -86,6 +87,14 @@ module Evilution
86
87
  end
87
88
 
88
89
  def run_mutations(mutations)
90
+ if config.jobs > 1
91
+ run_mutations_parallel(mutations)
92
+ else
93
+ run_mutations_sequential(mutations)
94
+ end
95
+ end
96
+
97
+ def run_mutations_sequential(mutations)
89
98
  integration = build_integration
90
99
  results = []
91
100
  survived_count = 0
@@ -100,6 +109,7 @@ module Evilution
100
109
  )
101
110
  results << result
102
111
  survived_count += 1 if result.survived?
112
+ log_progress(index + 1, mutations.length, result.status)
103
113
 
104
114
  if config.fail_fast? && survived_count >= config.fail_fast && index < mutations.length - 1
105
115
  truncated = true
@@ -110,6 +120,39 @@ module Evilution
110
120
  [results, truncated]
111
121
  end
112
122
 
123
+ def run_mutations_parallel(mutations)
124
+ integration = build_integration
125
+ pool = Parallel::Pool.new(size: config.jobs)
126
+ results = []
127
+ survived_count = 0
128
+ truncated = false
129
+ completed = 0
130
+
131
+ mutations.each_slice(config.jobs) do |batch|
132
+ break if truncated
133
+
134
+ batch_results = pool.map(batch) do |mutation|
135
+ test_command = ->(m) { integration.call(m) }
136
+ isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
137
+ end
138
+
139
+ batch_results.each do |result|
140
+ results << result
141
+ survived_count += 1 if result.survived?
142
+ completed += 1
143
+ log_progress(completed, mutations.length, result.status)
144
+ end
145
+
146
+ truncated = true if should_truncate?(survived_count, completed, mutations.length)
147
+ end
148
+
149
+ [results, truncated]
150
+ end
151
+
152
+ def should_truncate?(survived_count, completed, total)
153
+ config.fail_fast? && survived_count >= config.fail_fast && completed < total
154
+ end
155
+
113
156
  def build_integration
114
157
  case config.integration
115
158
  when :rspec
@@ -128,6 +171,12 @@ module Evilution
128
171
  $stdout.puts(output) unless config.quiet
129
172
  end
130
173
 
174
+ def log_progress(current, total, status)
175
+ return if config.quiet || !config.text? || !$stderr.tty?
176
+
177
+ $stderr.write("mutation #{current}/#{total} #{status}\n")
178
+ end
179
+
131
180
  def build_reporter
132
181
  case config.format
133
182
  when :json
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -25,8 +25,10 @@ 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"
31
+ require_relative "evilution/parallel/pool"
30
32
  require_relative "evilution/diff/parser"
31
33
  require_relative "evilution/diff/file_filter"
32
34
  require_relative "evilution/git/changed_files"
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.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-16 00:00:00.000000000 Z
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
@@ -91,6 +114,7 @@ files:
91
114
  - lib/evilution/mutator/operator/string_literal.rb
92
115
  - lib/evilution/mutator/operator/symbol_literal.rb
93
116
  - lib/evilution/mutator/registry.rb
117
+ - lib/evilution/parallel/pool.rb
94
118
  - lib/evilution/reporter/cli.rb
95
119
  - lib/evilution/reporter/json.rb
96
120
  - lib/evilution/reporter/suggestion.rb