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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +8 -8
  4. data/CHANGELOG.md +17 -0
  5. data/lib/evilution/ast/parser.rb +69 -68
  6. data/lib/evilution/ast/source_surgeon.rb +7 -9
  7. data/lib/evilution/ast.rb +4 -0
  8. data/lib/evilution/baseline.rb +73 -75
  9. data/lib/evilution/cache.rb +75 -77
  10. data/lib/evilution/cli.rb +408 -173
  11. data/lib/evilution/config.rb +141 -136
  12. data/lib/evilution/equivalent/detector.rb +25 -27
  13. data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
  14. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  15. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  16. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  17. data/lib/evilution/equivalent/heuristic.rb +6 -0
  18. data/lib/evilution/equivalent.rb +4 -0
  19. data/lib/evilution/git/changed_files.rb +35 -37
  20. data/lib/evilution/git.rb +4 -0
  21. data/lib/evilution/integration/base.rb +5 -7
  22. data/lib/evilution/integration/rspec.rb +114 -116
  23. data/lib/evilution/integration.rb +4 -0
  24. data/lib/evilution/isolation/fork.rb +98 -100
  25. data/lib/evilution/isolation/in_process.rb +59 -61
  26. data/lib/evilution/isolation.rb +4 -0
  27. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  28. data/lib/evilution/mcp/server.rb +12 -11
  29. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  30. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  31. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  32. data/lib/evilution/mcp.rb +4 -0
  33. data/lib/evilution/memory/leak_check.rb +80 -84
  34. data/lib/evilution/memory.rb +34 -36
  35. data/lib/evilution/mutation.rb +40 -42
  36. data/lib/evilution/mutator/base.rb +46 -48
  37. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  38. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  39. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  40. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  41. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  42. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  43. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  44. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  45. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  46. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  47. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  48. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  49. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  50. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  51. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  52. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  53. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  54. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  55. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  56. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  57. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  58. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  59. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  60. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  61. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  62. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  63. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  64. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  65. data/lib/evilution/mutator/operator.rb +6 -0
  66. data/lib/evilution/mutator/registry.rb +54 -56
  67. data/lib/evilution/mutator.rb +4 -0
  68. data/lib/evilution/parallel/pool.rb +56 -58
  69. data/lib/evilution/parallel.rb +4 -0
  70. data/lib/evilution/reporter/cli.rb +99 -101
  71. data/lib/evilution/reporter/html.rb +242 -244
  72. data/lib/evilution/reporter/json.rb +57 -59
  73. data/lib/evilution/reporter/suggestion.rb +326 -328
  74. data/lib/evilution/reporter.rb +4 -0
  75. data/lib/evilution/result/mutation_result.rb +43 -46
  76. data/lib/evilution/result/summary.rb +80 -81
  77. data/lib/evilution/result.rb +4 -0
  78. data/lib/evilution/runner.rb +334 -323
  79. data/lib/evilution/session/store.rb +147 -0
  80. data/lib/evilution/session.rb +4 -0
  81. data/lib/evilution/spec_resolver.rb +49 -47
  82. data/lib/evilution/subject.rb +14 -16
  83. data/lib/evilution/version.rb +1 -1
  84. data/lib/evilution.rb +13 -0
  85. metadata +19 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f21d98f9e9e791ae8876b416b88ecc55ac6f2503ab1841591f528a5901a463
4
- data.tar.gz: b9a32a1147ae03ad7cc0b81ad3b10cfee74376de195dbd67170e50aa484c8992
3
+ metadata.gz: 07cd7bdd77da17dd201aa8e598707c326f4a9763ded14edfcd871a67de1ab2f5
4
+ data.tar.gz: 47b0a007f57cc93f1a143555db3600c92dc38a116b58004a9ce7d1e4a0aadd5c
5
5
  SHA512:
6
- metadata.gz: eb819003413795c1cc25a257ef93a505cdf0f5975059340fd85ab228eb13e94e63f75e856179d17bbc264a04e93044d265454156518d7121cab26faed67c14ae
7
- data.tar.gz: 3e5991bbb1c4b3b6f0c94105ba78a6c01ea74561f9c3cb7757ffae52682a414af8369598e919825203e824f538a9055721df4d8072f59977259e08a1080a28b4
6
+ metadata.gz: ec518cde7ff3380d303ded60d7ea30b507d4b4d363d22211c06789de4cec6af8ad92e8b86e77a5a0d9551c9051cbf8afde4dc3eb261de8404260c22ada861648
7
+ data.tar.gz: 2e60811f5ebe09e8958024f8edbf2c1bd825dd82b5c1e94a044e097108141ecbba4c90dcee86a9c2934b53b3bab4c0afc0bcfb2080d337e4fc26675b46140231
@@ -1 +1 @@
1
- 1774239252
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":"open","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-23T11:18:13.658761141+07:00"}
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: run configuration, all mutation results, summary statistics, duration, and git context (branch, commit SHA).","status":"open","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-23T11:18:23.433170933+07:00","dependencies":[{"issue_id":"EV-150","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
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":"open","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-23T11:18:35.453549801+07:00","dependencies":[{"issue_id":"EV-156","depends_on_id":"EV-148","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":"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":"open","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-23T11:18:45.851348485+07:00","dependencies":[{"issue_id":"EV-160","depends_on_id":"EV-148","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
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":"open","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-23T11:18:56.398453511+07:00","dependencies":[{"issue_id":"EV-163","depends_on_id":"EV-148","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":"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":"open","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-23T11:22:50.697338124+07:00","dependencies":[{"issue_id":"EV-207","depends_on_id":"EV-206","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":"open","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-23T11:23:00.558002099+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":"open","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-23T11:23:10.316219498+07:00","dependencies":[{"issue_id":"EV-209","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
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
@@ -2,85 +2,86 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- module Evilution
6
- module AST
7
- class Parser
8
- def call(file_path)
9
- raise ParseError.new("file not found: #{file_path}", file: file_path) unless File.exist?(file_path)
10
-
11
- begin
12
- source = File.read(file_path)
13
- rescue SystemCallError => e
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
- private
24
-
25
- def extract_subjects(tree, source, file_path)
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
- class SubjectFinder < Prism::Visitor
33
- attr_reader :subjects
25
+ private
34
26
 
