evilution 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.beads/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +85 -15
- data/CHANGELOG.md +19 -0
- data/README.md +2 -1
- data/lib/evilution/baseline.rb +9 -1
- data/lib/evilution/cli.rb +11 -0
- data/lib/evilution/integration/rspec.rb +74 -5
- data/lib/evilution/isolation/fork.rb +10 -6
- data/lib/evilution/isolation/in_process.rb +14 -10
- data/lib/evilution/parallel/pool.rb +2 -2
- data/lib/evilution/parallel/work_queue.rb +54 -13
- data/lib/evilution/reporter/json.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +5 -2
- data/lib/evilution/runner.rb +7 -4
- data/lib/evilution/spec_resolver.rb +13 -1
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +22 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4c7494a072237a00c9d7d58ad405b454a702ccbe1583090ec9702a0e9d25f13
|
|
4
|
+
data.tar.gz: 80a563a5cfce99b212a9002064d0163700eb583192bca1aeac8615c75165e67f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04d89d24c4287d5863c9fcab61e38d2dc4a254cd2a54f729147b51390c41904237a639f4042a6563147b5ab5c978dc2cf0295c1bdc71a6a9e35f9a0d80f2139c
|
|
7
|
+
data.tar.gz: 55459bde7f70ec8061f6df67ce582b8fcbe942ce40452add6737d00cb52e92cd5859997b87474666a31e65ffb966600d32fd910300c604bbced4a98d78d1ce0f
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1775460080
|
data/.beads/issues.jsonl
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
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
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
|
-
{"id":"EV-100","title":"Epic: Expand send/method dispatch mutations","description":"Add more method dispatch mutation variants to close the gap with Mutant. Specifically called out in feedback: []/fetch, bang/non-bang, enumerable reductions.","status":"
|
|
9
|
+
{"id":"EV-100","title":"Epic: Expand send/method dispatch mutations","description":"Add more method dispatch mutation variants to close the gap with Mutant. Specifically called out in feedback: []/fetch, bang/non-bang, enumerable reductions.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:17.980288778+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590337935+07:00","closed_at":"2026-04-06T12:49:55.590337935+07:00","close_reason":"GH issue already closed"}
|
|
10
10
|
{"id":"EV-101","title":"Add RSpec suggestion templates for compound assignment mutations","description":"Add concrete RSpec it-block suggestion templates for survived compound assignment mutations to the SuggestionReporter. Follow patterns from existing suggestion templates.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:24.805269164+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T17:07:01.363637634+07:00","closed_at":"2026-03-23T17:07:01.363637634+07:00","close_reason":"PR merged. Added static and concrete RSpec suggestion templates for compound assignment mutations.","dependencies":[{"issue_id":"EV-101","depends_on_id":"EV-86","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
11
11
|
{"id":"EV-102","title":"Implement redo statement mutator","description":"Create a mutator for redo statements. Mutations: remove redo statement entirely. Prism redo_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:25.879559251+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T11:57:54.07790533+07:00","dependencies":[{"issue_id":"EV-102","depends_on_id":"EV-91","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
12
12
|
{"id":"EV-103","title":"Add pop/shift and push/unshift method swap pairs","description":"Add orthogonal method swap pairs: pop↔shift (remove from end vs start), push↔unshift (add to end vs start). Register in the collection method swap operator.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:26.957515204+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T12:07:42.123537403+07:00","dependencies":[{"issue_id":"EV-103","depends_on_id":"EV-96","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
13
13
|
{"id":"EV-104","title":"Implement global variable mutation","description":"Create a mutator for global variable writes ($gvar = val). Mutations: remove assignment, replace value with nil. Prism global_variable_write_node.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:27.098660867+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:16:27.098660867+07:00","dependencies":[{"issue_id":"EV-104","depends_on_id":"EV-87","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
14
|
-
{"id":"EV-105","title":"Epic: Bitwise operator mutations","description":"Add mutation operators for bitwise expressions (&, |, ^, ~). Mutant supports full bitwise swaps; Evilution has none. Important for low-level Ruby code and flag/bitmask operations.","status":"
|
|
14
|
+
{"id":"EV-105","title":"Epic: Bitwise operator mutations","description":"Add mutation operators for bitwise expressions (&, |, ^, ~). Mutant supports full bitwise swaps; Evilution has none. Important for low-level Ruby code and flag/bitmask operations.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:35.702212876+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590340292+07:00","closed_at":"2026-04-06T12:49:55.590340292+07:00","close_reason":"GH issue already closed"}
|
|
15
15
|
{"id":"EV-106","title":"Add each_key/each_value and assoc/rassoc method swap pairs","description":"Add orthogonal method swap pairs: each_key↔each_value (iterate keys vs values), assoc↔rassoc (search by key vs value). Register in the collection method swap operator.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:38.318168048+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T12:52:13.426110699+07:00","dependencies":[{"issue_id":"EV-106","depends_on_id":"EV-96","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
16
16
|
{"id":"EV-107","title":"Add RSpec suggestion templates for control flow mutations","description":"Add concrete RSpec suggestion templates for survived break/next/redo mutations to the SuggestionReporter.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:39.204347976+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T12:02:40.995082091+07:00","dependencies":[{"issue_id":"EV-107","depends_on_id":"EV-91","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
17
17
|
{"id":"EV-108","title":"Add RSpec suggestion templates for variable mutations","description":"Add concrete RSpec suggestion templates for survived variable mutations to the SuggestionReporter.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:42.648616139+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T01:53:09.086144843+07:00","dependencies":[{"issue_id":"EV-108","depends_on_id":"EV-87","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
18
18
|
{"id":"EV-109","title":"Implement bitwise binary operator mutator (&, |, ^)","description":"Create a new mutator that swaps bitwise binary operators (&, |, ^) with each other. Register in Mutator::Registry. These are Prism call_node operations with bitwise method names.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:44.961048186+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T13:57:57.624854414+07:00","dependencies":[{"issue_id":"EV-109","depends_on_id":"EV-105","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
19
19
|
{"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"}
|
|
20
|
-
{"id":"EV-110","title":"Epic: Hooks system for mutation lifecycle","description":"Implement a hooks system allowing users to register custom Ruby code at critical execution points. Mutant provides 8 hook points (env_infection_pre/post, setup_integration_pre/post, mutation_insert_pre/post, mutation_worker_process_start, test_worker_process_start). Essential for Rails database isolation in parallel workers, custom instrumentation, and specialized test environment setup.","status":"
|
|
20
|
+
{"id":"EV-110","title":"Epic: Hooks system for mutation lifecycle","description":"Implement a hooks system allowing users to register custom Ruby code at critical execution points. Mutant provides 8 hook points (env_infection_pre/post, setup_integration_pre/post, mutation_insert_pre/post, mutation_worker_process_start, test_worker_process_start). Essential for Rails database isolation in parallel workers, custom instrumentation, and specialized test environment setup.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:46.117603755+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590342574+07:00","closed_at":"2026-04-06T12:49:55.590342574+07:00","close_reason":"GH issue already closed"}
|
|
21
21
|
{"id":"EV-111","title":"Add grep/grep_v and take/drop method swap pairs","description":"Add orthogonal method swap pairs: grep↔grep_v (match vs non-match), take↔drop (keep first N vs remove first N). Register in the collection method swap operator.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:47.432188126+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T12:57:53.868853745+07:00","dependencies":[{"issue_id":"EV-111","depends_on_id":"EV-96","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
22
22
|
{"id":"EV-112","title":"Epic: Super call mutations","description":"Add mutations for super and zsuper (implicit super) calls. Mutant distinguishes zsuper (forwards all args) from explicit super (specific args) and mutates between them. Evilution has none.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:51.249178891+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:16:51.249178891+07:00"}
|
|
23
23
|
{"id":"EV-113","title":"Implement rescue clause removal mutator","description":"Create a mutator that removes individual rescue clauses from begin/rescue blocks. When multiple rescue clauses exist, remove each one individually. Prism rescue_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:53.280678046+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T01:58:05.000652348+07:00","dependencies":[{"issue_id":"EV-113","depends_on_id":"EV-89","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
{"id":"EV-196","title":"Show code diffs for survived mutations in text output","description":"Show the exact code diff (original vs mutated) for each survived mutation in the text reporter output, similar to how Mutant displays survived mutants. Currently Evilution shows the mutation description but not the visual diff. This was specifically requested in comparison testing feedback as it helps users quickly understand what test is missing.","status":"in_progress","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:02.574157928+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-03T11:00:52.543373742+07:00"}
|
|
115
115
|
{"id":"EV-197","title":"Implement module include/extend removal","description":"Mutate include/extend/prepend statements: remove each one individually. Tests whether the mixin is actually used.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:13.978482592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:57:39.685072781+07:00","dependencies":[{"issue_id":"EV-197","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
116
116
|
{"id":"EV-198","title":"Add RSpec suggestion templates for class/module mutations","description":"Add concrete suggestion templates for survived class/module mutations.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:23.284834592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:47:56.899533924+07:00","dependencies":[{"issue_id":"EV-198","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
117
|
-
{"id":"EV-199","title":"Implement keyword argument mutations","description":"Add mutations for keyword arguments and optional keyword argument defaults. Mutations: remove keyword arg default value, swap keyword arg with positional, remove optional keyword. Prism keyword_hash_node, keyword_rest_parameter_node.","status":"
|
|
117
|
+
{"id":"EV-199","title":"Implement keyword argument mutations","description":"Add mutations for keyword arguments and optional keyword argument defaults. Mutations: remove keyword arg default value, swap keyword arg with positional, remove optional keyword. Prism keyword_hash_node, keyword_rest_parameter_node.","notes":"GitHub: #492","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:33.74262984+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:13.891912984+07:00"}
|
|
118
118
|
{"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"}
|
|
119
119
|
{"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"}]}
|
|
120
120
|
{"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"}]}
|
|
@@ -130,9 +130,9 @@
|
|
|
130
130
|
{"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"}]}
|
|
131
131
|
{"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"}]}
|
|
132
132
|
{"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"}
|
|
133
|
-
{"id":"EV-200","title":"Implement multiple assignment (masgn) mutations","description":"Add mutations for destructuring/parallel assignment (a, b = 1, 2). Mutations: remove individual assignment targets, swap assignment order, reduce to single assignment. Prism multi_write_node.","status":"
|
|
134
|
-
{"id":"EV-201","title":"Implement yield statement mutations","description":"Add mutations for yield statements. Mutations: remove yield, remove yield arguments, replace yield value with nil. Prism yield_node.","status":"
|
|
135
|
-
{"id":"EV-202","title":"Implement splat operator mutations","description":"Add mutations for splat (*) and double-splat (**) operators. Mutations: remove splat (pass array directly), remove double-splat (pass hash directly). Prism splat_node, assoc_splat_node.","status":"
|
|
133
|
+
{"id":"EV-200","title":"Implement multiple assignment (masgn) mutations","description":"Add mutations for destructuring/parallel assignment (a, b = 1, 2). Mutations: remove individual assignment targets, swap assignment order, reduce to single assignment. Prism multi_write_node.","notes":"GitHub: #493","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:44.259585319+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:14.085581135+07:00"}
|
|
134
|
+
{"id":"EV-201","title":"Implement yield statement mutations","description":"Add mutations for yield statements. Mutations: remove yield, remove yield arguments, replace yield value with nil. Prism yield_node.","notes":"GitHub: #494","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:53.437749245+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:14.275449093+07:00"}
|
|
135
|
+
{"id":"EV-202","title":"Implement splat operator mutations","description":"Add mutations for splat (*) and double-splat (**) operators. Mutations: remove splat (pass array directly), remove double-splat (pass hash directly). Prism splat_node, assoc_splat_node.","notes":"GitHub: #491","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:01.958187896+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:13.694553415+07:00"}
|
|
136
136
|
{"id":"EV-203","title":"Epic: Extended equivalent mutation detection","description":"Extend equivalent mutation detection with more heuristics and manual marking support. Evilution's equivalent detection is already praised in feedback; extending it further strengthens a competitive advantage.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:11.400012571+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:11.400012571+07:00"}
|
|
137
137
|
{"id":"EV-204","title":"Add more automatic equivalence heuristics","description":"Add new equivalence detection strategies: frozen string mutations (frozen strings are immutable, mutation is equivalent), private method rename (if only called internally with same signature), constant folding equivalences.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:20.07787144+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T21:21:19.35465527+07:00","dependencies":[{"issue_id":"EV-204","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
138
138
|
{"id":"EV-205","title":"Support # evilution:equivalent manual marking","description":"Allow users to mark specific mutations as equivalent using source comments: # evilution:equivalent. These should be excluded from the score denominator like auto-detected equivalents.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:31.543822152+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T22:08:20.967272294+07:00","dependencies":[{"issue_id":"EV-205","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -141,16 +141,86 @@
|
|
|
141
141
|
{"id":"EV-208","title":"Add cross-run diff to MCP server","description":"Add MCP tool for comparing two sessions and returning the diff. Enables AI agents to detect regressions.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:00.558002099+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T10:58:31.301603302+07:00","dependencies":[{"issue_id":"EV-208","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
142
142
|
{"id":"EV-209","title":"Add streaming test suggestions via MCP","description":"Stream test suggestions for survived mutations via MCP as they are generated, rather than waiting for the full run to complete.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:10.316219498+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T11:10:26.677490317+07:00","dependencies":[{"issue_id":"EV-209","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
143
143
|
{"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"}]}
|
|
144
|
-
{"id":"EV-210","title":"Implement defined? check mutations","description":"Add mutation for defined?(expr) → true. Tests whether the defined? check is actually needed. Prism defined_node.","status":"
|
|
145
|
-
{"id":"EV-211","title":"Implement regex capture reference (, ) mutations","description":"Add mutations for numbered regex capture references ($1, $2, etc.). Mutations: swap capture numbers ($1↔$2), replace with nil. Prism numbered_reference_read_node.","status":"
|
|
146
|
-
{"id":"EV-212","title":"Implement self reference mutations","description":"Add mutations for self references. Mutations: remove self where it's used as an explicit receiver (self.foo → foo). Prism self_node.","status":"
|
|
147
|
-
{"id":"EV-213","title":"Critical: SourceSurgeon crashes on multi-byte UTF-8 source files","description":"SourceSurgeon.apply uses String#[]= with Prism byte offsets, but Ruby String#[]= interprets indices as character offsets for UTF-8 strings. This causes IndexError on files containing multi-byte characters (e.g. Cyrillic, CJK, emoji). Fix: use source.b to force ASCII-8BIT before byte-offset operations, or use byteslice-based replacement. Reported by end user on 2026-03-29 against v0.16.0.","status":"
|
|
144
|
+
{"id":"EV-210","title":"Implement defined? check mutations","description":"Add mutation for defined?(expr) → true. Tests whether the defined? check is actually needed. Prism defined_node.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:22.128405637+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:23:22.128405637+07:00"}
|
|
145
|
+
{"id":"EV-211","title":"Implement regex capture reference (, ) mutations","description":"Add mutations for numbered regex capture references ($1, $2, etc.). Mutations: swap capture numbers ($1↔$2), replace with nil. Prism numbered_reference_read_node.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:30.659728763+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:23:30.659728763+07:00"}
|
|
146
|
+
{"id":"EV-212","title":"Implement self reference mutations","description":"Add mutations for self references. Mutations: remove self where it's used as an explicit receiver (self.foo → foo). Prism self_node.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:42.173027795+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:23:42.173027795+07:00"}
|
|
147
|
+
{"id":"EV-213","title":"Critical: SourceSurgeon crashes on multi-byte UTF-8 source files","description":"SourceSurgeon.apply uses String#[]= with Prism byte offsets, but Ruby String#[]= interprets indices as character offsets for UTF-8 strings. This causes IndexError on files containing multi-byte characters (e.g. Cyrillic, CJK, emoji). Fix: use source.b to force ASCII-8BIT before byte-offset operations, or use byteslice-based replacement. Reported by end user on 2026-03-29 against v0.16.0.","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-29T23:19:47.803570297+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:14:32.115438439+07:00","closed_at":"2026-04-04T11:14:32.115438439+07:00","close_reason":"Fixed in commit e973a13, released in v0.17.0. SourceSurgeon now uses binary encoding for byte-offset operations."}
|
|
148
|
+
{"id":"EV-214","title":"While/until flip mutation","description":"Add mutation that swaps while cond to until cond and vice versa. Prism: while_node, until_node. Small operator.","notes":"GitHub: #495","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:15.904741396+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:48.098621754+07:00","dependencies":[{"issue_id":"EV-214","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
149
|
+
{"id":"EV-215","title":"defined? -> true mutation","description":"Add mutation that replaces defined?(expr) with true. Prism: defined_node. Small operator.","notes":"GitHub: #496","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:21.037349751+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:48.438019342+07:00","dependencies":[{"issue_id":"EV-215","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
150
|
+
{"id":"EV-216","title":"String interpolation content mutation","description":"Add mutations for string interpolation content. Replace #{expr} with #{nil} or empty string. Prism: interpolated_string_node, embedded_statements_node. Medium operator.","notes":"GitHub: #497","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:22.095986111+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:48.90150684+07:00","dependencies":[{"issue_id":"EV-216","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
151
|
+
{"id":"EV-217","title":"== -> .equal? identity mutation","description":"Add mutation that replaces a == b with a.equal?(b) (object identity check). Prism: call_node with method ==. Small operator.","notes":"GitHub: #501","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:22.723044425+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:01.958573728+07:00","dependencies":[{"issue_id":"EV-217","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
152
|
+
{"id":"EV-218","title":"case/when branch mutations","description":"Add mutations for case/when statements. Remove individual when-branches, replace when body with nil, remove else branch. Prism: case_node, when_node. Medium operator.","notes":"GitHub: #498","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:22.912808107+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:49.955500409+07:00","dependencies":[{"issue_id":"EV-218","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
153
|
+
{"id":"EV-219","title":"retry removal mutation","description":"Add mutation that removes retry statements in rescue blocks (replace with nil). Prism: retry_node. Small operator.","notes":"GitHub: #499","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:23.739947781+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:50.342965697+07:00","dependencies":[{"issue_id":"EV-219","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
148
154
|
{"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"}
|
|
155
|
+
{"id":"EV-220","title":"Predicate method -> true/false mutation","description":"Add mutation that replaces predicate method calls (methods ending with ?) with true and false literals. Existing NegationInsertion inserts ! before predicates but does not replace with literal boolean. Prism: call_node where method name ends with ?. Small operator.","notes":"GitHub: #500","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:25.02387319+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:11:50.77861531+07:00","dependencies":[{"issue_id":"EV-220","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
156
|
+
{"id":"EV-221","title":"Method body -> self/super replacement","description":"Extend MethodBodyReplacement to also replace method body with self and super (currently only replaces with nil). Mutant does all three: nil, self, super. Small enhancement to existing operator.","notes":"GitHub: #502","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:36.202930745+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:02.230820607+07:00","dependencies":[{"issue_id":"EV-221","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
157
|
+
{"id":"EV-222","title":"Proc/lambda body mutation","description":"Add mutation that replaces proc/lambda body with nil. ->() { expr } becomes ->() { nil }. Not covered by BlockRemoval which handles do..end/{ } blocks on method calls. Prism: lambda_node. Small operator.","notes":"GitHub: #503","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:38.002122541+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:02.561937274+07:00","dependencies":[{"issue_id":"EV-222","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
158
|
+
{"id":"EV-223","title":"begin..end unwrap mutation","description":"Add mutation that removes begin/end wrapper, leaving just the body statements. begin; expr; end becomes expr. Prism: begin_node. Small operator.","notes":"GitHub: #504","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:38.269119824+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:03.442158099+07:00","dependencies":[{"issue_id":"EV-223","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
159
|
+
{"id":"EV-224","title":"Block argument (&block) removal mutation","description":"Add mutation that removes explicit block parameter from method definitions. def foo(&block) becomes def foo. Prism: block_parameter_node. Small operator.","notes":"GitHub: #505","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:11:37.302228714+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:12:03.723910985+07:00","dependencies":[{"issue_id":"EV-224","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
160
|
+
{"id":"EV-225","title":"Bug: Version field in JSON output not updated after version bump","description":"Evilution JSON output (both CLI --format json and MCP tool response) reports version as 0.17.0 when evilution --version says 0.18.0. The MCP server correctly references Evilution::VERSION constant, so the issue is likely that VERSION constant was not bumped to 0.18.0 in lib/evilution/version.rb. Reported across 5 consecutive feedback sessions on 2026-04-04.","notes":"GitHub: #506","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:20:30.428077285+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T13:15:11.40532407+07:00","closed_at":"2026-04-04T13:15:11.40532407+07:00","close_reason":"Not a bug. MCP server is a long-running process — bundle update changes files on disk but the loaded Ruby process retains the old VERSION constant. Restart the MCP server after gem updates. Generic MCP behavior, not evilution-specific."}
|
|
161
|
+
{"id":"EV-226","title":"Bug: Memory leak: RSS grows linearly per mutation without releasing","description":"During mutation runs, child process RSS grows ~3-8 MB per mutation without releasing. Observed on v0.18.0: RSS went from 190 MB to 730 MB across 159 mutations on a 46-line file (~3.4 MB/mutation average delta). Previous memory fixes (AST node release, StringIO drain) helped but did not fully resolve. The leak appears to be in the fork/child process pool — memory accumulates across mutations within the same worker process. This should be investigated with rake memory:check and profiling tools.","notes":"GitHub: #507","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:20:32.021072642+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:25:48.383886909+07:00","closed_at":"2026-04-05T23:25:48.383886909+07:00","close_reason":"Core leak fixed (EV-256), regression test added (EV-257). Residual ~128 KB/mutation from MRI Class#subclasses tracking — not fixable without Ruby changes. Follow-ups: EV-274, EV-275, EV-276."}
|
|
162
|
+
{"id":"EV-227","title":"Smart spec auto-detection (narrow to matching spec file)","description":"Evilution's spec auto-detection selects too broad a test suite per mutation, causing mass timeouts and excessive memory usage. When --spec override is used to point at the correct spec file, timeouts disappear, speed improves 4-7x, and memory drops 6x (282 MB vs 1709 MB). The auto-detection should narrow to the closest matching spec file based on file path conventions (e.g., app/controllers/foo_controller.rb -> spec/requests/foo_spec.rb or spec/controllers/foo_controller_spec.rb). Reported in 7 separate feedback sessions from v0.12.0 through v0.17.0. This is the single biggest pain point in real-world usage. Evidence: HelpController 9-LOC file had 3/18 timeouts without --spec, 0/15 with --spec. GamesController had 25/246 timeouts (10.2%). HomeController: 110s without -> 29s with --spec, 1709 MB -> 282 MB.","status":"closed","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:16.691637724+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-07T09:55:50.887685731+07:00","closed_at":"2026-04-07T09:55:50.887685731+07:00","close_reason":"All subtasks complete: controller→request spec mapping, model/service/lib/Avo conventions, fallback chain with warnings, integration tests with fixture layouts. SpecResolver handles all Rails and gem conventions with parent directory fallback and full-suite fallback with user warnings.","comments":[{"id":1,"issue_id":"EV-227","author":"Denis Kiselev","text":"GitHub issue: #508 (https://github.com/marinazzio/evilution/issues/508)","created_at":"2026-04-04T11:21:46.103310023+07:00"}]}
|
|
163
|
+
{"id":"EV-227.1","title":"Audit current SpecResolver logic and document behavior","description":"Map the current spec resolution code path. Document what SpecResolver does today, what inputs it takes, what it returns. Identify where the 'run everything' fallback is triggered. This is the prerequisite investigation for smart spec auto-detection.","notes":"GitHub: #524","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:02.423705736+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T20:54:33.665002626+07:00","closed_at":"2026-04-06T20:54:33.665002626+07:00","close_reason":"Audit posted to GH #524 comment, references added to all sibling issues","dependencies":[{"issue_id":"EV-227.1","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
164
|
+
{"id":"EV-227.2","title":"Implement Rails controller to request spec convention mapping","description":"Map app/controllers/foo_controller.rb to spec/requests/foo_spec.rb or spec/controllers/foo_controller_spec.rb. Try both paths, prefer the one that exists. This is the highest-value convention since controller mutations cause the most timeouts.","notes":"GitHub: #530","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:27.573090402+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-07T09:36:22.413636229+07:00","closed_at":"2026-04-07T09:36:22.413636229+07:00","close_reason":"Already implemented: controller_to_request_spec method in SpecResolver maps app/controllers/ to spec/requests/, with fallback to spec/controllers/. 11 passing tests (8 unit + 3 integration). Merged via GH #530.","dependencies":[{"issue_id":"EV-227.2","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
165
|
+
{"id":"EV-227.3","title":"Implement model to model spec convention mapping","description":"Map app/models/foo.rb to spec/models/foo_spec.rb. Straightforward 1:1 path convention.","notes":"GitHub: #536","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:47.33711682+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T23:20:13.067596622+07:00","closed_at":"2026-04-06T23:20:13.067596622+07:00","close_reason":"Already handled by existing prefix-stripping logic (app/ → spec/). Covered by tests at spec_resolver_spec.rb:56 and :132.","dependencies":[{"issue_id":"EV-227.3","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
166
|
+
{"id":"EV-227.4","title":"Implement service/lib to spec convention mapping","description":"Map app/services/foo.rb to spec/services/foo_spec.rb. Map lib/foo.rb to spec/lib/foo_spec.rb. Covers the two most common non-controller, non-model source locations.","notes":"GitHub: #538","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:30:55.948920802+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T23:27:29.537297162+07:00","closed_at":"2026-04-06T23:27:29.537297162+07:00","close_reason":"Already handled by existing prefix-stripping logic. app/services/ covered at spec_resolver_spec.rb:68, lib/ covered at lines 23-52.","dependencies":[{"issue_id":"EV-227.4","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
167
|
+
{"id":"EV-227.5","title":"Implement Avo resource to spec convention mapping","description":"Map app/avo/resources/foo.rb to spec/avo/resources/foo_spec.rb. Mutant gets this wrong; evilution should get it right. Avo resources follow a predictable path convention.","notes":"GitHub: #553","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:31:11.220551992+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T23:36:54.160582811+07:00","closed_at":"2026-04-06T23:36:54.160582811+07:00","close_reason":"Already handled by existing app/ prefix-stripping logic. Verified for avo/resources/ and avo/actions/.","dependencies":[{"issue_id":"EV-227.5","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
168
|
+
{"id":"EV-227.6","title":"Implement fallback chain when primary convention doesn't match","description":"If the convention-mapped spec file doesn't exist, try parent directory patterns, then fall back to full suite. Log a warning when falling back so users know auto-detection couldn't narrow the spec.","notes":"GitHub: #555","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:31:33.254382872+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-07T09:54:52.004819913+07:00","closed_at":"2026-04-07T09:54:52.004819913+07:00","close_reason":"Fully implemented: parent_fallback_candidates in SpecResolver walks up directory tree. Warnings logged in both baseline.rb and integration/rspec.rb when falling back to full suite. Covered by unit, integration, baseline, and rspec integration tests. Merged via GH #555.","dependencies":[{"issue_id":"EV-227.6","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
169
|
+
{"id":"EV-227.7","title":"Add integration tests for spec auto-detection with fixture project layouts","description":"Test Rails conventions (controller, model, service, Avo), non-Rails lib layout, and fallback behavior using fixture directories under spec/support/fixtures/. Cover all convention mappings and the fallback chain.","notes":"GitHub: #556","status":"closed","priority":1,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:31:58.393834666+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-07T09:39:42.378355211+07:00","closed_at":"2026-04-07T09:39:42.378355211+07:00","close_reason":"Integration tests added for all spec auto-detection conventions (Rails controller/model/service/Avo, gem lib/, fallback chain) using fixture project layouts. 15 tests passing. Merged via GH #556.","dependencies":[{"issue_id":"EV-227.7","depends_on_id":"EV-227","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
170
|
+
{"id":"EV-228","title":"Equivalent detection: .count → .length as always-equivalent","description":".count → .length mutations are universally unkillable (both return identical integer results). Evilution should detect this pattern and classify as equivalent automatically. Reported in feedback for HomeController stats block.","notes":"GH #509","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:43.587875972+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:55.878850271+07:00"}
|
|
171
|
+
{"id":"EV-229","title":"Equivalent detection: .each → .map in void context","description":"When .each is called in void context (return value not assigned or passed), replacing with .map or .reverse_each produces equivalent behavior. Evilution should detect void-context method calls and mark these swaps as likely-equivalent. Reported for Avo reset_password action.","notes":"GH #511","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:46.8330208+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:56.769310284+07:00"}
|
|
149
172
|
{"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"}]}
|
|
173
|
+
{"id":"EV-230","title":"Regex simplification operators","description":"Add mutation operators for regex patterns: /\\s+/ → /\\s/ (remove quantifier), remove anchors (^, $, \\A, \\z), simplify character classes. Mutant's regex mutations caught real test gaps (case sensitivity, whitespace handling) that evilution missed. Reported in 2 sessions on Telegram::NewsScorer.","notes":"GH #514","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:48.930372762+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:57.239710634+07:00","dependencies":[{"issue_id":"EV-230","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
174
|
+
{"id":"EV-231","title":".downcase removal operator","description":"Add mutation that removes .downcase calls. This caught real case-sensitivity test gaps in mutant that evilution missed. Useful for testing that code handles mixed-case input. Reported in 2 sessions on NewsScorer.","notes":"GH #516","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:50.682791593+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:57.718594841+07:00","dependencies":[{"issue_id":"EV-231","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
175
|
+
{"id":"EV-232","title":"Method chain permutation operators (strip → lstrip/rstrip)","description":"Add mutations that replace string cleaning methods with partial variants: strip → lstrip/rstrip, chomp → chop, etc. Mutant's chain permutations caught a real test gap in NewsScorer keyword processing. Reported in 1 session.","notes":"GH #518","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:52.982750285+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:58.679882177+07:00","dependencies":[{"issue_id":"EV-232","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
176
|
+
{"id":"EV-233","title":"Related specs heuristic (run association specs when .includes() mutated)","description":"When mutations remove .includes() eager loading calls, the matching unit spec may not catch N+1 regressions. Consider a heuristic that also runs specs for the included associations or integration specs. Would complement the spec auto-detection feature. Reported in 1 session for NewsController.","notes":"GH #519","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:56.302076763+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:59.026458465+07:00"}
|
|
177
|
+
{"id":"EV-234","title":"Conceptual deduplication of survived mutations","description":"Multiple operators can flag the same underlying coverage gap (e.g., .includes removal via method_call_removal and symbol_literal both reveal missing eager-load test). Add a post-processing pass that groups conceptually similar survivors and presents them as a single coverage gap with multiple mutation evidence. Reduces noise in reports.","notes":"GitHub: #510","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:21:58.378005036+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:53.787757503+07:00"}
|
|
178
|
+
{"id":"EV-235","title":"Bug: Non-deterministic mutation count on same file","description":"Running evilution twice on the same file (HelpController, 9 LOC) produced different mutation counts (18 vs 15). Unclear cause — possibly non-deterministic operator selection or file state difference. Low priority but should be investigated. Reported once in v0.12.0.","notes":"GitHub: #512","status":"closed","priority":4,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:01.930671333+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T13:20:37.135440498+07:00","closed_at":"2026-04-04T13:20:37.135440498+07:00","close_reason":"Not a bug. The 18 vs 15 difference was exactly the 3 timed-out mutations — counted in run 1 total but excluded in run 2 summary. Mutation generation is fully deterministic (no rand/shuffle/sample in codepath). Reported once in v0.12.0, never reproduced. Reporting has been significantly improved since then."}
|
|
179
|
+
{"id":"EV-236","title":"--spec-dir flag for directory-level spec inclusion","description":"Add a --spec-dir flag that auto-includes all specs in a directory, reducing the chance of missing coverage from adjacent spec files. Useful when a controller has tests split across spec/requests/, spec/controllers/, and spec/features/. Reported once.","notes":"GitHub: #513","status":"closed","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:04.618160285+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-07T09:55:50.99913701+07:00","closed_at":"2026-04-07T09:55:50.99913701+07:00","close_reason":"--spec-dir CLI flag implemented, composes with --spec, validates directory existence. 3 unit tests passing. Merged via GH #513.","dependencies":[{"issue_id":"EV-236","depends_on_id":"EV-227","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
180
|
+
{"id":"EV-237","title":"Temp-file based mutation (don't modify original source)","description":"Evilution mutates source files in-place on the filesystem, which triggers file watchers, linters, and IDE notifications during runs. Even with the ensure-based restore (fixed earlier), race conditions exist if the process is killed. Write mutated source to a tempfile and point the test runner at it via load path manipulation. Never modify the original source file. Reported in 2 sessions (v0.16.1).","notes":"GH issue: #520 (https://github.com/marinazzio/evilution/issues/520)","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:06.770551806+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:23:18.529285207+07:00","external_ref":"gh-520","labels":["reliability"]}
|
|
181
|
+
{"id":"EV-238","title":"Epic: Research mutation density gap with mutant","description":"Evilution consistently generates 1.8-2.6x fewer mutations than mutant across 25 feedback sessions. While some of mutant's extras are equivalent ([]→fetch, to_i→Integer()), many catch real edge cases. This epic covers: (1) Audit mutant's operator list systematically against evilution's 54 operators, (2) Identify which missing operators catch real bugs vs produce noise, (3) Prioritize operator additions by signal-to-noise ratio, (4) Target closing the gap to <1.5x. Related: existing gap analysis created 15 new operator issues (EV-214 through EV-224, #491-#505).","notes":"GitHub: #515","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:11.095318733+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:22:55.790159971+07:00"}
|
|
182
|
+
{"id":"EV-239","title":"Epic: Research and fix high memory baseline","description":"Evilution's memory baseline is 718+ MB even for tiny files in v0.18.0 sessions, and grows across consecutive runs in the same session (718→763→795→800 MB). Previous fixes addressed AST node retention and StringIO leaks but baseline remains high. Mutant peaks at ~200 MB for comparable workloads. This epic covers: (1) Profile memory allocation during boot/setup phase, (2) Identify what's consuming the 718 MB baseline, (3) Investigate cross-run memory growth (session-level leak?), (4) Target bringing baseline under 300 MB. Related: existing rake memory:check infrastructure exists.","notes":"GitHub: #517","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:16.602817355+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T00:16:38.665740136+07:00","closed_at":"2026-04-06T00:16:38.665740136+07:00","close_reason":"Premise invalid: 718 MB baseline was the MCP host process, not evilution. Standalone evilution baseline is ~30 MB (confirmed via EV-242/245/246 profiling). No memory fix needed. Sub-issues resolved: EV-242 (30 MB boot baseline), EV-243 (single mutation profiled), EV-245 (no cross-run growth), EV-246 (fork has zero parent-side cost), EV-247 (RSS tracking added)."}
|
|
150
183
|
{"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"}]}
|
|
184
|
+
{"id":"EV-240","title":"Neutral mutation category (test errors vs test failures)","description":"Add a 'neutral' classification for mutations where tests error (crash/exception) rather than fail (assertion). This helps users distinguish test infrastructure problems from real coverage gaps. Mutant has this distinction and it's valuable — e.g., killfork 403 errors in vanilla-mafia are infrastructure noise, not coverage gaps. Currently evilution has neutral detection implemented but feedback suggests it doesn't always classify correctly. Reported in 3 sessions.","notes":"GH issue: #521 (https://github.com/marinazzio/evilution/issues/521)","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:18.548454128+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:23:19.479497894+07:00","external_ref":"gh-521","labels":["reliability"]}
|
|
185
|
+
{"id":"EV-241","title":"Heredoc-aware string literal mutations","description":"Evilution's string_literal operator generates many false survived mutants on heredoc templates by mutating whitespace and literal text around interpolations. MigrationGenerator scored 63.1% due to template whitespace mutations that don't affect generated code behavior. Should either skip literal text in heredocs, only mutate interpolated expressions, or add a --skip-heredoc-literals flag. Reported in 1 session but caused significant score distortion.","notes":"GH issue: #522 (https://github.com/marinazzio/evilution/issues/522)","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:22:20.534650035+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:23:19.83846784+07:00","external_ref":"gh-522","labels":["reliability","false-positives"]}
|
|
186
|
+
{"id":"EV-242","title":"Profile memory allocation during boot/setup phase","description":"Use memory_profiler gem or ObjectSpace to identify what objects are allocated during Evilution boot before any mutations run. Identify the top 10 memory consumers. This will help understand where the 718 MB baseline comes from.","notes":"GitHub: #527","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:28:39.583808805+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:36:18.3122267+07:00","closed_at":"2026-04-05T23:36:18.3122267+07:00","close_reason":"Boot footprint is ~30 MB. The 718 MB baseline was MCP host process, not evilution.","dependencies":[{"issue_id":"EV-242","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
187
|
+
{"id":"EV-243","title":"Profile memory allocation during single mutation cycle","description":"Measure memory before and after a single mutation+test cycle. Identify what is allocated and not released within one cycle. Compare with mutant's behavior for the same file.","notes":"GitHub: #528","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:28:46.085727121+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:37:08.936438689+07:00","dependencies":[{"issue_id":"EV-243","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
188
|
+
{"id":"EV-244","title":"Run head-to-head comparison on 10 diverse files, catalog every mutation mutant generates that evilution doesn't","description":"Pick 10 files of varying complexity (controller, model, service, validator, lib, migration, Avo resource, helper, concern, formatter). Run both mutant and evilution on each file. Catalog every mutation mutant produces that evilution misses. Classify each as: real signal, likely equivalent, or noise.","notes":"GitHub: #523","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:28:52.935963449+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T15:36:16.088018831+07:00","dependencies":[{"issue_id":"EV-244","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
189
|
+
{"id":"EV-245","title":"Investigate cross-run session memory growth","description":"Run multiple evilution invocations via MCP in the same session. Measure RSS between invocations. Determine if the growth (718->763->795->800 MB) is in the MCP server process, worker pool, or accumulated state.","notes":"GitHub: #529","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:28:54.234490102+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:40:31.266000331+07:00","dependencies":[{"issue_id":"EV-245","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
190
|
+
{"id":"EV-246","title":"Investigate parent vs child process memory split","description":"Measure RSS of the parent (coordinator) process separately from forked child (worker) processes. Determine where the 718 MB baseline lives — is it the parent before forking, or do children inherit and grow independently?","notes":"GitHub: #531","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:28:59.300347033+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:45:11.030185257+07:00","dependencies":[{"issue_id":"EV-246","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
191
|
+
{"id":"EV-247","title":"Add RSS tracking per mutation to JSON output","description":"Include parent_rss_kb and child_rss_kb fields in each mutation result. child_rss_kb partially exists (seen in feedback log) — verify it is accurate and add parent_rss_kb tracking. This provides ongoing observability for memory usage.","notes":"GitHub: #532","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:08.569266807+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:49:30.25391422+07:00","dependencies":[{"issue_id":"EV-247","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
192
|
+
{"id":"EV-248","title":"Implement memory budget CI gate","description":"Add a CI check that runs rake memory:check and fails if peak RSS exceeds a threshold (e.g., 400 MB for a reference fixture). This prevents memory regressions from being merged.","notes":"GitHub: #533","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:16.177682161+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T11:45:18.765568988+07:00","close_reason":"Growth-based leak detection in CI (EV-274/PR #571) is sufficient. Absolute peak RSS budget not needed — standalone baseline is ~30 MB (EV-239 premise was invalid). Per-mutation growth check catches regressions effectively.","dependencies":[{"issue_id":"EV-248","depends_on_id":"EV-239","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
193
|
+
{"id":"EV-249","title":"Audit current SourceSurgeon mutation-and-restore flow","description":"Document the current code path: where the file is read, mutated, written, and restored. Identify all callers and the ensure-based restore mechanism. Map the failure modes (SIGKILL, OOM, etc.).","notes":"GitHub: #525","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:17.689070042+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:29:32.825847413+07:00","dependencies":[{"issue_id":"EV-249","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
151
194
|
{"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"}
|
|
195
|
+
{"id":"EV-250","title":"Classify mutant's extra mutations by operator category","description":"From the head-to-head data (EV-244), group mutant's extra mutations by category (e.g., receiver mutations, argument permutations, method name substitutions, literal boundary values). Count how many are signal vs noise per category. Produce a table of categories with signal/noise breakdown.","notes":"GitHub: #526","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:21.320269648+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:29:32.613448913+07:00","dependencies":[{"issue_id":"EV-250","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
196
|
+
{"id":"EV-251","title":"Prioritize operator additions by signal-to-noise ratio","description":"Rank the missing operator categories by: (a) frequency of real signal catches, (b) implementation complexity, (c) expected equivalent mutant rate. Produce a prioritized implementation order for new operators.","notes":"GitHub: #534","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:38.370181376+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:29:53.283203728+07:00","dependencies":[{"issue_id":"EV-251","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
197
|
+
{"id":"EV-252","title":"Reproduce and measure per-mutation RSS growth on reference fixture","description":"Create a reproducible benchmark: run evilution on a known fixture file, record RSS after each mutation. Confirm the ~3-8 MB/mutation growth rate. Baseline for measuring fix effectiveness.","notes":"GitHub: #539","status":"in_progress","priority":0,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:39.421709567+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T15:08:27.260195719+07:00","dependencies":[{"issue_id":"EV-252","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
198
|
+
{"id":"EV-253","title":"Profile object allocation delta per mutation cycle","description":"Use ObjectSpace.count_objects or memory_profiler to capture what new objects are allocated during one mutation cycle and not released. Identify the top retained object types.","notes":"GitHub: #540 — Profiling complete. Root cause: RSpec ExampleGroup subclass ivars create reference cycles preventing GC (+3380 slots/mutation). Secondary: World#@sources_by_path cache. Fix proven: clearing EG ivars + sources cache after Runner.run = 0 growth.","status":"closed","priority":0,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:41.259785614+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T15:50:12.517441649+07:00","closed_at":"2026-04-05T15:50:12.517441649+07:00","close_reason":"Profiling complete. Root cause identified: RSpec ExampleGroup reference cycles + World source cache. Findings documented on GH #540.","dependencies":[{"issue_id":"EV-253","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
199
|
+
{"id":"EV-254","title":"Design temp-file mutation architecture","description":"Design how the temp file is created, where it lives (tmpdir vs .evilution/tmp), how the test process is redirected to load it (Ruby $LOAD_PATH manipulation vs file-level bootsnap override vs ENV-based), and how Rails autoloader interacts with it.","notes":"GitHub: #536","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:42.91131604+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:29:55.18898649+07:00","dependencies":[{"issue_id":"EV-254","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
200
|
+
{"id":"EV-255","title":"Investigate worker process reuse vs fresh fork","description":"Determine if workers are reused across mutations (accumulating state) or fresh-forked each time. If reused, the parent process is accumulating objects passed to/from workers. If fresh-forked, the parent's memory shouldn't grow — investigate what the parent retains.","notes":"## Investigation Results\n\n**Workers are NOT reused across batches** (fresh fork each batch), but ARE reused within a batch (InProcess isolation, batch_size = jobs count).\n\n### Sequential (jobs=1)\n- Fresh fork per mutation via Isolation::Fork\n- Child exits after 1 mutation — no accumulation\n\n### Parallel (jobs>N) \n- N workers forked per batch (each_slice(config.jobs))\n- Workers loop processing multiple mutations via InProcess isolation\n- Workers killed and re-forked between batches\n- RSpec state accumulates within a worker for batch duration\n- EV-256 fix (clear_examples + release_rspec_state) mitigates in-batch accumulation\n\n### Parent-side accumulation\n- Expected O(N) growth: MutationResult + stripped Mutation per mutation for final report\n- Not a leak — required for report generation\n\n**Conclusion:** Architecture is sound. No unexpected reuse or leak path identified.","status":"closed","priority":0,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:44.352572561+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T22:33:41.490184078+07:00","closed_at":"2026-04-05T22:33:41.490184078+07:00","close_reason":"Investigation complete. Worker lifecycle is sound: fresh fork per batch, InProcess reuse within batch mitigated by EV-256 cleanup. No action needed.","dependencies":[{"issue_id":"EV-255","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
201
|
+
{"id":"EV-256","title":"Fix identified leak source(s)","description":"Based on profiling results, fix the leak. This could be: mutation results accumulating without GC, Prism parse trees retained, test output buffers, or reporter state growing.","status":"in_progress","priority":0,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:46.524854416+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T16:21:16.522830162+07:00","dependencies":[{"issue_id":"EV-256","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
202
|
+
{"id":"EV-257","title":"Add per-mutation RSS regression test","description":"Add a spec that runs N mutations and asserts RSS growth stays below a threshold (e.g., <1 MB/mutation average). Integrate with rake memory:check.","notes":"GitHub: #544","status":"closed","priority":0,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:48.696476684+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T23:22:49.927334624+07:00","closed_at":"2026-04-05T23:22:49.927334624+07:00","close_reason":"Merged PR #568. Added RSpec integration per-mutation check to rake memory:check.","dependencies":[{"issue_id":"EV-257","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
203
|
+
{"id":"EV-258","title":"Identify heredoc nodes in Prism AST","description":"Determine how Prism represents heredocs (interpolated_string_node with heredoc opening?). Document the AST structure for plain heredocs, interpolated heredocs, and squiggly heredocs.","notes":"GitHub: #545","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:50.91761442+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:40.004849276+07:00","dependencies":[{"issue_id":"EV-258","depends_on_id":"EV-241","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
204
|
+
{"id":"EV-259","title":"Add heredoc detection to StringLiteral operator","description":"Modify the StringLiteral operator to detect when the target node is inside a heredoc. Add a heredoc? helper method.","notes":"GitHub: #546","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:52.767879347+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:40.244997087+07:00","dependencies":[{"issue_id":"EV-259","depends_on_id":"EV-241","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
152
205
|
{"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"}
|
|
206
|
+
{"id":"EV-260","title":"Implement interpolation-only mutation mode for heredocs","description":"When mutating a heredoc, only mutate the interpolated expressions (embedded_statements_node), not the literal text surrounding them. This preserves template structure while still testing dynamic content.","notes":"GitHub: #547","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:54.680419198+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:40.456648209+07:00","dependencies":[{"issue_id":"EV-260","depends_on_id":"EV-241","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
207
|
+
{"id":"EV-261","title":"Add --skip-heredoc-literals CLI flag","description":"Add a flag to completely skip string literal mutations inside heredocs. For users who prefer zero heredoc mutations.","notes":"GitHub: #548","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:56.515382643+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:40.698475949+07:00","dependencies":[{"issue_id":"EV-261","depends_on_id":"EV-241","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
208
|
+
{"id":"EV-262","title":"Add tests for heredoc mutation behavior","description":"Test: heredoc with no interpolation (skipped or mutated to empty), heredoc with interpolation (only expressions mutated), squiggly heredoc, nested heredoc.","notes":"GitHub: #549","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:56.333773283+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:40.923526567+07:00","dependencies":[{"issue_id":"EV-262","depends_on_id":"EV-241","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
209
|
+
{"id":"EV-263","title":"Implement temp-file write in SourceSurgeon","description":"Modify SourceSurgeon.apply to write mutated source to a temp file instead of overwriting the original. Return the temp file path. Original file is never touched.","notes":"GitHub: #537","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:29:59.265360981+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:11.816752768+07:00","dependencies":[{"issue_id":"EV-263","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
210
|
+
{"id":"EV-264","title":"Define target metric and measurement methodology for mutation density gap","description":"Define what 'closing the gap' means: target ratio (e.g., <1.5x), measurement protocol (which files, which mutant config), and a benchmark script that can be re-run to track progress over time.","notes":"GitHub: #541","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:30:08.545241632+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:26.634822515+07:00","dependencies":[{"issue_id":"EV-264","depends_on_id":"EV-238","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
211
|
+
{"id":"EV-265","title":"Implement load-path redirection for forked test process","description":"In the fork isolation, prepend the temp directory to $LOAD_PATH (or use a more targeted mechanism) so that require and load pick up the mutated file instead of the original.","notes":"GitHub: #550","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:30:17.522950262+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:34.39887441+07:00","dependencies":[{"issue_id":"EV-265","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
212
|
+
{"id":"EV-266","title":"Handle Rails autoloader (Zeitwerk) compatibility","description":"Zeitwerk uses absolute paths. Test that the temp-file approach works with Zeitwerk's file-to-constant mapping. May need to use Zeitwerk's on_load callbacks or file override mechanism.","notes":"GitHub: #551","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:30:40.575881302+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:30:50.895546091+07:00","dependencies":[{"issue_id":"EV-266","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
213
|
+
{"id":"EV-267","title":"Add cleanup of temp files after mutation run","description":"Ensure temp files are cleaned up on normal exit, exception, and signal (SIGTERM/SIGINT). Use at_exit hooks and signal traps.","notes":"GitHub: #552","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:30:57.733824316+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:31:05.665879929+07:00","dependencies":[{"issue_id":"EV-267","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
214
|
+
{"id":"EV-268","title":"Add integration tests for temp-file mutation","description":"Test: original file never modified during run, mutated code is what the test process sees, cleanup on normal/exceptional exit, Rails autoloader compatibility.","notes":"GitHub: #554","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T11:31:13.886751721+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T11:31:24.33988294+07:00","dependencies":[{"issue_id":"EV-268","depends_on_id":"EV-237","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
215
|
+
{"id":"EV-269","title":"Add 'evil' as alternative executable name","description":"Add exe/evil as an alternative entry point so users with alias be='bundle exec' can run 'be evil'. Need to consider: (1) thin wrapper vs symlink, (2) whether to document the alias suggestion, (3) Windows compatibility of symlinks. Fun quality-of-life feature, not urgent.","status":"open","priority":4,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T12:52:10.764169051+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-04T12:52:10.764169051+07:00"}
|
|
153
216
|
{"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"}]}
|
|
217
|
+
{"id":"EV-270","title":"Bug: Pipe buffer deadlock in Fork isolation when Marshal payload exceeds 64KB","description":"In Isolation::Fork#wait_for_result, the parent calls read_io.read (blocking until EOF) while the child calls Marshal.dump(result, write_io). If the serialized result exceeds the ~64KB Linux pipe buffer, the child blocks on write waiting for the parent to drain, while the parent blocks on read waiting for the child to close the pipe. Classic pipe deadlock. This is the most likely cause of occasional evilution hangs. Fix: read in a loop with IO.select, or use a tempfile for large payloads, or use non-blocking IO.","notes":"GitHub: #557","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T23:39:33.862423078+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T11:36:22.036913254+07:00","closed_at":"2026-04-05T11:36:22.036913254+07:00","close_reason":"False positive: Ruby IO#read drains pipe incrementally, no deadlock occurs. Real bug (double Process.wait) filed as EV-273 / GH #561.","dependencies":[{"issue_id":"EV-270","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
218
|
+
{"id":"EV-271","title":"Bug: No timeout on IO.select in WorkQueue#collect_results","description":"In Parallel::WorkQueue#collect_results (line 123), IO.select(result_ios) has no timeout. If a worker hangs on its current mutation (e.g. infinite loop in mutated code, RSpec deadlock), IO.select blocks forever. Additionally, dead worker pipes stay in result_ios after the worker exits, causing repeated nil reads. Fix: add a timeout to IO.select, remove dead worker IOs from the select set after detecting nil reads.","notes":"GitHub: #558","status":"in_progress","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T23:39:36.855170018+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T11:55:49.178620433+07:00","dependencies":[{"issue_id":"EV-271","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
219
|
+
{"id":"EV-272","title":"Bug: No timeout on collect_worker_timing blocks shutdown indefinitely","description":"In WorkQueue#shutdown_workers, collect_worker_timing does a blocking io.read(4) on each worker's res_read pipe waiting for the STATS message. If a worker is stuck processing its current item (hung RSpec, infinite loop in mutated code), the STATS message is never written and the parent blocks forever. The SHUTDOWN sentinel has been sent but the worker hasn't read it yet. Fix: add a timeout on the read, or use IO.select with timeout before reading, or skip timing collection for unresponsive workers.","notes":"GitHub: #559","status":"in_progress","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-04T23:39:39.901110522+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T14:11:29.437745793+07:00","dependencies":[{"issue_id":"EV-272","depends_on_id":"EV-226","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
220
|
+
{"id":"EV-273","title":"Bug: Double Process.wait in Fork isolation wait_for_result","description":"In Isolation::Fork#wait_for_result (fork.rb:66-78), when read_io.read returns empty data, Process.wait(pid) is called on line 69, then called again on line 72. The second call either raises Errno::ECHILD (caught by rescue nil) or, worse, could reap an unrelated child process if the PID has been recycled. The fix is to skip the second wait when the first already succeeded.","notes":"GitHub: #561","status":"in_progress","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-05T11:34:10.345379629+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-05T14:42:33.770786495+07:00"}
|
|
221
|
+
{"id":"EV-274","title":"Add rake memory:check to CI pipeline","description":"Add the memory leak regression check (rake memory:check) as a CI step. This catches regressions in isolation/integration code by detecting per-mutation RSS growth spikes. Requires Linux runner (/proc/self/status). Consider running after the main spec suite to avoid blocking fast feedback.","notes":"GitHub: #566","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-05T22:53:27.447410371+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T11:52:58.110045281+07:00","closed_at":"2026-04-06T11:52:58.110045281+07:00","close_reason":"Implemented in PR #571. Added memory_check job to CI workflow with pinned runner (ubuntu-24.04), SHA-pinned actions, and explicit threshold env vars."}
|
|
222
|
+
{"id":"EV-275","title":"Use project's own complex classes as memory check fixture","description":"Replace the simple_class.rb fixture in script/memory_check with more complex classes from the evilution codebase itself (e.g. Runner, Config, AST::Parser). This provides realistic per-mutation load: more ExampleGroup subclasses, deeper spec nesting, and heavier metadata — closer to what users see in real projects. Affects check #5 (RSpec integration per-mutation) primarily.","notes":"GitHub: #567","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-05T22:53:30.214655275+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:36:50.959507734+07:00"}
|
|
223
|
+
{"id":"EV-276","title":"InProcess suppress_output closes /dev/null handles causing 'closed stream' on reuse with clear_examples","description":"InProcess#suppress_output uses File.open with blocks that auto-close /dev/null handles after each call. When the RSpec integration uses clear_examples (which reuses Configuration), the formatter retains a reference to $stdout from the first run. On subsequent calls, the formatter writes to the closed handle, causing 'closed stream' errors. Fix: use persistent /dev/null handles or StringIO.","notes":"GitHub: #569","status":"in_progress","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-04-05T23:06:51.502713099+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:02:05.127419951+07:00"}
|
|
154
224
|
{"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"}
|
|
155
225
|
{"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"}
|
|
156
226
|
{"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"}]}
|
|
@@ -273,17 +343,17 @@
|
|
|
273
343
|
{"id":"EV-84","title":"Minitest concrete suggestion templates","description":"All 16 CONCRETE_TEMPLATES in Reporter::Suggestion generate RSpec it/expect blocks. Add parallel Minitest templates using assert_equal style. suggestion_for should select the right template set based on the configured integration.","notes":"GitHub issue: #229","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-22T17:28:41.5343649+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-22T17:28:59.242395265+07:00","dependencies":[{"issue_id":"EV-84","depends_on_id":"EV-69","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-84","depends_on_id":"EV-79","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
274
344
|
{"id":"EV-85","title":"Update documentation for Minitest support","description":"Update CLI help text, MCP tool descriptions, and README to reflect Minitest as a supported integration. Currently --suggest-tests and MCP tool explicitly say 'RSpec'. Make descriptions framework-aware or generic.","notes":"GitHub issue: #230","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-22T17:28:51.219728494+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-22T17:29:06.644497418+07:00","dependencies":[{"issue_id":"EV-85","depends_on_id":"EV-69","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-85","depends_on_id":"EV-79","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-85","depends_on_id":"EV-82","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
275
345
|
{"id":"EV-86","title":"Epic: Compound assignment operator mutations","description":"Add mutation operators for compound assignment expressions (+=, -=, *=, /=, %=, **=, &=, |=, ^=, <<=, >>=, &&=, ||=). Mutant supports all 12+ swaps; Evilution has none. This is the highest-impact missing operator category.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:23.191166178+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T17:07:23.690634548+07:00","closed_at":"2026-03-23T17:07:23.690634548+07:00","close_reason":"All 5 sub-issues complete. Compound assignment operator fully implemented with arithmetic, bitwise, logical swaps, removal mutations, and suggestion templates."}
|
|
276
|
-
{"id":"EV-87","title":"Epic: Variable mutations","description":"Add mutation operators for variable assignments and references (local, instance, class, global variables). Mutant mutates variable assignments (remove, swap values) and references (swap with other variables of same type in scope). Evilution has none.","status":"
|
|
346
|
+
{"id":"EV-87","title":"Epic: Variable mutations","description":"Add mutation operators for variable assignments and references (local, instance, class, global variables). Mutant mutates variable assignments (remove, swap values) and references (swap with other variables of same type in scope). Evilution has none.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:33.213204188+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.58934621+07:00","closed_at":"2026-04-06T12:49:55.58934621+07:00","close_reason":"GH issue already closed"}
|
|
277
347
|
{"id":"EV-88","title":"Implement arithmetic compound assignment mutator (+=, -=, *=, /=, %=, **=)","description":"Create a new mutator that swaps arithmetic compound assignment operators with each other (e.g., += becomes -=, *=, /=). Register in Mutator::Registry. Follow existing operator patterns like BinaryOperatorSwap.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:37.132942691+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T14:27:41.923799202+07:00","closed_at":"2026-03-23T12:02:13.019996743+07:00","close_reason":"PR #361 merged. Implemented CompoundAssignment mutator for +=, -=, *=, /=, %=, **= across all variable types. 15 specs, 100% mutation score.","dependencies":[{"issue_id":"EV-88","depends_on_id":"EV-86","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
278
|
-
{"id":"EV-89","title":"Epic: Exception handling mutations","description":"Add mutation operators for rescue/ensure blocks. Mutant removes rescue clauses, mutates inline rescue, and removes ensure blocks. These are important for testing error handling paths. Evilution has none.","status":"
|
|
348
|
+
{"id":"EV-89","title":"Epic: Exception handling mutations","description":"Add mutation operators for rescue/ensure blocks. Mutant removes rescue clauses, mutates inline rescue, and removes ensure blocks. These are important for testing error handling paths. Evilution has none.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:44.503025925+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590320555+07:00","closed_at":"2026-04-06T12:49:55.590320555+07:00","close_reason":"GH issue already closed"}
|
|
279
349
|
{"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"}
|
|
280
350
|
{"id":"EV-90","title":"Implement bitwise compound assignment mutator (&=, |=, ^=, <<=, >>=)","description":"Create mutations that swap bitwise compound assignment operators with each other. These are Prism op_asgn nodes with bitwise operator tokens.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:49.296076429+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T16:41:58.956765738+07:00","closed_at":"2026-03-23T16:41:58.956765738+07:00","close_reason":"PR merged. Added bitwise compound assignment mutations (&=, |=, ^=, <<=, >>=) to CompoundAssignment mutator.","dependencies":[{"issue_id":"EV-90","depends_on_id":"EV-86","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
281
|
-
{"id":"EV-91","title":"Epic: Break/next/redo control flow mutations","description":"Add mutations for break, next, and redo statements. Mutant mutates these control flow interrupts (swap break/next, remove redo, mutate break/next values). Important for testing loop behavior. Evilution has none.","status":"
|
|
351
|
+
{"id":"EV-91","title":"Epic: Break/next/redo control flow mutations","description":"Add mutations for break, next, and redo statements. Mutant mutates these control flow interrupts (swap break/next, remove redo, mutate break/next values). Important for testing loop behavior. Evilution has none.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:52.890173894+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590329267+07:00","closed_at":"2026-04-06T12:49:55.590329267+07:00","close_reason":"GH issue already closed"}
|
|
282
352
|
{"id":"EV-92","title":"Implement local variable assignment removal mutator","description":"Create a mutator that removes local variable assignments (lvasgn nodes), keeping only the value expression or removing the entire statement. Follow existing statement deletion patterns.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:54.68007817+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T01:05:19.57588682+07:00","dependencies":[{"issue_id":"EV-92","depends_on_id":"EV-87","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
283
353
|
{"id":"EV-93","title":"Implement logical compound assignment mutator (&&=, ||=)","description":"Create mutations that swap &&= with ||= and vice versa. These are Prism and_asgn/or_asgn nodes, distinct from op_asgn.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:15:59.501149639+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T16:57:19.169563553+07:00","closed_at":"2026-03-23T16:57:19.169563553+07:00","close_reason":"PR #363 merged. Added &&= and ||= swap mutations for all variable types.","dependencies":[{"issue_id":"EV-93","depends_on_id":"EV-86","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
284
354
|
{"id":"EV-94","title":"Implement break statement mutator","description":"Create a mutator for break statements. Mutations: remove break (let loop continue), replace break value with nil, swap break with next. Prism break_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:05.462729749+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T10:58:26.095710774+07:00","dependencies":[{"issue_id":"EV-94","depends_on_id":"EV-91","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
285
355
|
{"id":"EV-95","title":"Implement instance variable mutation","description":"Create a mutator for instance variable writes (@ivar = val). Mutations: remove assignment, replace value with nil. Prism instance_variable_write_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:07.10678859+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T01:10:37.845543866+07:00","dependencies":[{"issue_id":"EV-95","depends_on_id":"EV-87","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
286
|
-
{"id":"EV-96","title":"Epic: Expand collection method swap pairs","description":"Add more orthogonal method swap pairs to close the gap with Mutant's 20+ pairs. Evilution has 14 pairs; needs 6+ more covering common Ruby collection methods.","status":"
|
|
356
|
+
{"id":"EV-96","title":"Epic: Expand collection method swap pairs","description":"Add more orthogonal method swap pairs to close the gap with Mutant's 20+ pairs. Evilution has 14 pairs; needs 6+ more covering common Ruby collection methods.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:07.686812036+07:00","created_by":"Denis Kiselev","updated_at":"2026-04-06T12:49:55.590334822+07:00","closed_at":"2026-04-06T12:49:55.590334822+07:00","close_reason":"GH issue already closed"}
|
|
287
357
|
{"id":"EV-97","title":"Add compound assignment to removal mutator","description":"Extend the compound assignment mutators to also generate a removal mutation (remove the entire compound assignment statement). Aligns with Mutant's statement deletion behavior.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:15.098552795+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T17:02:39.203119113+07:00","closed_at":"2026-03-23T17:02:39.203119113+07:00","close_reason":"PR merged. Added removal mutations for all compound assignment types.","dependencies":[{"issue_id":"EV-97","depends_on_id":"EV-86","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
288
358
|
{"id":"EV-98","title":"Implement next statement mutator","description":"Create a mutator for next statements. Mutations: remove next (let block continue), replace next value with nil, swap next with break. Prism next_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:16.638335477+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T11:48:21.34281786+07:00","dependencies":[{"issue_id":"EV-98","depends_on_id":"EV-91","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
289
359
|
{"id":"EV-99","title":"Implement class variable mutation","description":"Create a mutator for class variable writes (@@cvar = val). Mutations: remove assignment, replace value with nil. Prism class_variable_write_node.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:16:15.967143759+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T01:20:18.161043033+07:00","dependencies":[{"issue_id":"EV-99","depends_on_id":"EV-87","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0] - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Smart spec auto-detection** — `SpecResolver` maps source files to the closest matching spec using Rails conventions: controllers to request specs (`app/controllers/foo_controller.rb` → `spec/requests/foo_spec.rb`), models, services, Avo resources, and lib/ paths; falls back through parent directory patterns when exact match doesn't exist; warns when falling back to full suite so users know to use `--spec` (#530, #555)
|
|
8
|
+
- **`--spec-dir DIR` CLI flag** — include all `*_spec.rb` files in a directory recursively; composable with `--spec` for combining explicit files and directories (#513)
|
|
9
|
+
- **RSS tracking per mutation** — JSON output includes per-mutation RSS memory measurements for profiling memory behavior across mutations (#532)
|
|
10
|
+
- **Memory budget CI gate** — dedicated benchmark workflow with memory check step; uses realistic project classes as fixtures (#533, #567)
|
|
11
|
+
- **Worker hang protection** — `WorkQueue` item timeout prevents indefinite worker hangs; timeout handling for worker timing collection (#558, #559)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **InProcess `suppress_output` closing `/dev/null` handle** — prevent closing the shared `/dev/null` file descriptor which caused subsequent output suppression to fail (#569)
|
|
16
|
+
- **Double `Process.wait` in Fork isolation** — handle empty child processes without raising (#561)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **RSpec integration memory management** — use clear hooks to release AST nodes and source strings between mutations, preventing memory retention across mutation runs (#543)
|
|
21
|
+
|
|
3
22
|
## [0.18.0] - 2026-04-03
|
|
4
23
|
|
|
5
24
|
### Added
|
data/README.md
CHANGED
|
@@ -51,7 +51,8 @@ evilution [command] [options] [files...]
|
|
|
51
51
|
| `-f`, `--format FORMAT` | String | `text` | Output format: `text`, `json`, or `html`. |
|
|
52
52
|
| `--target EXPR` | String | _(none)_ | Only mutate matching methods. Supports method name (`Foo::Bar#calculate`), class (`Foo`), namespace wildcards (`Foo::Bar*`), method-type selectors (`Foo#`, `Foo.`), descendants (`descendants:Foo`), and source globs (`source:lib/**/*.rb`). |
|
|
53
53
|
| `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
|
|
54
|
-
| `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `
|
|
54
|
+
| `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to auto-detection via `SpecResolver`. |
|
|
55
|
+
| `--spec-dir DIR` | String | _(none)_ | Include all `*_spec.rb` files in DIR recursively. Composable with `--spec`. |
|
|
55
56
|
| `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Uses demand-driven work distribution with pipe-based IPC. |
|
|
56
57
|
| `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
|
|
57
58
|
| `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
|
data/lib/evilution/baseline.rb
CHANGED
|
@@ -93,6 +93,14 @@ class Evilution::Baseline
|
|
|
93
93
|
private
|
|
94
94
|
|
|
95
95
|
def resolve_unique_spec_files(subjects)
|
|
96
|
-
|
|
96
|
+
warned = Set.new
|
|
97
|
+
subjects.map do |s|
|
|
98
|
+
resolved = @spec_resolver.call(s.file_path)
|
|
99
|
+
if resolved.nil? && warned.add?(s.file_path)
|
|
100
|
+
warn "[evilution] No matching spec found for #{s.file_path}, running full suite. " \
|
|
101
|
+
"Use --spec to specify the spec file."
|
|
102
|
+
end
|
|
103
|
+
resolved || "spec"
|
|
104
|
+
end.uniq
|
|
97
105
|
end
|
|
98
106
|
end
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -53,6 +53,16 @@ class Evilution::CLI
|
|
|
53
53
|
0
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def expand_spec_dir(dir)
|
|
57
|
+
unless File.directory?(dir)
|
|
58
|
+
warn("Error: #{dir} is not a directory")
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
specs = Dir.glob(File.join(dir, "**/*_spec.rb"))
|
|
63
|
+
@options[:spec_files] = Array(@options[:spec_files]) + specs
|
|
64
|
+
end
|
|
65
|
+
|
|
56
66
|
def run_subcommand_error(message)
|
|
57
67
|
warn("Error: #{message}")
|
|
58
68
|
2
|
|
@@ -224,6 +234,7 @@ class Evilution::CLI
|
|
|
224
234
|
def add_filter_options(opts)
|
|
225
235
|
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
226
236
|
opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
|
|
237
|
+
opts.on("--spec-dir DIR", "Include all specs in DIR") { |d| expand_spec_dir(d) }
|
|
227
238
|
opts.on("--target EXPR",
|
|
228
239
|
"Filter: method (Foo#bar), type (Foo#/Foo.), namespace (Foo*),",
|
|
229
240
|
"class (Foo), glob (source:**/*.rb), hierarchy (descendants:Foo)") do |m|
|
|
@@ -12,6 +12,8 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
12
12
|
def initialize(test_files: nil, hooks: nil)
|
|
13
13
|
@test_files = test_files
|
|
14
14
|
@rspec_loaded = false
|
|
15
|
+
@spec_resolver = Evilution::SpecResolver.new
|
|
16
|
+
@warned_files = Set.new
|
|
15
17
|
super(hooks: hooks)
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -98,9 +100,14 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
98
100
|
# process exits after each run.
|
|
99
101
|
#
|
|
100
102
|
# This integration can also be invoked directly (e.g. in specs or alternative
|
|
101
|
-
# runners) without fork isolation.
|
|
102
|
-
#
|
|
103
|
-
|
|
103
|
+
# runners) without fork isolation. clear_examples reuses the existing World
|
|
104
|
+
# and Configuration (avoiding per-run instance growth) while clearing loaded
|
|
105
|
+
# example groups, constants, and configuration state.
|
|
106
|
+
if ::RSpec.respond_to?(:clear_examples)
|
|
107
|
+
::RSpec.clear_examples
|
|
108
|
+
else
|
|
109
|
+
::RSpec.reset
|
|
110
|
+
end
|
|
104
111
|
|
|
105
112
|
out = StringIO.new
|
|
106
113
|
err = StringIO.new
|
|
@@ -108,11 +115,60 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
108
115
|
args = build_args(mutation)
|
|
109
116
|
command = "rspec #{args.join(" ")}"
|
|
110
117
|
|
|
118
|
+
eg_before = snapshot_example_groups
|
|
111
119
|
status = ::RSpec::Core::Runner.run(args, out, err)
|
|
112
120
|
|
|
113
121
|
{ passed: status.zero?, test_command: command }
|
|
114
122
|
rescue StandardError => e
|
|
115
123
|
{ passed: false, error: e.message, test_command: command }
|
|
124
|
+
ensure
|
|
125
|
+
release_rspec_state(eg_before)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def snapshot_example_groups
|
|
129
|
+
groups = Set.new
|
|
130
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
131
|
+
groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
|
|
132
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
133
|
+
end
|
|
134
|
+
groups
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def release_rspec_state(eg_before)
|
|
138
|
+
release_example_groups(eg_before)
|
|
139
|
+
# Remove ExampleGroups constants so the named reference is dropped.
|
|
140
|
+
# We avoid a full RSpec.reset here because it creates new World and
|
|
141
|
+
# Configuration instances each call; the pre-run reset already handles
|
|
142
|
+
# that. Instead, clear the world's example_groups array (which holds
|
|
143
|
+
# direct class references) and the source cache.
|
|
144
|
+
::RSpec::ExampleGroups.remove_all_constants if defined?(::RSpec::ExampleGroups)
|
|
145
|
+
release_world_example_groups
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def release_example_groups(eg_before)
|
|
149
|
+
return unless eg_before
|
|
150
|
+
|
|
151
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
152
|
+
next unless klass < ::RSpec::Core::ExampleGroup
|
|
153
|
+
next if eg_before.include?(klass.object_id)
|
|
154
|
+
|
|
155
|
+
# Remove nested module constants (LetDefinitions, NamedSubjectPreventSuper)
|
|
156
|
+
klass.constants(false).each do |const|
|
|
157
|
+
klass.send(:remove_const, const)
|
|
158
|
+
rescue NameError # rubocop:disable Lint/SuppressedException
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
klass.instance_variables.each do |ivar|
|
|
162
|
+
klass.remove_instance_variable(ivar)
|
|
163
|
+
end
|
|
164
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def release_world_example_groups
|
|
169
|
+
world = ::RSpec.world
|
|
170
|
+
world.instance_variable_get(:@example_groups).clear if world.instance_variable_defined?(:@example_groups)
|
|
171
|
+
world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
|
|
116
172
|
end
|
|
117
173
|
|
|
118
174
|
def build_args(mutation)
|
|
@@ -123,7 +179,20 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
123
179
|
def resolve_test_files(mutation)
|
|
124
180
|
return test_files if test_files
|
|
125
181
|
|
|
126
|
-
resolved =
|
|
127
|
-
|
|
182
|
+
resolved = @spec_resolver.call(mutation.file_path)
|
|
183
|
+
if resolved
|
|
184
|
+
[resolved]
|
|
185
|
+
else
|
|
186
|
+
warn_unresolved_spec(mutation.file_path)
|
|
187
|
+
["spec"]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def warn_unresolved_spec(file_path)
|
|
192
|
+
return if @warned_files.include?(file_path)
|
|
193
|
+
|
|
194
|
+
@warned_files << file_path
|
|
195
|
+
warn "[evilution] No matching spec found for #{file_path}, running full suite. " \
|
|
196
|
+
"Use --spec to specify the spec file."
|
|
128
197
|
end
|
|
129
198
|
end
|
|
@@ -16,6 +16,7 @@ class Evilution::Isolation::Fork
|
|
|
16
16
|
def call(mutation:, test_command:, timeout:)
|
|
17
17
|
sandbox_dir = Dir.mktmpdir("evilution-run")
|
|
18
18
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
parent_rss = Evilution::Memory.rss_kb
|
|
19
20
|
read_io, write_io = IO.pipe
|
|
20
21
|
|
|
21
22
|
pid = ::Process.fork do
|
|
@@ -33,7 +34,7 @@ class Evilution::Isolation::Fork
|
|
|
33
34
|
result = wait_for_result(pid, read_io, timeout)
|
|
34
35
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
35
36
|
|
|
36
|
-
build_mutation_result(mutation, result, duration)
|
|
37
|
+
build_mutation_result(mutation, result, duration, parent_rss)
|
|
37
38
|
ensure
|
|
38
39
|
read_io&.close
|
|
39
40
|
write_io&.close
|
|
@@ -67,10 +68,12 @@ class Evilution::Isolation::Fork
|
|
|
67
68
|
if read_io.wait_readable(timeout)
|
|
68
69
|
data = read_io.read
|
|
69
70
|
::Process.wait(pid)
|
|
70
|
-
return { timeout: false }.merge(Marshal.load(data)) unless data.empty? # rubocop:disable Security/MarshalLoad
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if data.empty?
|
|
73
|
+
{ timeout: false, passed: false, error: "empty result from child" }
|
|
74
|
+
else
|
|
75
|
+
{ timeout: false }.merge(Marshal.load(data)) # rubocop:disable Security/MarshalLoad
|
|
76
|
+
end
|
|
74
77
|
else
|
|
75
78
|
terminate_child(pid)
|
|
76
79
|
{ timeout: true }
|
|
@@ -90,7 +93,7 @@ class Evilution::Isolation::Fork
|
|
|
90
93
|
::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
def build_mutation_result(mutation, result, duration)
|
|
96
|
+
def build_mutation_result(mutation, result, duration, parent_rss_kb)
|
|
94
97
|
status = if result[:timeout]
|
|
95
98
|
:timeout
|
|
96
99
|
elsif result[:error]
|
|
@@ -106,7 +109,8 @@ class Evilution::Isolation::Fork
|
|
|
106
109
|
status: status,
|
|
107
110
|
duration: duration,
|
|
108
111
|
test_command: result[:test_command],
|
|
109
|
-
child_rss_kb: result[:child_rss_kb]
|
|
112
|
+
child_rss_kb: result[:child_rss_kb],
|
|
113
|
+
parent_rss_kb: parent_rss_kb
|
|
110
114
|
)
|
|
111
115
|
end
|
|
112
116
|
end
|
|
@@ -7,6 +7,13 @@ require_relative "../result/mutation_result"
|
|
|
7
7
|
require_relative "../isolation"
|
|
8
8
|
|
|
9
9
|
class Evilution::Isolation::InProcess
|
|
10
|
+
@null_out = File.open(File::NULL, "w")
|
|
11
|
+
@null_err = File.open(File::NULL, "w")
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :null_out, :null_err
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def call(mutation:, test_command:, timeout:)
|
|
11
18
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
12
19
|
rss_before = Evilution::Memory.rss_kb
|
|
@@ -15,7 +22,7 @@ class Evilution::Isolation::InProcess
|
|
|
15
22
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
16
23
|
delta = compute_memory_delta(rss_before, rss_after, result)
|
|
17
24
|
|
|
18
|
-
build_mutation_result(mutation, result, duration, rss_after, delta)
|
|
25
|
+
build_mutation_result(mutation, result, duration, rss_before, rss_after, delta)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
private
|
|
@@ -34,13 +41,9 @@ class Evilution::Isolation::InProcess
|
|
|
34
41
|
def suppress_output
|
|
35
42
|
saved_stdout = $stdout
|
|
36
43
|
saved_stderr = $stderr
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
$stderr = null_err
|
|
41
|
-
yield
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
+
$stdout = self.class.null_out
|
|
45
|
+
$stderr = self.class.null_err
|
|
46
|
+
yield
|
|
44
47
|
ensure
|
|
45
48
|
$stdout = saved_stdout
|
|
46
49
|
$stderr = saved_stderr
|
|
@@ -53,7 +56,7 @@ class Evilution::Isolation::InProcess
|
|
|
53
56
|
rss_after - rss_before
|
|
54
57
|
end
|
|
55
58
|
|
|
56
|
-
def build_mutation_result(mutation, result, duration, rss_after, memory_delta_kb)
|
|
59
|
+
def build_mutation_result(mutation, result, duration, rss_before, rss_after, memory_delta_kb)
|
|
57
60
|
status = if result[:timeout]
|
|
58
61
|
:timeout
|
|
59
62
|
elsif result[:error]
|
|
@@ -70,7 +73,8 @@ class Evilution::Isolation::InProcess
|
|
|
70
73
|
duration: duration,
|
|
71
74
|
test_command: result[:test_command],
|
|
72
75
|
child_rss_kb: rss_after,
|
|
73
|
-
memory_delta_kb: memory_delta_kb
|
|
76
|
+
memory_delta_kb: memory_delta_kb,
|
|
77
|
+
parent_rss_kb: rss_before
|
|
74
78
|
)
|
|
75
79
|
end
|
|
76
80
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require_relative "work_queue"
|
|
4
4
|
|
|
5
5
|
class Evilution::Parallel::Pool
|
|
6
|
-
def initialize(size:, hooks: nil)
|
|
7
|
-
@queue = Evilution::Parallel::WorkQueue.new(size: size, hooks: hooks)
|
|
6
|
+
def initialize(size:, hooks: nil, item_timeout: nil)
|
|
7
|
+
@queue = Evilution::Parallel::WorkQueue.new(size: size, hooks: hooks, item_timeout: item_timeout)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def map(items, &)
|
|
@@ -7,6 +7,8 @@ class Evilution::Parallel::WorkQueue
|
|
|
7
7
|
|
|
8
8
|
STATS = :__stats__
|
|
9
9
|
|
|
10
|
+
TIMING_GRACE_PERIOD = 5
|
|
11
|
+
|
|
10
12
|
WorkerStat = Struct.new(:pid, :items_completed, :busy_time, :wall_time) do
|
|
11
13
|
def idle_time
|
|
12
14
|
wall_time - busy_time
|
|
@@ -19,13 +21,17 @@ class Evilution::Parallel::WorkQueue
|
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def initialize(size:, hooks: nil, prefetch: 1)
|
|
24
|
+
def initialize(size:, hooks: nil, prefetch: 1, item_timeout: nil)
|
|
23
25
|
raise ArgumentError, "pool size must be a positive integer, got #{size.inspect}" unless size.is_a?(Integer) && size >= 1
|
|
24
26
|
raise ArgumentError, "prefetch must be a positive integer, got #{prefetch.inspect}" unless prefetch.is_a?(Integer) && prefetch >= 1
|
|
27
|
+
unless item_timeout.nil? || (item_timeout.is_a?(Numeric) && item_timeout.positive?)
|
|
28
|
+
raise ArgumentError, "item_timeout must be nil or a positive number, got #{item_timeout.inspect}"
|
|
29
|
+
end
|
|
25
30
|
|
|
26
31
|
@size = size
|
|
27
32
|
@hooks = hooks
|
|
28
33
|
@prefetch = prefetch
|
|
34
|
+
@item_timeout = item_timeout
|
|
29
35
|
@worker_stats = []
|
|
30
36
|
end
|
|
31
37
|
|
|
@@ -63,7 +69,7 @@ class Evilution::Parallel::WorkQueue
|
|
|
63
69
|
cmd_read.close
|
|
64
70
|
res_write.close
|
|
65
71
|
|
|
66
|
-
{ pid: pid, cmd_write: cmd_write, res_read: res_read, items_completed: 0 }
|
|
72
|
+
{ pid: pid, cmd_write: cmd_write, res_read: res_read, items_completed: 0, pending: 0 }
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
|
|
@@ -120,8 +126,18 @@ class Evilution::Parallel::WorkQueue
|
|
|
120
126
|
result_ios = io_to_worker.keys
|
|
121
127
|
|
|
122
128
|
while state.in_flight.positive?
|
|
123
|
-
readable, = IO.select(result_ios)
|
|
124
|
-
|
|
129
|
+
readable, = IO.select(result_ios, nil, nil, @item_timeout)
|
|
130
|
+
|
|
131
|
+
if readable.nil?
|
|
132
|
+
terminate_stuck_workers(workers)
|
|
133
|
+
state.first_error = Evilution::Error.new("worker timed out after #{@item_timeout}s") if state.first_error.nil?
|
|
134
|
+
break
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
readable.each do |io|
|
|
138
|
+
alive = handle_result(io, io_to_worker[io], items, state)
|
|
139
|
+
result_ios.delete(io) unless alive
|
|
140
|
+
end
|
|
125
141
|
end
|
|
126
142
|
end
|
|
127
143
|
|
|
@@ -130,23 +146,27 @@ class Evilution::Parallel::WorkQueue
|
|
|
130
146
|
|
|
131
147
|
if message.nil?
|
|
132
148
|
state.first_error = Evilution::Error.new("worker process exited unexpectedly") if state.first_error.nil?
|
|
133
|
-
state.in_flight -=
|
|
134
|
-
|
|
149
|
+
state.in_flight -= worker[:pending]
|
|
150
|
+
worker[:pending] = 0
|
|
151
|
+
return false
|
|
135
152
|
end
|
|
136
153
|
|
|
137
154
|
index, status, value = message
|
|
138
155
|
state.first_error = value if status == :error && state.first_error.nil?
|
|
139
156
|
state.results[index] = value if status == :ok
|
|
140
157
|
state.in_flight -= 1
|
|
158
|
+
worker[:pending] -= 1
|
|
141
159
|
worker[:items_completed] += 1
|
|
142
160
|
|
|
143
161
|
send_item(worker, items, state) if state.next_index < items.length && state.first_error.nil?
|
|
162
|
+
true
|
|
144
163
|
end
|
|
145
164
|
|
|
146
165
|
def send_item(worker, items, state)
|
|
147
166
|
write_message(worker[:cmd_write], [state.next_index, items[state.next_index]])
|
|
148
167
|
state.next_index += 1
|
|
149
168
|
state.in_flight += 1
|
|
169
|
+
worker[:pending] += 1
|
|
150
170
|
end
|
|
151
171
|
|
|
152
172
|
def build_worker_stats(workers)
|
|
@@ -155,6 +175,14 @@ class Evilution::Parallel::WorkQueue
|
|
|
155
175
|
end
|
|
156
176
|
end
|
|
157
177
|
|
|
178
|
+
def terminate_stuck_workers(workers)
|
|
179
|
+
workers.each do |worker|
|
|
180
|
+
Process.kill("KILL", worker[:pid])
|
|
181
|
+
rescue Errno::ESRCH
|
|
182
|
+
nil # Already exited
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
158
186
|
def shutdown_workers(workers)
|
|
159
187
|
workers.each do |worker|
|
|
160
188
|
write_message(worker[:cmd_write], SHUTDOWN)
|
|
@@ -174,18 +202,31 @@ class Evilution::Parallel::WorkQueue
|
|
|
174
202
|
end
|
|
175
203
|
|
|
176
204
|
def collect_worker_timing(workers)
|
|
177
|
-
workers.
|
|
178
|
-
|
|
179
|
-
next if message.nil?
|
|
205
|
+
io_to_worker = workers.reject { |w| w[:res_read].closed? }.to_h { |w| [w[:res_read], w] }
|
|
206
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TIMING_GRACE_PERIOD
|
|
180
207
|
|
|
181
|
-
|
|
182
|
-
|
|
208
|
+
until io_to_worker.empty?
|
|
209
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
210
|
+
break if remaining <= 0
|
|
183
211
|
|
|
184
|
-
|
|
185
|
-
|
|
212
|
+
readable, = IO.select(io_to_worker.keys, nil, nil, remaining)
|
|
213
|
+
break unless readable
|
|
214
|
+
|
|
215
|
+
readable.each { |io| apply_worker_timing(io_to_worker.delete(io), io) }
|
|
186
216
|
end
|
|
187
217
|
end
|
|
188
218
|
|
|
219
|
+
def apply_worker_timing(worker, io)
|
|
220
|
+
message = read_result(io)
|
|
221
|
+
return if message.nil?
|
|
222
|
+
|
|
223
|
+
tag, busy_time, wall_time = message
|
|
224
|
+
return unless tag == STATS
|
|
225
|
+
|
|
226
|
+
worker[:busy_time] = busy_time
|
|
227
|
+
worker[:wall_time] = wall_time
|
|
228
|
+
end
|
|
229
|
+
|
|
189
230
|
def write_message(io, data)
|
|
190
231
|
payload = Marshal.dump(data)
|
|
191
232
|
io.write([payload.bytesize].pack("N"))
|
|
@@ -75,6 +75,7 @@ class Evilution::Reporter::JSON
|
|
|
75
75
|
}
|
|
76
76
|
detail[:suggestion] = @suggestion.suggestion_for(mutation) if result.status == :survived
|
|
77
77
|
detail[:test_command] = result.test_command if result.test_command
|
|
78
|
+
detail[:parent_rss_kb] = result.parent_rss_kb if result.parent_rss_kb
|
|
78
79
|
detail[:child_rss_kb] = result.child_rss_kb if result.child_rss_kb
|
|
79
80
|
detail[:memory_delta_kb] = result.memory_delta_kb if result.memory_delta_kb
|
|
80
81
|
detail
|
|
@@ -6,9 +6,11 @@ class Evilution::Result::MutationResult
|
|
|
6
6
|
STATUSES = %i[killed survived timeout error neutral equivalent].freeze
|
|
7
7
|
|
|
8
8
|
attr_reader :mutation, :status, :duration, :killing_test, :test_command,
|
|
9
|
-
:child_rss_kb, :memory_delta_kb
|
|
9
|
+
:child_rss_kb, :memory_delta_kb, :parent_rss_kb
|
|
10
10
|
|
|
11
|
-
def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
|
|
11
|
+
def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
|
|
12
|
+
test_command: nil, child_rss_kb: nil, memory_delta_kb: nil,
|
|
13
|
+
parent_rss_kb: nil)
|
|
12
14
|
raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
|
|
13
15
|
|
|
14
16
|
@mutation = mutation
|
|
@@ -18,6 +20,7 @@ class Evilution::Result::MutationResult
|
|
|
18
20
|
@test_command = test_command
|
|
19
21
|
@child_rss_kb = child_rss_kb
|
|
20
22
|
@memory_delta_kb = memory_delta_kb
|
|
23
|
+
@parent_rss_kb = parent_rss_kb
|
|
21
24
|
freeze
|
|
22
25
|
end
|
|
23
26
|
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -325,7 +325,7 @@ class Evilution::Runner
|
|
|
325
325
|
|
|
326
326
|
def run_mutations_parallel(mutations, baseline_result = nil)
|
|
327
327
|
integration = build_integration
|
|
328
|
-
pool = Evilution::Parallel::Pool.new(size: config.jobs, hooks: @hooks)
|
|
328
|
+
pool = Evilution::Parallel::Pool.new(size: config.jobs, hooks: @hooks, item_timeout: config.timeout ? config.timeout * 2 : nil)
|
|
329
329
|
worker_isolator = Evilution::Isolation::InProcess.new
|
|
330
330
|
spec_resolver = baseline_result&.failed? ? Evilution::SpecResolver.new : nil
|
|
331
331
|
state = { results: [], survived_count: 0, truncated: false, completed: 0 }
|
|
@@ -396,7 +396,8 @@ class Evilution::Runner
|
|
|
396
396
|
duration: result.duration,
|
|
397
397
|
test_command: result.test_command,
|
|
398
398
|
child_rss_kb: result.child_rss_kb,
|
|
399
|
-
memory_delta_kb: result.memory_delta_kb
|
|
399
|
+
memory_delta_kb: result.memory_delta_kb,
|
|
400
|
+
parent_rss_kb: result.parent_rss_kb
|
|
400
401
|
)
|
|
401
402
|
end
|
|
402
403
|
|
|
@@ -407,7 +408,8 @@ class Evilution::Runner
|
|
|
407
408
|
killing_test: result.killing_test,
|
|
408
409
|
test_command: result.test_command,
|
|
409
410
|
child_rss_kb: result.child_rss_kb,
|
|
410
|
-
memory_delta_kb: result.memory_delta_kb
|
|
411
|
+
memory_delta_kb: result.memory_delta_kb,
|
|
412
|
+
parent_rss_kb: result.parent_rss_kb
|
|
411
413
|
}
|
|
412
414
|
end
|
|
413
415
|
|
|
@@ -420,7 +422,8 @@ class Evilution::Runner
|
|
|
420
422
|
killing_test: data[:killing_test],
|
|
421
423
|
test_command: data[:test_command],
|
|
422
424
|
child_rss_kb: data[:child_rss_kb],
|
|
423
|
-
memory_delta_kb: data[:memory_delta_kb]
|
|
425
|
+
memory_delta_kb: data[:memory_delta_kb],
|
|
426
|
+
parent_rss_kb: data[:parent_rss_kb]
|
|
424
427
|
)
|
|
425
428
|
end
|
|
426
429
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Evilution::SpecResolver
|
|
4
4
|
STRIPPABLE_PREFIXES = %w[lib/ app/].freeze
|
|
5
|
+
CONTROLLER_PREFIX = "controllers/"
|
|
5
6
|
|
|
6
7
|
def call(source_path)
|
|
7
8
|
return nil if source_path.nil? || source_path.empty?
|
|
@@ -29,7 +30,8 @@ class Evilution::SpecResolver
|
|
|
29
30
|
|
|
30
31
|
candidates = if prefix
|
|
31
32
|
stripped = base.delete_prefix(prefix)
|
|
32
|
-
|
|
33
|
+
request_spec = controller_to_request_spec(stripped)
|
|
34
|
+
[request_spec, "spec/#{stripped}", "spec/#{base}"].compact
|
|
33
35
|
else
|
|
34
36
|
["spec/#{base}"]
|
|
35
37
|
end
|
|
@@ -39,6 +41,16 @@ class Evilution::SpecResolver
|
|
|
39
41
|
candidates + fallbacks
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def controller_to_request_spec(stripped_path)
|
|
45
|
+
return nil unless stripped_path.start_with?(CONTROLLER_PREFIX)
|
|
46
|
+
return nil unless stripped_path.end_with?("_controller_spec.rb")
|
|
47
|
+
|
|
48
|
+
request_path = stripped_path
|
|
49
|
+
.delete_prefix(CONTROLLER_PREFIX)
|
|
50
|
+
.sub(/_controller_spec\.rb\z/, "_spec.rb")
|
|
51
|
+
"spec/requests/#{request_path}"
|
|
52
|
+
end
|
|
53
|
+
|
|
42
54
|
def parent_fallback_candidates(spec_path)
|
|
43
55
|
parts = spec_path.split("/")
|
|
44
56
|
# parts: ["spec", "foo", "bar_spec.rb"] — need at least 3 parts for fallback
|
data/lib/evilution/version.rb
CHANGED
data/script/memory_check
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
require_relative "../lib/evilution"
|
|
5
5
|
require_relative "../lib/evilution/memory/leak_check"
|
|
6
|
+
require_relative "../lib/evilution/integration/rspec"
|
|
6
7
|
|
|
7
8
|
FIXTURE = File.expand_path("../spec/support/fixtures/simple_class.rb", __dir__)
|
|
9
|
+
FIXTURE_SPEC = File.expand_path("../spec/support/fixtures/simple_class_spec.rb", __dir__)
|
|
10
|
+
COMPLEX_FIXTURE = File.expand_path("../lib/evilution/config.rb", __dir__)
|
|
11
|
+
COMPLEX_FIXTURE_SPEC = File.expand_path("../spec/evilution/config_spec.rb", __dir__)
|
|
8
12
|
ITERATIONS = Integer(ENV.fetch("MEMORY_CHECK_ITERATIONS", 50))
|
|
9
13
|
MAX_GROWTH_KB = Integer(ENV.fetch("MEMORY_CHECK_MAX_GROWTH_KB", 10_240))
|
|
10
14
|
|
|
@@ -90,5 +94,23 @@ if mutations.size >= 2
|
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
96
|
|
|
97
|
+
# 5. RSpec integration per-mutation with complex fixture
|
|
98
|
+
# Uses Config (227 LOC, 73 specs, 564 mutations) for realistic per-mutation load:
|
|
99
|
+
# more ExampleGroup subclasses, deeper spec nesting, heavier metadata.
|
|
100
|
+
complex_parser = Evilution::AST::Parser.new
|
|
101
|
+
complex_registry = Evilution::Mutator::Registry.default
|
|
102
|
+
complex_subjects = complex_parser.call(COMPLEX_FIXTURE)
|
|
103
|
+
complex_mutations = complex_subjects.flat_map { |s| complex_registry.mutations_for(s) }
|
|
104
|
+
|
|
105
|
+
integration = Evilution::Integration::RSpec.new(test_files: [COMPLEX_FIXTURE_SPEC])
|
|
106
|
+
|
|
107
|
+
all_passed &= run_check("RSpec integration per-mutation (Config)", iterations: 20, max_growth_kb: 20_480) do
|
|
108
|
+
mutation = complex_mutations.sample
|
|
109
|
+
result = integration.call(mutation)
|
|
110
|
+
raise "RSpec integration memory check failed: #{result[:error]}" if result[:error]
|
|
111
|
+
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
93
115
|
puts all_passed ? "All memory checks passed." : "Some memory checks failed!"
|
|
94
116
|
exit(all_passed ? 0 : 1)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.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-04-
|
|
11
|
+
date: 2026-04-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|