evilution 0.1.0 → 0.2.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: 34fdc4c7cf9abac2c71dc0aa7d1aa4e4452e6237b8f1564c2dd92db2f5746cf5
4
- data.tar.gz: 6d7bb8ef692815bbc62d8ca2a3446ee642bdc2c1834856064a48e4c311256b63
3
+ metadata.gz: d8dc33cb03f8b2d2d94c2ca64ade2397c1933c1b431c5b804637949451ede95d
4
+ data.tar.gz: 36bf7bc0172dd6a7262f5ef8c716dbe64662c1c7c2c7a05b0ef0109d3479131d
5
5
  SHA512:
6
- metadata.gz: 9f08ab9b14a0b43e26a15e499d65ce819f5443efa77311ff45bccf3ae230b3f4fc61bc1d20cf4beb464b6c70ca82964a21a5f8d889edb30b5ab2f18f8af3ca44
7
- data.tar.gz: 84ba0567669d197cc56a2723a5e0fdeca84cf2b74085b5a273d9d7279211d156397ae520e7986fba81562393d25e21e3c99a1b6626cf472f79eeae11fede48ba
6
+ metadata.gz: d8ae3315b4c8b118e60f2a84c1d9d7854fb9f666997db5ae5697d6d9ac12838724a6be10627cc2c30bb2c8bd5e4f7babb97086fb27616b378e07e1fcc9c0ca0e
7
+ data.tar.gz: 557abcaebed599383335a06f913c04565513240e984f65304d4fd8f547c38dc0bc58d8c34a7f465d3947df56ba88277516970b87e791f1811d323a6f3b8562fe
@@ -1 +1 @@
1
- 1772686671
1
+ 1773064889
data/.beads/issues.jsonl CHANGED
@@ -5,10 +5,16 @@
5
5
  {"id":"EV-1.4","title":"Delete services.md prompt","description":"Remove .claude/prompts/services.md — not applicable to a gem project.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.649482526+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:19.226636126+07:00","closed_at":"2026-03-02T10:41:19.226636126+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.4","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
6
6
  {"id":"EV-1.5","title":"Update gemspec metadata","description":"Fill in summary, description, homepage, source_code_uri, changelog_uri. Add runtime dependency: diff-lcs (>= 1.5, < 3). Set allowed_push_host to rubygems.org.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.756448433+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:47.168270415+07:00","closed_at":"2026-03-02T10:41:47.168270415+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.5","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
7
7
  {"id":"EV-1.6","title":"Update .rubocop.yml for gem conventions","description":"Configure rubocop for gem: enable NewCops, set reasonable line length, exclude spec fixtures.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.855784448+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:42:03.456572349+07:00","closed_at":"2026-03-02T10:42:03.456572349+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.6","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
8
- {"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:53.571182801+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T16:21:53.571182801+07:00"}
8
+ {"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:53.571182801+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:44:35.748868691+07:00","closed_at":"2026-03-06T11:44:35.748868691+07:00","close_reason":"Published to RubyGems"}
9
9
  {"id":"EV-11","title":"Wire coverage-based filtering into Runner","description":"Collector and TestMap exist and pass specs, but Runner never calls them. When config.coverage is true, Runner collects aggregate line-level coverage via Coverage::Collector, builds a Coverage::TestMap, and skips mutations on lines that no test exercises (marking them as survived with zero duration). Note: Ruby Coverage provides aggregate per-file line hit counts, not per-test-file tracking, so we filter out uncovered mutations rather than selecting specific test files.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-04T11:52:42.607616728+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:25.4848637+07:00","closed_at":"2026-03-05T14:53:24.894588088+07:00","close_reason":"Coverage-based test selection wired into Runner"}
10
10
  {"id":"EV-12","title":"Resolve suggestion gap","description":"The workflow section instructs agents to read a suggestion field from survived[], but the JSON reporter output does not include suggestion (lib/evilution/reporter/json.rb only emits operator/file/line/status/duration/diff). Either add suggestion to the JSON output/schema or update the workflow steps to match the actual report fields. See GitHub issue #12.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T12:33:23.674094791+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:05:48.785616141+07:00","closed_at":"2026-03-05T13:05:48.785616141+07:00","close_reason":"Wired Suggestion into JSON reporter for survived mutations, updated README schema, added specs"}
11
11
  {"id":"EV-13","title":"Enable true per-mutation isolation with temp file copies","description":"Replace direct file writes with temp-dir + $LOAD_PATH isolation so multiple workers can mutate the same file in parallel. Remove per-file grouping from Pool#partition.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T13:42:04.711580397+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:44:34.779951959+07:00","closed_at":"2026-03-05T13:44:34.779951959+07:00","close_reason":"Implemented temp file isolation via $LOAD_PATH and round-robin partition"}
12
+ {"id":"EV-14","title":"Add changelog_uri to gemspec metadata","description":"The published gem on RubyGems is missing the Changelog link. Add changelog_uri to spec.metadata in evilution.gemspec pointing to CHANGELOG.md on master.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-06T11:46:12.648668594+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:47:41.23691915+07:00","closed_at":"2026-03-06T11:47:41.23691915+07:00","close_reason":"Added changelog_uri to gemspec"}
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
+ {"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
+ {"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"}
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"}
12
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"}
13
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"}]}
14
20
  {"id":"EV-2.10","title":"Implement Evilution::Integration::Base and RSpec adapter","description":"Base: abstract adapter with interface #call(test_files) -> {passed: bool, example_count: int, failure_count: int}. RSpec: programmatic runner using RSpec::Core::Runner.run with StringIO for capture. Auto-detects spec/ directory. Files: lib/evilution/integration/base.rb, lib/evilution/integration/rspec.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.246990324+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:58:18.324768622+07:00","closed_at":"2026-03-02T10:58:18.324768622+07:00","close_reason":"Integration::Base and Integration::RSpec implemented with 8 passing specs","dependencies":[{"issue_id":"EV-2.10","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
@@ -23,6 +29,7 @@
23
29
  {"id":"EV-2.7","title":"Implement ComparisonReplacement operator","description":"First mutation operator. Targets CallNode where name is :>, :<, :>=, :<=, :==, :!=. Replacements: > -> [>=, ==], < -> [<=, ==], >= -> [>, ==], <= -> [<, ==], == -> [!=], != -> [==]. Uses SourceSurgeon to apply text replacement at message_loc offsets. File: lib/evilution/mutator/operator/comparison_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.917501278+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:50:01.327985674+07:00","closed_at":"2026-03-02T10:50:01.327985674+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.7","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
24
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"}]}
25
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
+ {"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"}
26
33
  {"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"}]}
27
34
  {"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"}]}
28
35
  {"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"}]}
@@ -30,7 +37,7 @@
30
37
  {"id":"EV-3.12","title":"Implement ConditionalBranch operator","description":"Targets IfNode with both if and else branches. Mutations: replace entire if/else with if-branch only, replace with else-branch only. File: lib/evilution/mutator/operator/conditional_branch.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.138508575+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541106251+07:00","closed_at":"2026-03-02T11:13:36.541106251+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.12","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.12","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
31
38
  {"id":"EV-3.13","title":"Implement StatementDeletion operator","description":"Targets StatementsNode bodies with 2+ statements. For each statement, produce a mutation that removes it. File: lib/evilution/mutator/operator/statement_deletion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.240151154+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954716442+07:00","closed_at":"2026-03-02T11:16:08.954716442+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.13","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.13","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
32
39
  {"id":"EV-3.14","title":"Implement MethodBodyReplacement operator","description":"Targets DefNode with body. Mutations: replace body with nil, replace with raise NotImplementedError. File: lib/evilution/mutator/operator/method_body_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.333934644+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954887847+07:00","closed_at":"2026-03-02T11:16:08.954887847+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.14","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.14","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
33
- {"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.95490096+07:00","closed_at":"2026-03-02T11:16:08.95490096+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.15","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.15","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
40
+ {"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T16:11:55.50396051+07:00","closed_at":"2026-03-02T11:16:08.95490096+07:00","close_reason":"Commands now listed in --help output","dependencies":[{"issue_id":"EV-3.15","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.15","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
34
41
  {"id":"EV-3.16","title":"Implement ReturnValueRemoval operator","description":"Targets ReturnNode with arguments. Mutations: return x -> return, return x -> return nil. File: lib/evilution/mutator/operator/return_value_removal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.541333631+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396683739+07:00","closed_at":"2026-03-02T11:18:14.396683739+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.16","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.16","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
35
42
  {"id":"EV-3.17","title":"Implement CollectionReplacement operator","description":"Targets CallNode where name matches collection methods. Replacements: map->each, select<->reject, all?<->any?, first<->last, min<->max, flat_map->map. File: lib/evilution/mutator/operator/collection_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.635444378+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396880048+07:00","closed_at":"2026-03-02T11:18:14.396880048+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.17","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.17","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
36
43
  {"id":"EV-3.18","title":"Implement Evilution::CLI","description":"OptionParser-based CLI. Commands: run [FILES...], version, init. Flags: --jobs/-j, --timeout/-t, --format/-f (json/text), --diff BASE, --target PATTERN, --min-score FLOAT, --no-coverage, --config FILE, --verbose/-v, --quiet/-q. Parses args into Config, delegates to Runner. File: lib/evilution/cli.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:26.540459363+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:20:50.084035782+07:00","closed_at":"2026-03-02T11:20:50.084035782+07:00","close_reason":"CLI, Reporter::CLI, and Registry registration all implemented","dependencies":[{"issue_id":"EV-3.18","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
@@ -63,6 +70,6 @@
63
70
  {"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"}]}
64
71
  {"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"}]}
65
72
  {"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"}
66
- {"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":"open","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-02T16:21:46.820210376+07:00"}
73
+ {"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"}
67
74
  {"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."}
68
75
  {"id":"EV-9","title":"Add multi-Ruby CI test matrix (3.2, 3.3, 4.0)","description":"CI currently only tests Ruby 4.0.1. Add a matrix strategy testing Ruby 3.2, 3.3, and 4.0 to ensure compatibility across supported Ruby versions. Prism ships with Ruby 3.3+ so 3.2 may need the prism gem as a dependency.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:51.239774764+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.83181612+07:00","closed_at":"2026-03-05T12:31:51.83181612+07:00","close_reason":"Closed"}
@@ -73,7 +73,6 @@ Evilution
73
73
  ::Coverage::TestMap # Source line → test file mapping
74
74
  ::Diff::Parser # Git diff output parser
75
75
  ::Diff::FileFilter # Filter subjects to changed code
76
- ::Parallel::Pool # Thread-based worker pool
77
76
  ::Result::MutationResult # Single mutation outcome
78
77
  ::Result::Summary # Aggregated results
79
78
  ::Reporter::JSON # Structured output for AI agents
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-03-10
4
+
5
+ ### Added
6
+
7
+ - **Line-range targeting** — scope mutations to exact lines: `lib/foo.rb:15-30`, `lib/foo.rb:15`, `lib/foo.rb:15-`
8
+ - **Method-name targeting** (`--target`) — mutate a single method by fully-qualified name (e.g. `Foo::Bar#calculate`)
9
+ - **Commands section** in `--help` output
10
+
11
+ ### Changed
12
+
13
+ - Added `changelog_uri` to gemspec metadata
14
+ - Added GitHub publish workflow
15
+
16
+ ### Deprecated
17
+
18
+ - **`--diff` flag** — use line-range targeting instead
19
+ - **Coverage-based filtering flags/config** (`--no-coverage` flag and `coverage` config key) — deprecated and now ignored; coverage-based filtering behavior has been removed from `Runner`
20
+
21
+ ### Removed
22
+
23
+ - **Parallel execution** (`--jobs` flag) — simplifies codebase for AI-agent-first design; will be reintroduced later
24
+ - **File-discovery logic** from `Integration::RSpec` — spec files are now passed explicitly or default to `spec/`
25
+
3
26
  ## [0.1.0] - 2026-03-02
4
27
 
5
28
  ### Added
data/README.md CHANGED
@@ -37,12 +37,13 @@ evilution [command] [options] [files...]
37
37
 
38
38
  | Flag | Type | Default | Description |
39
39
  |-------------------------|---------|--------------|---------------------------------------------------|
40
- | `-j`, `--jobs N` | Integer | CPU cores | Parallel worker count. Use `1` for sequential. |
41
40
  | `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
42
41
  | `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
43
- | `--diff BASE` | String | _(none)_ | Git ref. Only mutate methods whose definition line changed since BASE. |
42
+ | `--target METHOD` | String | _(none)_ | Only mutate the named method (e.g. `Foo::Bar#calculate`). |
43
+ | `--diff BASE` | String | _(none)_ | **DEPRECATED**: Use line-range targeting instead. Git ref. Only mutate methods whose definition line changed since BASE. |
44
44
  | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
45
- | `--no-coverage` | Boolean | false | Reserved; currently has no effect. |
45
+ | `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `spec/`. |
46
+ | `--no-coverage` | Boolean | false | **DEPRECATED, NO-OP**: Kept for backward compatibility. Will be removed. |
46
47
  | `-v`, `--verbose` | Boolean | false | Verbose output. |
47
48
  | `-q`, `--quiet` | Boolean | false | Suppress output. |
48
49
 
@@ -61,12 +62,10 @@ Generate default config: `bundle exec evilution init`
61
62
  Creates `.evilution.yml`:
62
63
 
63
64
  ```yaml
64
- # jobs: 4 # parallel workers
65
65
  # timeout: 10 # seconds per mutation
66
66
  # format: text # text | json
67
67
  # min_score: 0.0 # 0.0–1.0
68
68
  # integration: rspec # test framework
69
- # coverage: true # skip mutations on uncovered lines
70
69
  ```
71
70
 
72
71
  **Precedence**: CLI flags override `.evilution.yml` values.
@@ -137,20 +136,51 @@ Each operator name is stable and appears in JSON output under `survived[].operat
137
136
  ### 1. Full project scan
138
137
 
139
138
  ```bash
140
- bundle exec evilution run lib/ --format json --jobs 4 --min-score 0.8
139
+ bundle exec evilution run lib/ --format json --min-score 0.8
141
140
  ```
142
141
 
143
142
  Parse JSON output. Exit code 0 = pass, 1 = surviving mutants to address.
144
143
 
145
- ### 2. PR / diff-only scan (fast feedback)
144
+ ### 2. PR / changed-lines scan (fast feedback)
146
145
 
147
146
  ```bash
148
- bundle exec evilution run lib/ --format json --diff main --min-score 0.9
147
+ bundle exec evilution run lib/foo.rb:15-30 lib/bar.rb:5-20 --format json --min-score 0.9
149
148
  ```
150
149
 
151
- Mutates methods whose definition (starting) line is changed compared to `main` (the diff filter is based on the method’s first line, not any line in its body). Use this for incremental checks — it's fast and focused on newly added or moved methods and changed signatures.
150
+ Target the exact lines you changed for fast, focused mutation testing. See line-range syntax below.
152
151
 
153
- ### 3. Single-file targeted scan
152
+ > **Note**: `--diff BASE` is deprecated and will be removed in a future version. Prefer line-range targeting for new workflows.
153
+
154
+ ### 3. Line-range targeted scan (fastest)
155
+
156
+ ```bash
157
+ bundle exec evilution run lib/foo.rb:15-30 --format json
158
+ ```
159
+
160
+ Target exact lines you changed. Supports multiple syntaxes:
161
+
162
+ ```bash
163
+ evilution run lib/foo.rb:15-30 # lines 15 through 30
164
+ evilution run lib/foo.rb:15 # single line 15
165
+ evilution run lib/foo.rb:15- # from line 15 to end of file
166
+ evilution run lib/foo.rb # whole file (existing behavior)
167
+ ```
168
+
169
+ Methods whose body overlaps the requested range are included. Mix targeted and whole-file arguments freely:
170
+
171
+ ```bash
172
+ evilution run lib/foo.rb:15-30 lib/bar.rb --format json
173
+ ```
174
+
175
+ ### 4. Method-name targeted scan
176
+
177
+ ```bash
178
+ bundle exec evilution run lib/foo.rb --target Foo::Bar#calculate --format json
179
+ ```
180
+
181
+ Target a specific method by its fully-qualified name. Useful when you want to focus on a single method without knowing its exact line numbers.
182
+
183
+ ### 5. Single-file targeted scan
154
184
 
155
185
  ```bash
156
186
  bundle exec evilution run lib/specific_file.rb --format json
@@ -158,7 +188,7 @@ bundle exec evilution run lib/specific_file.rb --format json
158
188
 
159
189
  Use when you know which file was modified and want to verify its test coverage.
160
190
 
161
- ### 4. Fixing surviving mutants
191
+ ### 6. Fixing surviving mutants
162
192
 
163
193
  For each entry in `survived[]`:
164
194
  1. Read `file` at `line` to understand the code context
@@ -167,7 +197,7 @@ For each entry in `survived[]`:
167
197
  4. Write a test that would fail if the mutation were applied
168
198
  5. Re-run evilution on just that file to verify the mutant is now killed
169
199
 
170
- ### 5. CI gate
200
+ ### 7. CI gate
171
201
 
172
202
  ```bash
173
203
  bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
data/lib/evilution/cli.rb CHANGED
@@ -11,7 +11,27 @@ module Evilution
11
11
  @options = {}
12
12
  @command = :run
13
13
  argv = argv.dup
14
+ argv = extract_command(argv)
15
+ argv = warn_removed_flags(argv)
16
+ raw_args = build_option_parser.parse!(argv)
17
+ @files, @line_ranges = parse_file_args(raw_args)
18
+ end
14
19
 
20
+ def call
21
+ case @command
22
+ when :version
23
+ $stdout.puts(VERSION)
24
+ 0
25
+ when :init
26
+ run_init
27
+ when :run
28
+ run_mutations
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def extract_command(argv)
15
35
  case argv.first
16
36
  when "version"
17
37
  @command = :version
@@ -22,37 +42,65 @@ module Evilution
22
42
  when "run"
23
43
  argv.shift
24
44
  end
45
+ argv
46
+ end
47
+
48
+ def warn_removed_flags(argv)
49
+ result = []
50
+ i = 0
51
+ while i < argv.length
52
+ arg = argv[i]
53
+ if %w[--jobs -j].include?(arg)
54
+ warn("Warning: --jobs is no longer supported and will be ignored.")
55
+ next_arg = argv[i + 1]
56
+ i += next_arg&.match?(/\A-?\d+\z/) ? 2 : 1
57
+ elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
58
+ warn("Warning: --jobs is no longer supported and will be ignored.")
59
+ i += 1
60
+ else
61
+ result << arg
62
+ i += 1
63
+ end
64
+ end
65
+ result
66
+ end
25
67
 
26
- parser = OptionParser.new do |opts|
68
+ def build_option_parser
69
+ OptionParser.new do |opts|
27
70
  opts.banner = "Usage: evilution [command] [options] [files...]"
28
-
29
- opts.on("-j", "--jobs N", Integer, "Number of parallel workers") { |n| @options[:jobs] = n }
30
- opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
31
- opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
32
- opts.on("--diff BASE", "Only mutate code changed since BASE") { |b| @options[:diff_base] = b }
33
- opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
34
- opts.on("--no-coverage", "Disable coverage-based filtering of uncovered mutations") { @options[:coverage] = false }
35
- opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
36
- opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
71
+ add_separators(opts)
72
+ add_options(opts)
37
73
  end
74
+ end
38
75
 
39
- @files = parser.parse!(argv)
76
+ def add_separators(opts)
77
+ opts.separator ""
78
+ opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
79
+ opts.separator ""
80
+ opts.separator "Commands: run (default), init, version"
81
+ opts.separator ""
82
+ opts.separator "Options:"
40
83
  end
41
84
 
42
- def call
43
- case @command
44
- when :version
45
- $stdout.puts(VERSION)
46
- 0
47
- when :init
48
- run_init
49
- when :run
50
- run_mutations
85
+ def add_options(opts)
86
+ opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
87
+ opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
88
+ opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
89
+ warn("Warning: --diff is deprecated and will be removed in a future version. " \
90
+ "Use line-range targeting instead: evilution run lib/foo.rb:15-30")
91
+ @options[:diff_base] = b
51
92
  end
93
+ opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
94
+ opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
95
+ opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
96
+ opts.on("--no-coverage", "DEPRECATED: Has no effect and will be removed in a future version") do
97
+ warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
98
+ @options[:coverage] = false
99
+ end
100
+ opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
101
+ opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
52
102
  end
53
103
 
54
- private
55
-
56
104
  def run_init
57
105
  path = ".evilution.yml"
58
106
  if File.exist?(path)
@@ -65,8 +113,35 @@ module Evilution
65
113
  0
66
114
  end
67
115
 
116
+ def parse_file_args(raw_args)
117
+ files = []
118
+ ranges = {}
119
+
120
+ raw_args.each do |arg|
121
+ file, range_str = arg.split(":", 2)
122
+ files << file
123
+ next unless range_str
124
+
125
+ ranges[file] = parse_line_range(range_str)
126
+ end
127
+
128
+ [files, ranges]
129
+ end
130
+
131
+ def parse_line_range(str)
132
+ if str.include?("-")
133
+ start_str, end_str = str.split("-", 2)
134
+ start_line = Integer(start_str)
135
+ end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
136
+ start_line..end_line
137
+ else
138
+ line = Integer(str)
139
+ line..line
140
+ end
141
+ end
142
+
68
143
  def run_mutations
69
- config = Config.new(**@options, target_files: @files)
144
+ config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
70
145
  runner = Runner.new(config: config)
71
146
  summary = runner.call
72
147
  summary.success?(min_score: config.min_score) ? 0 : 1
@@ -7,7 +7,6 @@ module Evilution
7
7
  CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
8
8
 
9
9
  DEFAULTS = {
10
- jobs: nil,
11
10
  timeout: 10,
12
11
  format: :text,
13
12
  diff_base: nil,
@@ -16,17 +15,20 @@ module Evilution
16
15
  integration: :rspec,
17
16
  coverage: true,
18
17
  verbose: false,
19
- quiet: false
18
+ quiet: false,
19
+ line_ranges: {},
20
+ spec_files: []
20
21
  }.freeze
21
22
 
22
- attr_reader :target_files, :jobs, :timeout, :format, :diff_base,
23
- :target, :min_score, :integration, :coverage, :verbose, :quiet
23
+ attr_reader :target_files, :timeout, :format, :diff_base,
24
+ :target, :min_score, :integration, :coverage, :verbose, :quiet,
25
+ :line_ranges, :spec_files
24
26
 
25
27
  def initialize(**options)
26
28
  file_options = options.delete(:skip_config_file) ? {} : load_config_file
27
29
  merged = DEFAULTS.merge(file_options).merge(options)
30
+ warn_removed_options(merged, file_options)
28
31
  @target_files = Array(merged[:target_files])
29
- @jobs = merged[:jobs] || default_jobs
30
32
  @timeout = merged[:timeout]
31
33
  @format = merged[:format].to_sym
32
34
  @diff_base = merged[:diff_base]
@@ -36,6 +38,8 @@ module Evilution
36
38
  @coverage = merged[:coverage]
37
39
  @verbose = merged[:verbose]
38
40
  @quiet = merged[:quiet]
41
+ @line_ranges = merged[:line_ranges] || {}
42
+ @spec_files = Array(merged[:spec_files])
39
43
  freeze
40
44
  end
41
45
 
@@ -51,15 +55,20 @@ module Evilution
51
55
  !diff_base.nil?
52
56
  end
53
57
 
58
+ def line_ranges?
59
+ !line_ranges.empty?
60
+ end
61
+
62
+ def target?
63
+ !target.nil?
64
+ end
65
+
54
66
  # Generates a default config file template.
55
67
  def self.default_template
56
68
  <<~YAML
57
69
  # Evilution configuration
58
70
  # See: https://github.com/marinazzio/evilution
59
71
 
60
- # Number of parallel workers (default: number of CPU cores)
61
- # jobs: 4
62
-
63
72
  # Per-mutation timeout in seconds (default: 10)
64
73
  # timeout: 10
65
74
 
@@ -72,13 +81,30 @@ module Evilution
72
81
  # Test integration: rspec (default: rspec)
73
82
  # integration: rspec
74
83
 
75
- # Skip mutations on uncovered lines (default: true)
84
+ # DEPRECATED: Coverage filtering is deprecated and will be removed
76
85
  # coverage: true
77
86
  YAML
78
87
  end
79
88
 
80
89
  private
81
90
 
91
+ def warn_removed_options(merged, file_options)
92
+ if merged.key?(:jobs)
93
+ warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
94
+ "Remove it from your configuration or invocation.")
95
+ end
96
+
97
+ if file_options.key?(:coverage)
98
+ warn("Warning: 'coverage' in config file is deprecated and ignored. " \
99
+ "This option will be removed in a future version.")
100
+ end
101
+
102
+ return unless file_options[:diff_base]
103
+
104
+ warn("Warning: 'diff_base' in config file is deprecated and will be removed in a future version. " \
105
+ "Use line-range targeting instead: evilution run lib/foo.rb:15-30")
106
+ end
107
+
82
108
  def load_config_file
83
109
  CONFIG_FILES.each do |path|
84
110
  next unless File.exist?(path)
@@ -89,10 +115,5 @@ module Evilution
89
115
 
90
116
  {}
91
117
  end
92
-
93
- def default_jobs
94
- require "etc"
95
- Etc.nprocessors
96
- end
97
118
  end
98
119
  end
@@ -108,77 +108,10 @@ module Evilution
108
108
  { passed: false, error: e.message }
109
109
  end
110
110
 
111
- def build_args(mutation)
112
- files = test_files || detect_test_files(mutation)
111
+ def build_args(_mutation)
112
+ files = test_files || ["spec"]
113
113
  ["--format", "progress", "--no-color", "--order", "defined", *files]
114
114
  end
115
-
116
- def detect_test_files(mutation)
117
- # Convention: lib/foo/bar.rb -> spec/foo/bar_spec.rb
118
- candidates = spec_file_candidates(mutation.file_path)
119
- found = candidates.select { |f| File.exist?(f) }
120
- return found unless found.empty?
121
-
122
- # Fallback: find spec/ directory relative to the mutation's project root
123
- fallback = fallback_spec_dir(mutation.file_path)
124
- return [fallback] if fallback
125
-
126
- []
127
- end
128
-
129
- def fallback_spec_dir(source_path)
130
- expanded = File.expand_path(source_path)
131
-
132
- # Derive spec/ from mutation's project, not CWD
133
- if expanded.include?("/lib/")
134
- project_root = expanded.split(%r{/lib/}, 2).first
135
- spec_dir = File.join(project_root, "spec")
136
- return spec_dir if Dir.exist?(spec_dir)
137
- end
138
-
139
- # Walk up from the file's directory looking for spec/
140
- dir = File.dirname(expanded)
141
- loop do
142
- spec_dir = File.join(dir, "spec")
143
- return spec_dir if Dir.exist?(spec_dir)
144
-
145
- parent = File.dirname(dir)
146
- break if parent == dir
147
-
148
- dir = parent
149
- end
150
-
151
- nil
152
- end
153
-
154
- def spec_file_candidates(source_path)
155
- candidates = []
156
-
157
- if source_path.start_with?("lib/")
158
- # lib/foo/bar.rb -> spec/foo/bar_spec.rb
159
- relative = source_path.sub(%r{^lib/}, "")
160
- spec_name = relative.sub(/\.rb$/, "_spec.rb")
161
- candidates << File.join("spec", spec_name)
162
- candidates << File.join("spec", "unit", spec_name)
163
- elsif source_path.include?("/lib/")
164
- # /absolute/path/lib/foo/bar.rb -> /absolute/path/spec/foo/bar_spec.rb
165
- prefix, relative = source_path.split(%r{/lib/}, 2)
166
- spec_name = relative.sub(/\.rb$/, "_spec.rb")
167
- candidates << File.join(prefix, "spec", spec_name)
168
- candidates << File.join(prefix, "spec", "unit", spec_name)
169
- end
170
-
171
- # Same directory: foo/bar.rb -> foo/bar_spec.rb
172
- sibling_spec = source_path.sub(/\.rb$/, "_spec.rb")
173
- candidates << sibling_spec
174
-
175
- # Subdirectory spec/ variant: foo/bar.rb -> foo/spec/bar_spec.rb
176
- dir = File.dirname(source_path)
177
- base = File.basename(source_path, ".rb")
178
- candidates << File.join(dir, "spec", "#{base}_spec.rb")
179
-
180
- candidates.uniq
181
- end
182
115
  end
183
116
  end
184
117
  end
@@ -4,13 +4,10 @@ require_relative "config"
4
4
  require_relative "ast/parser"
5
5
  require_relative "mutator/registry"
6
6
  require_relative "isolation/fork"
7
- require_relative "parallel/pool"
8
7
  require_relative "integration/rspec"
9
8
  require_relative "reporter/json"
10
9
  require_relative "reporter/cli"
11
10
  require_relative "reporter/suggestion"
12
- require_relative "coverage/collector"
13
- require_relative "coverage/test_map"
14
11
  require_relative "diff/parser"
15
12
  require_relative "diff/file_filter"
16
13
  require_relative "result/mutation_result"
@@ -31,12 +28,11 @@ module Evilution
31
28
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
29
 
33
30
  subjects = parse_subjects
31
+ subjects = filter_by_target(subjects) if config.target?
32
+ subjects = filter_by_line_ranges(subjects) if config.line_ranges?
34
33
  subjects = filter_by_diff(subjects) if config.diff?
35
34
  mutations = generate_mutations(subjects)
36
- test_map = collect_coverage if config.coverage && config.integration == :rspec
37
- mutations, skipped = filter_by_coverage(mutations, test_map) if test_map
38
35
  results = run_mutations(mutations)
39
- results.concat(skipped) if skipped
40
36
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
41
37
 
42
38
  summary = Result::Summary.new(results: results, duration: duration)
@@ -53,6 +49,24 @@ module Evilution
53
49
  config.target_files.flat_map { |file| parser.call(file) }
54
50
  end
55
51
 
52
+ def filter_by_target(subjects)
53
+ matched = subjects.select { |s| s.name == config.target }
54
+ raise Error, "no method found matching '#{config.target}'" if matched.empty?
55
+
56
+ matched
57
+ end
58
+
59
+ def filter_by_line_ranges(subjects)
60
+ subjects.select do |subject|
61
+ range = config.line_ranges[subject.file_path]
62
+ next true unless range
63
+
64
+ subject_start = subject.line_number
65
+ subject_end = subject_start + subject.source.count("\n")
66
+ subject_start <= range.last && subject_end >= range.first
67
+ end
68
+ end
69
+
56
70
  def filter_by_diff(subjects)
57
71
  diff_parser = Diff::Parser.new
58
72
  changed_ranges = diff_parser.parse(config.diff_base)
@@ -63,37 +77,9 @@ module Evilution
63
77
  subjects.flat_map { |subject| registry.mutations_for(subject) }
64
78
  end
65
79
 
66
- def collect_coverage
67
- test_files = Dir.glob("spec/**/*_spec.rb")
68
- return nil if test_files.empty?
69
-
70
- data = Coverage::Collector.new.call(test_files: test_files)
71
- Coverage::TestMap.new(data)
72
- end
73
-
74
- def filter_by_coverage(mutations, test_map)
75
- covered, uncovered = mutations.partition do |m|
76
- test_map.covered?(File.expand_path(m.file_path), m.line)
77
- end
78
-
79
- skipped = uncovered.map do |m|
80
- Result::MutationResult.new(mutation: m, status: :survived, duration: 0.0)
81
- end
82
-
83
- [covered, skipped]
84
- end
85
-
86
80
  def run_mutations(mutations)
87
81
  integration = build_integration
88
82
 
89
- if config.jobs > 1 && mutations.size > 1
90
- run_parallel(mutations, integration)
91
- else
92
- run_sequential(mutations, integration)
93
- end
94
- end
95
-
96
- def run_sequential(mutations, integration)
97
83
  mutations.map do |mutation|
98
84
  test_command = ->(m) { integration.call(m) }
99
85
  isolator.call(
@@ -104,16 +90,11 @@ module Evilution
104
90
  end
105
91
  end
106
92
 
107
- def run_parallel(mutations, integration)
108
- pool = Parallel::Pool.new(jobs: config.jobs)
109
- test_command_builder = ->(_mutation) { ->(m) { integration.call(m) } }
110
- pool.call(mutations: mutations, test_command_builder: test_command_builder, timeout: config.timeout)
111
- end
112
-
113
93
  def build_integration
114
94
  case config.integration
115
95
  when :rspec
116
- Integration::RSpec.new
96
+ test_files = config.spec_files.empty? ? nil : config.spec_files
97
+ Integration::RSpec.new(test_files: test_files)
117
98
  else
118
99
  raise Error, "unknown integration: #{config.integration}"
119
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -27,8 +27,6 @@ require_relative "evilution/mutator/operator/return_value_removal"
27
27
  require_relative "evilution/mutator/operator/collection_replacement"
28
28
  require_relative "evilution/mutator/registry"
29
29
  require_relative "evilution/isolation/fork"
30
- require_relative "evilution/parallel/worker"
31
- require_relative "evilution/parallel/pool"
32
30
  require_relative "evilution/diff/parser"
33
31
  require_relative "evilution/diff/file_filter"
34
32
  require_relative "evilution/integration/base"
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-03-09 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: diff-lcs
@@ -89,8 +90,6 @@ files:
89
90
  - lib/evilution/mutator/operator/string_literal.rb
90
91
  - lib/evilution/mutator/operator/symbol_literal.rb
91
92
  - lib/evilution/mutator/registry.rb
92
- - lib/evilution/parallel/pool.rb
93
- - lib/evilution/parallel/worker.rb
94
93
  - lib/evilution/reporter/cli.rb
95
94
  - lib/evilution/reporter/json.rb
96
95
  - lib/evilution/reporter/suggestion.rb
@@ -107,9 +106,11 @@ metadata:
107
106
  allowed_push_host: https://rubygems.org
108
107
  bug_tracker_uri: https://github.com/marinazzio/evilution/issues
109
108
  documentation_uri: https://github.com/marinazzio/evilution/blob/master/README.md
109
+ changelog_uri: https://github.com/marinazzio/evilution/blob/master/CHANGELOG.md
110
110
  homepage_uri: https://github.com/marinazzio/evilution
111
111
  rubygems_mfa_required: 'true'
112
112
  source_code_uri: https://github.com/marinazzio/evilution
113
+ post_install_message:
113
114
  rdoc_options: []
114
115
  require_paths:
115
116
  - lib
@@ -124,7 +125,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
125
  - !ruby/object:Gem::Version
125
126
  version: '0'
126
127
  requirements: []
127
- rubygems_version: 4.0.3
128
+ rubygems_version: 3.5.22
129
+ signing_key:
128
130
  specification_version: 4
129
131
  summary: Free, MIT-licensed mutation testing for Ruby
130
132
  test_files: []
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evilution
4
- module Parallel
5
- class Pool
6
- def initialize(jobs:)
7
- @jobs = jobs
8
- end
9
-
10
- # Executes mutations in parallel across N worker processes.
11
- #
12
- # @param mutations [Array] Array of mutation objects to run
13
- # @param test_command_builder [#call] Callable that receives a mutation and returns a test command callable
14
- # @param timeout [Numeric] Per-mutation timeout in seconds
15
- # @return [Array<Result::MutationResult>]
16
- def call(mutations:, test_command_builder:, timeout:)
17
- return [] if mutations.empty?
18
-
19
- worker_count = [@jobs, mutations.size].min
20
- chunks = partition(mutations, worker_count)
21
-
22
- pipes = worker_count.times.map { IO.pipe }
23
-
24
- pids = chunks.each_with_index.map do |chunk, index|
25
- _, write_io = pipes[index]
26
-
27
- pid = Process.fork do
28
- # Close all read ends in the child; close sibling write ends too
29
- pipes.each_with_index do |(r, w), i|
30
- if i == index
31
- r.close
32
- else
33
- r.close
34
- w.close
35
- end
36
- end
37
-
38
- results = run_chunk(chunk, test_command_builder, timeout)
39
- Marshal.dump(results, write_io)
40
- write_io.close
41
- exit!(0)
42
- end
43
-
44
- write_io.close
45
- pid
46
- end
47
-
48
- results = collect_results(pipes.map(&:first), pids)
49
-
50
- results
51
- ensure
52
- # Ensure all pipes are closed even if something goes wrong
53
- pipes&.each do |read_io, write_io|
54
- read_io.close unless read_io.closed?
55
- write_io.close unless write_io.closed?
56
- end
57
- end
58
-
59
- private
60
-
61
- attr_reader :jobs
62
-
63
- # Distributes mutations across N chunks using round-robin.
64
- # File isolation is handled by Integration::RSpec via temp directories
65
- # and $LOAD_PATH, so same-file mutations can safely run in parallel.
66
- def partition(mutations, n)
67
- chunks = Array.new(n) { [] }
68
- mutations.each_with_index { |m, i| chunks[i % n] << m }
69
- chunks
70
- end
71
-
72
- # Runs a chunk of mutations sequentially inside a worker process.
73
- def run_chunk(mutations, test_command_builder, timeout)
74
- worker = Worker.new
75
- worker.call(mutations: mutations, test_command_builder: test_command_builder, timeout: timeout)
76
- end
77
-
78
- # Reads results from all worker pipes and waits for workers to finish.
79
- def collect_results(read_ios, pids)
80
- results = []
81
-
82
- read_ios.each_with_index do |read_io, _index|
83
- data = read_io.read
84
- read_io.close
85
-
86
- unless data.empty?
87
- chunk_results = Marshal.load(data) # rubocop:disable Security/MarshalLoad
88
- results.concat(chunk_results)
89
- end
90
- end
91
-
92
- pids.each { |pid| Process.wait(pid) rescue nil } # rubocop:disable Style/RescueModifier
93
-
94
- results
95
- end
96
- end
97
- end
98
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evilution
4
- module Parallel
5
- class Worker
6
- def initialize(isolator: Isolation::Fork.new)
7
- @isolator = isolator
8
- end
9
-
10
- # Runs a batch of mutations sequentially using fork isolation.
11
- #
12
- # @param mutations [Array<Mutation>] Mutations to execute
13
- # @param test_command_builder [#call] Receives a mutation, returns a test command callable
14
- # @param timeout [Numeric] Per-mutation timeout in seconds
15
- # @return [Array<Result::MutationResult>]
16
- def call(mutations:, test_command_builder:, timeout:)
17
- mutations.map do |mutation|
18
- test_command = test_command_builder.call(mutation)
19
- @isolator.call(mutation: mutation, test_command: test_command, timeout: timeout)
20
- end
21
- end
22
- end
23
- end
24
- end