35
- def initialize(source, file_path)
36
- @source = source
37
- @file_path = file_path
38
- @subjects = []
39
- @context = []
40
- end
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
- def visit_module_node(node)
43
- @context.push(constant_name(node.constant_path))
44
- super
45
- @context.pop
46
- end
34
+ class SubjectFinder < Prism::Visitor
35
+ attr_reader :subjects
47
36
 
48
- def visit_class_node(node)
49
- @context.push(constant_name(node.constant_path))
50
- super
51
- @context.pop
52
- end
37
+ def initialize(source, file_path)
38
+ @source = source
39
+ @file_path = file_path
40
+ @subjects = []
41
+ @context = []
42
+ end
53
43
 
54
- def visit_def_node(node)
55
- scope = @context.join("::")
56
- name = if scope.empty?
57
- "##{node.name}"
58
- else
59
- "#{scope}##{node.name}"
60
- end
61
-
62
- loc = node.location
63
- method_source = @source[loc.start_offset...loc.end_offset]
64
-
65
- @subjects << Subject.new(
66
- name: name,
67
- file_path: @file_path,
68
- line_number: loc.start_line,
69
- source: method_source,
70
- node: node
71
- )
72
-
73
- super
74
- end
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
- private
78
+ private
77
79
 
78
- def constant_name(node)
79
- if node.respond_to?(:full_name)
80
- node.full_name
81
- else
82
- node.name.to_s
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
- module Evilution
4
- module AST
5
- module SourceSurgeon
6
- def self.apply(source, offset:, length:, replacement:)
7
- result = source.dup
8
- result[offset, length] = replacement
9
- result
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::AST
4
+ end
@@ -2,99 +2,97 @@
2
2
 
3
3
  require_relative "spec_resolver"
4
4
 
5
- module Evilution
6
- class Baseline
7
- Result = Struct.new(:failed_spec_files, :duration) do
8
- def initialize(**)
9
- super
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 initialize(spec_resolver: SpecResolver.new, timeout: 30)
19
- @spec_resolver = spec_resolver
20
- @timeout = timeout
12
+ def failed?
13
+ !failed_spec_files.empty?
21
14
  end
15
+ end
22
16
 
23
- def call(subjects)
24
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
- spec_files = resolve_unique_spec_files(subjects)
26
- failed = Set.new
17
+ def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30)
18
+ @spec_resolver = spec_resolver
19
+ @timeout = timeout
20
+ end
27
21
 
28
- spec_files.each do |spec_file|
29
- failed.add(spec_file) unless run_spec_file(spec_file)
30
- end
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
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
33
- Result.new(failed_spec_files: failed, duration: duration)
27
+ spec_files.each do |spec_file|
28
+ failed.add(spec_file) unless run_spec_file(spec_file)
34
29
  end
35
30
 
36
- def run_spec_file(spec_file)
37
- read_io, write_io = IO.pipe
38
- pid = fork_spec_runner(spec_file, read_io, write_io)
39
- write_io.close
40
- read_result(read_io, pid)
41
- rescue StandardError
42
- false
43
- ensure
44
- read_io&.close
45
- write_io&.close
46
- end
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
- def fork_spec_runner(spec_file, read_io, write_io)
49
- Process.fork do
50
- read_io.close
51
- $stdout.reopen(File::NULL, "w")
52
- $stderr.reopen(File::NULL, "w")
53
-
54
- require "rspec/core"
55
- ::RSpec.reset
56
- status = ::RSpec::Core::Runner.run(
57
- ["--format", "progress", "--no-color", "--order", "defined", spec_file]
58
- )
59
- Marshal.dump({ passed: status.zero? }, write_io)
60
- write_io.close
61
- exit!(status.zero? ? 0 : 1)
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
- GRACE_PERIOD = 0.5
64
+ GRACE_PERIOD = 0.5
66
65
 
67
- def read_result(read_io, pid)
68
- if read_io.wait_readable(@timeout)
69
- data = read_io.read
70
- Process.wait(pid)
71
- return false if data.empty?
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
- result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
74
- result[:passed]
75
- else
76
- terminate_child(pid)
77
- false
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
- def terminate_child(pid)
82
- Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
83
- _, status = Process.waitpid2(pid, Process::WNOHANG)
84
- return if status
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
- sleep(GRACE_PERIOD)
87
- _, status = Process.waitpid2(pid, Process::WNOHANG)
88
- return if status
85
+ sleep(GRACE_PERIOD)
86
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
87
+ return if status
89
88
 
90
- Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
91
- Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
92
- end
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
- private
93
+ private
95
94
 
96
- def resolve_unique_spec_files(subjects)
97
- subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
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