evilution 0.12.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 +127 -0
  4. data/CHANGELOG.md +29 -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 +119 -0
  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 +18 -44
  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 -55
  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 -313
  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 +14 -0
  85. metadata +20 -2
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require "time"
6
+ require "fileutils"
7
+
8
+ require_relative "../session"
9
+
10
+ class Evilution::Session::Store
11
+ DEFAULT_DIR = ".evilution/results"
12
+
13
+ def initialize(results_dir: DEFAULT_DIR)
14
+ @results_dir = results_dir
15
+ end
16
+
17
+ def save(summary)
18
+ FileUtils.mkdir_p(@results_dir)
19
+
20
+ now = Time.now
21
+ data = build_session_data(summary, now)
22
+ filename = "#{format_timestamp(now)}-#{SecureRandom.hex(4)}.json"
23
+ path = File.join(@results_dir, filename)
24
+ atomic_write(path, JSON.pretty_generate(data))
25
+ path
26
+ end
27
+
28
+ def list
29
+ return [] unless Dir.exist?(@results_dir)
30
+
31
+ Dir
32
+ .glob(File.join(@results_dir, "*.json"))
33
+ .sort_by { |f| File.basename(f) }
34
+ .reverse
35
+ .filter_map { |f| build_list_entry(f) }
36
+ end
37
+
38
+ def load(path)
39
+ raise Evilution::Error, "session file not found: #{path}" unless File.exist?(path)
40
+
41
+ JSON.parse(File.read(path))
42
+ end
43
+
44
+ def gc(older_than:)
45
+ return [] unless Dir.exist?(@results_dir)
46
+
47
+ deleted = []
48
+ Dir.glob(File.join(@results_dir, "*.json")).each do |file|
49
+ timestamp = parse_filename_timestamp(File.basename(file))
50
+ next unless timestamp
51
+ next unless timestamp < older_than
52
+
53
+ File.delete(file)
54
+ deleted << file
55
+ end
56
+ deleted
57
+ end
58
+
59
+ private
60
+
61
+ def build_session_data(summary, now)
62
+ {
63
+ version: Evilution::VERSION,
64
+ timestamp: now.iso8601,
65
+ git: git_context,
66
+ summary: build_summary(summary),
67
+ survived: summary.survived_results.map { |r| build_mutation_detail(r) },
68
+ killed_count: summary.killed,
69
+ timed_out_count: summary.timed_out,
70
+ error_count: summary.errors,
71
+ neutral_count: summary.neutral,
72
+ equivalent_count: summary.equivalent
73
+ }
74
+ end
75
+
76
+ def build_summary(summary)
77
+ {
78
+ total: summary.total,
79
+ killed: summary.killed,
80
+ survived: summary.survived,
81
+ timed_out: summary.timed_out,
82
+ errors: summary.errors,
83
+ neutral: summary.neutral,
84
+ equivalent: summary.equivalent,
85
+ score: summary.score.round(4),
86
+ duration: summary.duration.round(4)
87
+ }
88
+ end
89
+
90
+ def build_mutation_detail(result)
91
+ mutation = result.mutation
92
+ {
93
+ operator: mutation.operator_name,
94
+ file: mutation.file_path,
95
+ line: mutation.line,
96
+ subject: mutation.subject.name,
97
+ diff: mutation.diff
98
+ }
99
+ end
100
+
101
+ def git_context
102
+ sha = `git rev-parse HEAD 2>/dev/null`.strip
103
+ branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
104
+ {
105
+ sha: sha.empty? ? nil : sha,
106
+ branch: branch.empty? ? nil : branch
107
+ }
108
+ end
109
+
110
+ def parse_filename_timestamp(basename)
111
+ match = basename.match(/\A(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/)
112
+ return nil unless match
113
+
114
+ Time.new(*match[1..6].map(&:to_i))
115
+ end
116
+
117
+ def format_timestamp(time)
118
+ time.strftime("%Y%m%dT%H%M%S")
119
+ end
120
+
121
+ def atomic_write(path, content)
122
+ temp_path = "#{path}.tmp-#{Process.pid}-#{SecureRandom.hex(4)}"
123
+ File.write(temp_path, content)
124
+ File.rename(temp_path, path)
125
+ rescue StandardError
126
+ FileUtils.rm_f(temp_path)
127
+ raise
128
+ end
129
+
130
+ def build_list_entry(file)
131
+ data = ::JSON.parse(File.read(file))
132
+ summary = data["summary"]
133
+ return nil unless data.is_a?(Hash) && summary.is_a?(Hash)
134
+
135
+ {
136
+ file: file,
137
+ timestamp: data["timestamp"],
138
+ total: summary["total"],
139
+ killed: summary["killed"],
140
+ survived: summary["survived"],
141
+ score: summary["score"],
142
+ duration: summary["duration"]
143
+ }
144
+ rescue ::JSON::ParserError, SystemCallError
145
+ nil
146
+ end
147
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Session
4
+ end
@@ -1,62 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- class SpecResolver
5
- STRIPPABLE_PREFIXES = %w[lib/ app/].freeze
3
+ class Evilution::SpecResolver
4
+ STRIPPABLE_PREFIXES = %w[lib/ app/].freeze
6
5
 
7
- def call(source_path)
8
- return nil if source_path.nil? || source_path.empty?
6
+ def call(source_path)
7
+ return nil if source_path.nil? || source_path.empty?
9
8
 
10
- normalized = normalize_path(source_path)
11
- candidates = candidate_spec_paths(normalized)
12
- candidates.find { |path| File.exist?(path) }
13
- end
9
+ normalized = normalize_path(source_path)
10
+ candidates = candidate_spec_paths(normalized)
11
+ candidates.find { |path| File.exist?(path) }
12
+ end
14
13
 
15
- def resolve_all(source_paths)
16
- Array(source_paths).filter_map { |path| call(path) }.uniq
17
- end
14
+ def resolve_all(source_paths)
15
+ Array(source_paths).filter_map { |path| call(path) }.uniq
16
+ end
18
17
 
19
- private
18
+ private
20
19
 
21
- def normalize_path(path)
22
- path = path.delete_prefix("./")
23
- path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
24
- path
25
- end
20
+ def normalize_path(path)
21
+ path = path.delete_prefix("./")
22
+ path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
23
+ path
24
+ end
26
25
 
27
- def candidate_spec_paths(source_path)
28
- base = source_path.sub(/\.rb\z/, "_spec.rb")
29
- prefix = STRIPPABLE_PREFIXES.find { |p| source_path.start_with?(p) }
26
+ def candidate_spec_paths(source_path)
27
+ base = source_path.sub(/\.rb\z/, "_spec.rb")
28
+ prefix = STRIPPABLE_PREFIXES.find { |p| source_path.start_with?(p) }
30
29
 
31
- candidates = if prefix
32
- stripped = base.delete_prefix(prefix)
33
- ["spec/#{stripped}", "spec/#{base}"]
34
- else
35
- ["spec/#{base}"]
36
- end
30
+ candidates = if prefix
31
+ stripped = base.delete_prefix(prefix)
32
+ ["spec/#{stripped}", "spec/#{base}"]
33
+ else
34
+ ["spec/#{base}"]
35
+ end
37
36
 
38
- fallbacks = candidates.flat_map { |c| parent_fallback_candidates(c) }.uniq
39
- candidates + fallbacks
40
- end
37
+ fallbacks = candidates.flat_map { |c| parent_fallback_candidates(c) }.uniq
41
38
 
42
- def parent_fallback_candidates(spec_path)
43
- parts = spec_path.split("/")
44
- # parts: ["spec", "foo", "bar_spec.rb"] — need at least 3 parts for fallback
45
- return [] if parts.length < 3
46
-
47
- candidates = []
48
- # Remove filename, then progressively remove directories
49
- dir_parts = parts[1..-2] # ["models", "game"]
50
- (dir_parts.length - 1).downto(0) do |i|
51
- file = "#{dir_parts[i]}_spec.rb"
52
- if i.zero?
53
- candidates << "spec/#{file}"
54
- else
55
- parent = dir_parts[0...i].join("/")
56
- candidates << "spec/#{parent}/#{file}"
57
- end
39
+ candidates + fallbacks
40
+ end
41
+
42
+ def parent_fallback_candidates(spec_path)
43
+ parts = spec_path.split("/")
44
+ # parts: ["spec", "foo", "bar_spec.rb"] — need at least 3 parts for fallback
45
+ return [] if parts.length < 3
46
+
47
+ candidates = []
48
+ # Remove filename, then progressively remove directories
49
+ dir_parts = parts[1..-2] # ["models", "game"]
50
+
51
+ (dir_parts.length - 1).downto(0) do |i|
52
+ file = "#{dir_parts[i]}_spec.rb"
53
+
54
+ if i.zero?
55
+ candidates << "spec/#{file}"
56
+ else
57
+ parent = dir_parts[0...i].join("/")
58
+ candidates << "spec/#{parent}/#{file}"
58
59
  end
59
- candidates
60
60
  end
61
+
62
+ candidates
61
63
  end
62
64
  end
@@ -1,23 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- class Subject
5
- attr_reader :name, :file_path, :line_number, :source, :node
3
+ class Evilution::Subject
4
+ attr_reader :name, :file_path, :line_number, :source, :node
6
5
 
7
- def initialize(name:, file_path:, line_number:, source:, node:)
8
- @name = name
9
- @file_path = file_path
10
- @line_number = line_number
11
- @source = source
12
- @node = node
13
- end
6
+ def initialize(name:, file_path:, line_number:, source:, node:)
7
+ @name = name
8
+ @file_path = file_path
9
+ @line_number = line_number
10
+ @source = source
11
+ @node = node
12
+ end
14
13
 
15
- def release_node!
16
- @node = nil
17
- end
14
+ def release_node!
15
+ @node = nil
16
+ end
18
17
 
19
- def to_s
20
- "#{name} (#{file_path}:#{line_number})"
21
- end
18
+ def to_s
19
+ "#{name} (#{file_path}:#{line_number})"
22
20
  end
23
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.12.0"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -4,10 +4,15 @@ require_relative "evilution/version"
4
4
  require_relative "evilution/memory"
5
5
  require_relative "evilution/config"
6
6
  require_relative "evilution/subject"
7
+ require_relative "evilution/result"
7
8
  require_relative "evilution/mutation"
9
+ require_relative "evilution/ast"
10
+ require_relative "evilution/parallel"
8
11
  require_relative "evilution/ast/source_surgeon"
9
12
  require_relative "evilution/ast/parser"
13
+ require_relative "evilution/mutator"
10
14
  require_relative "evilution/mutator/base"
15
+ require_relative "evilution/mutator/operator"
11
16
  require_relative "evilution/mutator/operator/comparison_replacement"
12
17
  require_relative "evilution/mutator/operator/boolean_literal_replacement"
13
18
  require_relative "evilution/mutator/operator/integer_literal"
@@ -35,16 +40,25 @@ require_relative "evilution/mutator/operator/regexp_mutation"
35
40
  require_relative "evilution/mutator/operator/receiver_replacement"
36
41
  require_relative "evilution/mutator/operator/send_mutation"
37
42
  require_relative "evilution/mutator/operator/argument_nil_substitution"
43
+ require_relative "evilution/mutator/operator/compound_assignment"
38
44
  require_relative "evilution/mutator/registry"
45
+ require_relative "evilution/equivalent"
46
+ require_relative "evilution/equivalent/heuristic"
39
47
  require_relative "evilution/equivalent/detector"
48
+ require_relative "evilution/isolation"
40
49
  require_relative "evilution/isolation/fork"
41
50
  require_relative "evilution/isolation/in_process"
42
51
  require_relative "evilution/parallel/pool"
52
+ require_relative "evilution/session"
53
+ require_relative "evilution/session/store"
54
+ require_relative "evilution/git"
43
55
  require_relative "evilution/git/changed_files"
56
+ require_relative "evilution/integration"
44
57
  require_relative "evilution/integration/base"
45
58
  require_relative "evilution/integration/rspec"
46
59
  require_relative "evilution/result/mutation_result"
47
60
  require_relative "evilution/result/summary"
61
+ require_relative "evilution/reporter"
48
62
  require_relative "evilution/reporter/json"
49
63
  require_relative "evilution/reporter/cli"
50
64
  require_relative "evilution/reporter/html"
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.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-22 00:00:00.000000000 Z
11
+ date: 2026-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -78,28 +78,40 @@ files:
78
78
  - claude-swarm.yml
79
79
  - exe/evilution
80
80
  - lib/evilution.rb
81
+ - lib/evilution/ast.rb
81
82
  - lib/evilution/ast/parser.rb
82
83
  - lib/evilution/ast/source_surgeon.rb
83
84
  - lib/evilution/baseline.rb
84
85
  - lib/evilution/cache.rb
85
86
  - lib/evilution/cli.rb
86
87
  - lib/evilution/config.rb
88
+ - lib/evilution/equivalent.rb
87
89
  - lib/evilution/equivalent/detector.rb
90
+ - lib/evilution/equivalent/heuristic.rb
88
91
  - lib/evilution/equivalent/heuristic/alias_swap.rb
89
92
  - lib/evilution/equivalent/heuristic/dead_code.rb
90
93
  - lib/evilution/equivalent/heuristic/method_body_nil.rb
91
94
  - lib/evilution/equivalent/heuristic/noop_source.rb
95
+ - lib/evilution/git.rb
92
96
  - lib/evilution/git/changed_files.rb
97
+ - lib/evilution/integration.rb
93
98
  - lib/evilution/integration/base.rb
94
99
  - lib/evilution/integration/rspec.rb
100
+ - lib/evilution/isolation.rb
95
101
  - lib/evilution/isolation/fork.rb
96
102
  - lib/evilution/isolation/in_process.rb
103
+ - lib/evilution/mcp.rb
97
104
  - lib/evilution/mcp/mutate_tool.rb
98
105
  - lib/evilution/mcp/server.rb
106
+ - lib/evilution/mcp/session_diff_tool.rb
107
+ - lib/evilution/mcp/session_list_tool.rb
108
+ - lib/evilution/mcp/session_show_tool.rb
99
109
  - lib/evilution/memory.rb
100
110
  - lib/evilution/memory/leak_check.rb
101
111
  - lib/evilution/mutation.rb
112
+ - lib/evilution/mutator.rb
102
113
  - lib/evilution/mutator/base.rb
114
+ - lib/evilution/mutator/operator.rb
103
115
  - lib/evilution/mutator/operator/argument_nil_substitution.rb
104
116
  - lib/evilution/mutator/operator/argument_removal.rb
105
117
  - lib/evilution/mutator/operator/arithmetic_replacement.rb
@@ -109,6 +121,7 @@ files:
109
121
  - lib/evilution/mutator/operator/boolean_operator_replacement.rb
110
122
  - lib/evilution/mutator/operator/collection_replacement.rb
111
123
  - lib/evilution/mutator/operator/comparison_replacement.rb
124
+ - lib/evilution/mutator/operator/compound_assignment.rb
112
125
  - lib/evilution/mutator/operator/conditional_branch.rb
113
126
  - lib/evilution/mutator/operator/conditional_flip.rb
114
127
  - lib/evilution/mutator/operator/conditional_negation.rb
@@ -128,14 +141,19 @@ files:
128
141
  - lib/evilution/mutator/operator/string_literal.rb
129
142
  - lib/evilution/mutator/operator/symbol_literal.rb
130
143
  - lib/evilution/mutator/registry.rb
144
+ - lib/evilution/parallel.rb
131
145
  - lib/evilution/parallel/pool.rb
146
+ - lib/evilution/reporter.rb
132
147
  - lib/evilution/reporter/cli.rb
133
148
  - lib/evilution/reporter/html.rb
134
149
  - lib/evilution/reporter/json.rb
135
150
  - lib/evilution/reporter/suggestion.rb
151
+ - lib/evilution/result.rb
136
152
  - lib/evilution/result/mutation_result.rb
137
153
  - lib/evilution/result/summary.rb
138
154
  - lib/evilution/runner.rb
155
+ - lib/evilution/session.rb
156
+ - lib/evilution/session/store.rb
139
157
  - lib/evilution/spec_resolver.rb
140
158
  - lib/evilution/subject.rb
141
159
  - lib/evilution/version.rb