evilution 0.13.0 → 0.14.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 +8 -8
- data/CHANGELOG.md +17 -0
- data/lib/evilution/ast/parser.rb +69 -68
- data/lib/evilution/ast/source_surgeon.rb +7 -9
- data/lib/evilution/ast.rb +4 -0
- data/lib/evilution/baseline.rb +73 -75
- data/lib/evilution/cache.rb +75 -77
- data/lib/evilution/cli.rb +408 -173
- data/lib/evilution/config.rb +141 -136
- data/lib/evilution/equivalent/detector.rb +25 -27
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
- data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
- data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
- data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
- data/lib/evilution/equivalent/heuristic.rb +6 -0
- data/lib/evilution/equivalent.rb +4 -0
- data/lib/evilution/git/changed_files.rb +35 -37
- data/lib/evilution/git.rb +4 -0
- data/lib/evilution/integration/base.rb +5 -7
- data/lib/evilution/integration/rspec.rb +114 -116
- data/lib/evilution/integration.rb +4 -0
- data/lib/evilution/isolation/fork.rb +98 -100
- data/lib/evilution/isolation/in_process.rb +59 -61
- data/lib/evilution/isolation.rb +4 -0
- data/lib/evilution/mcp/mutate_tool.rb +172 -143
- data/lib/evilution/mcp/server.rb +12 -11
- data/lib/evilution/mcp/session_diff_tool.rb +89 -0
- data/lib/evilution/mcp/session_list_tool.rb +46 -0
- data/lib/evilution/mcp/session_show_tool.rb +53 -0
- data/lib/evilution/mcp.rb +4 -0
- data/lib/evilution/memory/leak_check.rb +80 -84
- data/lib/evilution/memory.rb +34 -36
- data/lib/evilution/mutation.rb +40 -42
- data/lib/evilution/mutator/base.rb +46 -48
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
- data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
- data/lib/evilution/mutator/operator/array_literal.rb +18 -22
- data/lib/evilution/mutator/operator/block_removal.rb +16 -20
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
- data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
- data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
- data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
- data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
- data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
- data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
- data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
- data/lib/evilution/mutator/operator/float_literal.rb +22 -26
- data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
- data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
- data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
- data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
- data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
- data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
- data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
- data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
- data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
- data/lib/evilution/mutator/operator/string_literal.rb +18 -22
- data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
- data/lib/evilution/mutator/operator.rb +6 -0
- data/lib/evilution/mutator/registry.rb +54 -56
- data/lib/evilution/mutator.rb +4 -0
- data/lib/evilution/parallel/pool.rb +56 -58
- data/lib/evilution/parallel.rb +4 -0
- data/lib/evilution/reporter/cli.rb +99 -101
- data/lib/evilution/reporter/html.rb +242 -244
- data/lib/evilution/reporter/json.rb +57 -59
- data/lib/evilution/reporter/suggestion.rb +326 -328
- data/lib/evilution/reporter.rb +4 -0
- data/lib/evilution/result/mutation_result.rb +43 -46
- data/lib/evilution/result/summary.rb +80 -81
- data/lib/evilution/result.rb +4 -0
- data/lib/evilution/runner.rb +334 -323
- data/lib/evilution/session/store.rb +147 -0
- data/lib/evilution/session.rb +4 -0
- data/lib/evilution/spec_resolver.rb +49 -47
- data/lib/evilution/subject.rb +14 -16
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +13 -0
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07cd7bdd77da17dd201aa8e598707c326f4a9763ded14edfcd871a67de1ab2f5
|
|
4
|
+
data.tar.gz: 47b0a007f57cc93f1a143555db3600c92dc38a116b58004a9ce7d1e4a0aadd5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec518cde7ff3380d303ded60d7ea30b507d4b4d363d22211c06789de4cec6af8ad92e8b86e77a5a0d9551c9051cbf8afde4dc3eb261de8404260c22ada861648
|
|
7
|
+
data.tar.gz: 2e60811f5ebe09e8958024f8edbf2c1bd825dd82b5c1e94a044e097108141ecbba4c90dcee86a9c2934b53b3bab4c0afc0bcfb2080d337e4fc26675b46140231
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1774634346
|
data/.beads/issues.jsonl
CHANGED
|
@@ -58,24 +58,24 @@
|
|
|
58
58
|
{"id":"EV-145","title":"Add hooks configuration to .evilution.yml","description":"Allow hooks to be specified in .evilution.yml config file. Support both inline Ruby blocks and file paths to Ruby scripts. Example: hooks: { worker_process_start: 'config/evilution_hooks.rb' }.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:04.207200619+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:04.207200619+07:00","dependencies":[{"issue_id":"EV-145","depends_on_id":"EV-110","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
59
59
|
{"id":"EV-146","title":"Add RSpec suggestion templates for index access mutations","description":"Add concrete RSpec suggestion templates for survived index access mutations to the SuggestionReporter.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:11.229320728+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:11.229320728+07:00","dependencies":[{"issue_id":"EV-146","depends_on_id":"EV-131","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
60
60
|
{"id":"EV-147","title":"Implement pattern matching guard mutation","description":"Mutate guard clauses in pattern matching (in pattern if guard). Mutations: remove guard (always match), negate guard. Prism in_node with guard.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:12.368560689+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:12.368560689+07:00","dependencies":[{"issue_id":"EV-147","depends_on_id":"EV-141","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
61
|
-
{"id":"EV-148","title":"Session recording and history","description":"Store JSON results per run in .evilution/results/ with CLI commands to list, show, and garbage-collect past sessions. Mutant v1.0.0 has this. Enables trend analysis and regression detection across runs.","status":"
|
|
61
|
+
{"id":"EV-148","title":"Session recording and history","description":"Store JSON results per run in .evilution/results/ with CLI commands to list, show, and garbage-collect past sessions. Mutant v1.0.0 has this. Enables trend analysis and regression detection across runs.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:13.658761141+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T01:20:56.545069251+07:00","closed_at":"2026-03-28T01:20:56.545069251+07:00","close_reason":"GH #294 closed by user"}
|
|
62
62
|
{"id":"EV-149","title":"Add RSpec tests for hooks system","description":"Comprehensive test coverage for the hooks system: registration, dispatch, error isolation, configuration loading, all hook points firing at correct times.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:16.004153638+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:16.004153638+07:00","dependencies":[{"issue_id":"EV-149","depends_on_id":"EV-110","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
63
63
|
{"id":"EV-15","title":"Add line-range targeting (file:line-line syntax)","description":"Parse line-range syntax in CLI, store in Config, filter subjects in Runner. GH #29.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T00:36:29.164188342+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-08T00:38:02.739833414+07:00","closed_at":"2026-03-08T00:38:02.739833414+07:00","close_reason":"Implemented line-range targeting in CLI, Config, and Runner with full test coverage"}
|
|
64
|
-
{"id":"EV-150","title":"Implement session result storage","description":"After each mutation run, save a JSON file to .evilution/results/<timestamp>-<hash>.json containing:
|
|
64
|
+
{"id":"EV-150","title":"Implement session result storage","description":"After each mutation run (when --save-session is passed), save a JSON file to .evilution/results/<timestamp>-<hash>.json containing: version, timestamp, git context (branch, commit SHA), summary statistics, and survived mutation details.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:23.433170933+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-24T00:32:49.095176663+07:00","dependencies":[{"issue_id":"EV-150","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
65
65
|
{"id":"EV-151","title":"Implement pattern matching alternative mutation","description":"Mutate pattern alternatives (in pat1 | pat2). Mutations: remove each alternative individually, swap order. Prism alternation_pattern_node.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:23.447046732+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:23.447046732+07:00","dependencies":[{"issue_id":"EV-151","depends_on_id":"EV-141","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
66
66
|
{"id":"EV-152","title":"Epic: Type-aware return value mutations","description":"Add mutations that replace return values with type-appropriate empty/zero values: Array→[], Hash→{}, String→\"\", Integer→0, Float→0.0, true→false. Mutant does this extensively; Evilution only does body→nil.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:23.546834696+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:23.546834696+07:00"}
|
|
67
67
|
{"id":"EV-153","title":"Epic: Dynamic work allocation for parallel execution","description":"Replace static batch-based work distribution with a shared work queue for dynamic load balancing. Mutant uses a shared queue where workers pull the next mutation as they complete work. Evilution's current static batching can lead to uneven utilization when mutation execution times vary.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:26.946371704+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:26.946371704+07:00"}
|
|
68
68
|
{"id":"EV-154","title":"Implement type-aware return mutations for collection types","description":"When a method body returns an Array literal or Hash literal, also generate mutations returning the empty version ([] or {}). Detect by return value node type.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:31.525962391+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:31.525962391+07:00","dependencies":[{"issue_id":"EV-154","depends_on_id":"EV-152","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
69
69
|
{"id":"EV-155","title":"Implement pattern matching find/array pattern mutations","description":"Mutate find and array patterns: remove individual pattern elements, replace with wildcard (_). Prism find_pattern_node, array_pattern_node.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:33.136069529+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:33.136069529+07:00","dependencies":[{"issue_id":"EV-155","depends_on_id":"EV-141","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
70
|
-
{"id":"EV-156","title":"Add 'evilution session list' command","description":"Add a CLI subcommand that lists past session results with date, target, mutation count, score, and duration. Support --limit and --since filters.","status":"
|
|
70
|
+
{"id":"EV-156","title":"Add 'evilution session list' command","description":"Add a CLI subcommand that lists past session results with date, target, mutation count, score, and duration. Support --limit and --since filters.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:35.453549801+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-24T09:01:22.324682217+07:00","dependencies":[{"issue_id":"EV-156","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
71
71
|
{"id":"EV-157","title":"Implement shared work queue with pipe-based IPC","description":"Create a work queue that distributes mutations to worker processes dynamically. Use pipe-based IPC (or DRb) so workers pull the next mutation when ready, rather than receiving a pre-assigned batch.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:35.963687077+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:35.963687077+07:00","dependencies":[{"issue_id":"EV-157","depends_on_id":"EV-153","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
72
72
|
{"id":"EV-158","title":"Implement type-aware return mutations for scalar types","description":"When a method body returns a String, Integer, or Float literal, also generate mutations returning the zero/empty value ('', 0, 0.0). Detect by return value node type.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:43.59763546+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:43.59763546+07:00","dependencies":[{"issue_id":"EV-158","depends_on_id":"EV-152","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
73
73
|
{"id":"EV-159","title":"Add RSpec suggestion templates for pattern matching mutations","description":"Add concrete RSpec suggestion templates for survived pattern matching mutations.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:45.083995741+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:45.083995741+07:00","dependencies":[{"issue_id":"EV-159","depends_on_id":"EV-141","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
74
74
|
{"id":"EV-16","title":"Remove file-discovery logic from Integration::RSpec (GH #33)","description":"Integration::RSpec has detect_test_files, spec_file_candidates, and fallback_spec_dir that guess which specs to run. With precise targeting, agents can pass spec files directly. Simplify or remove this guessing logic, possibly adding a --spec flag.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T19:17:27.268579626+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T18:13:46.899195957+07:00","closed_at":"2026-03-09T18:13:46.899195957+07:00","close_reason":"Completed via PR #38 (tracked as EV-17)"}
|
|
75
|
-
{"id":"EV-160","title":"Add 'evilution session show' command","description":"Add a CLI subcommand that displays the full report for a specific past session, including all survived mutations with diffs.","status":"
|
|
75
|
+
{"id":"EV-160","title":"Add 'evilution session show' command","description":"Add a CLI subcommand that displays the full report for a specific past session, including all survived mutations with diffs.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:45.851348485+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-24T12:24:53.120184549+07:00","dependencies":[{"issue_id":"EV-160","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
76
76
|
{"id":"EV-161","title":"Add work-stealing for idle workers","description":"When a worker's local queue is empty, allow it to pull work from the shared queue immediately rather than waiting. Track per-worker completion rates for monitoring.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:47.426119432+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:47.426119432+07:00","dependencies":[{"issue_id":"EV-161","depends_on_id":"EV-153","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
77
77
|
{"id":"EV-162","title":"Add RSpec suggestion templates for type-aware return mutations","description":"Add concrete RSpec suggestion templates for survived type-aware return mutations to the SuggestionReporter.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:53.50501093+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:53.50501093+07:00","dependencies":[{"issue_id":"EV-162","depends_on_id":"EV-152","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
78
|
-
{"id":"EV-163","title":"Add 'evilution session gc' command","description":"Add a CLI subcommand to garbage-collect old session results. Support --older-than flag (e.g., --older-than 30d). Default to keeping all results.","status":"
|
|
78
|
+
{"id":"EV-163","title":"Add 'evilution session gc' command","description":"Add a CLI subcommand to garbage-collect old session results. Support --older-than flag (e.g., --older-than 30d). Default to keeping all results.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:56.398453511+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-24T13:45:03.75407328+07:00","dependencies":[{"issue_id":"EV-163","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
79
79
|
{"id":"EV-164","title":"Epic: AST pattern language for ignore_patterns","description":"Implement a pattern language for ignore_patterns configuration, similar to Mutant's (e.g., send{selector=log}, send{receiver=send{selector=logger}}). Allows precise, semantic exclusion of mutations on logging, debugging, or infrastructure code without requiring file/line targeting.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:56.583664516+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:56.583664516+07:00"}
|
|
80
80
|
{"id":"EV-165","title":"Update Parallel::Pool to use dynamic queue","description":"Refactor Parallel::Pool to use the new dynamic work queue instead of static batch distribution. Maintain backward compatibility with the existing API (results should be identical, just better distributed).","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:18:57.644578151+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:18:57.644578151+07:00","dependencies":[{"issue_id":"EV-165","depends_on_id":"EV-153","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
81
81
|
{"id":"EV-166","title":"Design AST pattern language syntax","description":"Design the pattern matching DSL syntax for ignore_patterns. Support at minimum: node type matching, attribute matching (selector, receiver), nested patterns, and wildcards. Document the syntax with examples.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:19:04.486430975+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:19:04.486430975+07:00","dependencies":[{"issue_id":"EV-166","depends_on_id":"EV-164","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -137,9 +137,9 @@
|
|
|
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":"open","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-23T11:22:20.07787144+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":"open","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-23T11:22:31.543822152+07:00","dependencies":[{"issue_id":"EV-205","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
139
139
|
{"id":"EV-206","title":"Epic: MCP server enhancements","description":"Enhance the MCP server with session history browsing, cross-run diffs, and test suggestion streaming. MCP integration is Evilution's unique competitive advantage — investing here widens the moat.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:41.847761146+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:41.847761146+07:00"}
|
|
140
|
-
{"id":"EV-207","title":"Add session history browsing to MCP server","description":"Add MCP tools for listing and viewing past session results. Enables AI agents to review mutation testing history without CLI.","status":"
|
|
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":"
|
|
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":"
|
|
140
|
+
{"id":"EV-207","title":"Add session history browsing to MCP server","description":"Add MCP tools for listing and viewing past session results. Enables AI agents to review mutation testing history without CLI.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:50.697338124+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T01:23:57.262720975+07:00","dependencies":[{"issue_id":"EV-207","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-207","depends_on_id":"EV-150","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
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
|
+
{"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
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
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"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Session result storage** (`--save-results`) — persist mutation run results as timestamped JSON files under `.evilution/results/`; enables cross-run comparison and history browsing (#298)
|
|
8
|
+
- **`evilution session list`** — CLI command to list saved session results with timestamps, scores, and mutation counts (#302)
|
|
9
|
+
- **`evilution session show`** — CLI command to display detailed session results including per-file mutation breakdown (#306)
|
|
10
|
+
- **`evilution session gc`** — CLI command for garbage collection of old session results; supports `--keep` flag to control retention count (#310)
|
|
11
|
+
- **MCP session history tools** — `evilution-session-list` and `evilution-session-show` MCP tools for AI agent browsing of session history (#353)
|
|
12
|
+
- **MCP cross-run diff tool** (`evilution-session-diff`) — compares two sessions and returns fixed mutations, new survivors, and persistent survivors (#354)
|
|
13
|
+
- **MCP streaming test suggestions** — survived mutations stream concrete RSpec suggestions via MCP progress notifications during execution (#355)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Compact class/module style** — all class and module declarations switched to compact style (e.g. `class Evilution::Session::Store`); intermediate module files added for standalone loading (#359)
|
|
18
|
+
- **Dependency updates** — mcp 0.9.0 → 0.9.1 (#375), rubocop 1.85.1 → 1.86.0 (#376)
|
|
19
|
+
|
|
3
20
|
## [0.13.0] - 2026-03-23
|
|
4
21
|
|
|
5
22
|
### Added
|
data/lib/evilution/ast/parser.rb
CHANGED
|
@@ -2,85 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
module Evilution
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
raise ParseError.new("cannot read #{file_path}: #{e.message}", file: file_path)
|
|
15
|
-
end
|
|
16
|
-
result = Prism.parse(source)
|
|
17
|
-
|
|
18
|
-
raise ParseError.new("failed to parse #{file_path}: #{result.errors.map(&:message).join(", ")}", file: file_path) if result.failure?
|
|
19
|
-
|
|
20
|
-
extract_subjects(result.value, source, file_path)
|
|
5
|
+
module Evilution::AST
|
|
6
|
+
class Parser
|
|
7
|
+
def call(file_path)
|
|
8
|
+
raise Evilution::ParseError.new("file not found: #{file_path}", file: file_path) unless File.exist?(file_path)
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
source = File.read(file_path)
|
|
12
|
+
rescue SystemCallError => e
|
|
13
|
+
raise Evilution::ParseError.new("cannot read #{file_path}: #{e.message}", file: file_path)
|
|
21
14
|
end
|
|
15
|
+
result = Prism.parse(source)
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
finder = SubjectFinder.new(source, file_path)
|
|
27
|
-
finder.visit(tree)
|
|
28
|
-
finder.subjects
|
|
17
|
+
if result.failure?
|
|
18
|
+
raise Evilution::ParseError.new("failed to parse #{file_path}: #{result.errors.map(&:message).join(", ")}",
|
|
19
|
+
file: file_path)
|
|
29
20
|
end
|
|
21
|
+
|
|
22
|
+
extract_subjects(result.value, source, file_path)
|
|
30
23
|
end
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
attr_reader :subjects
|
|
25
|
+
private
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
def extract_subjects(tree, source, file_path)
|
|
28
|
+
finder = SubjectFinder.new(source, file_path)
|
|
29
|
+
finder.visit(tree)
|
|
30
|
+
finder.subjects
|
|
31
|
+
end
|
|
32
|
+
end
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
super
|
|
45
|
-
@context.pop
|
|
46
|
-
end
|
|
34
|
+
class SubjectFinder < Prism::Visitor
|
|
35
|
+
attr_reader :subjects
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
def initialize(source, file_path)
|
|
38
|
+
@source = source
|
|
39
|
+
@file_path = file_path
|
|
40
|
+
@subjects = []
|
|
41
|
+
@context = []
|
|
42
|
+
end
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
44
|
+
def visit_module_node(node)
|
|
45
|
+
@context.push(constant_name(node.constant_path))
|
|
46
|
+
super
|
|
47
|
+
@context.pop
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_class_node(node)
|
|
51
|
+
@context.push(constant_name(node.constant_path))
|
|
52
|
+
super
|
|
53
|
+
@context.pop
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def visit_def_node(node)
|
|
57
|
+
scope = @context.join("::")
|
|
58
|
+
name = if scope.empty?
|
|
59
|
+
"##{node.name}"
|
|
60
|
+
else
|
|
61
|
+
"#{scope}##{node.name}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
loc = node.location
|
|
65
|
+
method_source = @source[loc.start_offset...loc.end_offset]
|
|
66
|
+
|
|
67
|
+
@subjects << Evilution::Subject.new(
|
|
68
|
+
name: name,
|
|
69
|
+
file_path: @file_path,
|
|
70
|
+
line_number: loc.start_line,
|
|
71
|
+
source: method_source,
|
|
72
|
+
node: node
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
super
|
|
76
|
+
end
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
private
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
end
|
|
80
|
+
def constant_name(node)
|
|
81
|
+
if node.respond_to?(:full_name)
|
|
82
|
+
node.full_name
|
|
83
|
+
else
|
|
84
|
+
node.name.to_s
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
end
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
11
|
-
end
|
|
3
|
+
require_relative "../ast"
|
|
4
|
+
|
|
5
|
+
module Evilution::AST::SourceSurgeon
|
|
6
|
+
def self.apply(source, offset:, length:, replacement:)
|
|
7
|
+
result = source.dup
|
|
8
|
+
result[offset, length] = replacement
|
|
9
|
+
result
|
|
12
10
|
end
|
|
13
11
|
end
|
data/lib/evilution/baseline.rb
CHANGED
|
@@ -2,99 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "spec_resolver"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
freeze
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def failed?
|
|
14
|
-
!failed_spec_files.empty?
|
|
15
|
-
end
|
|
5
|
+
class Evilution::Baseline
|
|
6
|
+
Result = Struct.new(:failed_spec_files, :duration) do
|
|
7
|
+
def initialize(**)
|
|
8
|
+
super
|
|
9
|
+
freeze
|
|
16
10
|
end
|
|
17
11
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
@timeout = timeout
|
|
12
|
+
def failed?
|
|
13
|
+
!failed_spec_files.empty?
|
|
21
14
|
end
|
|
15
|
+
end
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30)
|
|
18
|
+
@spec_resolver = spec_resolver
|
|
19
|
+
@timeout = timeout
|
|
20
|
+
end
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
def call(subjects)
|
|
23
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
+
spec_files = resolve_unique_spec_files(subjects)
|
|
25
|
+
failed = Set.new
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
spec_files.each do |spec_file|
|
|
28
|
+
failed.add(spec_file) unless run_spec_file(spec_file)
|
|
34
29
|
end
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
32
|
+
Result.new(failed_spec_files: failed, duration: duration)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run_spec_file(spec_file)
|
|
36
|
+
read_io, write_io = IO.pipe
|
|
37
|
+
pid = fork_spec_runner(spec_file, read_io, write_io)
|
|
38
|
+
write_io.close
|
|
39
|
+
read_result(read_io, pid)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
ensure
|
|
43
|
+
read_io&.close
|
|
44
|
+
write_io&.close
|
|
45
|
+
end
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
47
|
+
def fork_spec_runner(spec_file, read_io, write_io)
|
|
48
|
+
Process.fork do
|
|
49
|
+
read_io.close
|
|
50
|
+
$stdout.reopen(File::NULL, "w")
|
|
51
|
+
$stderr.reopen(File::NULL, "w")
|
|
52
|
+
|
|
53
|
+
require "rspec/core"
|
|
54
|
+
RSpec.reset
|
|
55
|
+
status = RSpec::Core::Runner.run(
|
|
56
|
+
["--format", "progress", "--no-color", "--order", "defined", spec_file]
|
|
57
|
+
)
|
|
58
|
+
Marshal.dump({ passed: status.zero? }, write_io)
|
|
59
|
+
write_io.close
|
|
60
|
+
exit!(status.zero? ? 0 : 1)
|
|
63
61
|
end
|
|
62
|
+
end
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
GRACE_PERIOD = 0.5
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
def read_result(read_io, pid)
|
|
67
|
+
if read_io.wait_readable(@timeout)
|
|
68
|
+
data = read_io.read
|
|
69
|
+
Process.wait(pid)
|
|
70
|
+
return false if data.empty?
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
72
|
+
result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
73
|
+
result[:passed]
|
|
74
|
+
else
|
|
75
|
+
terminate_child(pid)
|
|
76
|
+
false
|
|
79
77
|
end
|
|
78
|
+
end
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
def terminate_child(pid)
|
|
81
|
+
Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
82
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
83
|
+
return if status
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
sleep(GRACE_PERIOD)
|
|
86
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
87
|
+
return if status
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
90
|
+
Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
91
|
+
end
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
private
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
95
|
+
def resolve_unique_spec_files(subjects)
|
|
96
|
+
subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
|
|
99
97
|
end
|
|
100
98
|
end
|