evilution 0.24.0 → 0.26.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +210 -0
  3. data/.claude/prompts/architect.md +14 -1
  4. data/.claude/skills/create-issue/SKILL.md +55 -0
  5. data/CHANGELOG.md +51 -0
  6. data/README.md +80 -4
  7. data/exe/evil +6 -0
  8. data/lib/evilution/ast/constant_names.rb +34 -0
  9. data/lib/evilution/ast/source_surgeon.rb +15 -1
  10. data/lib/evilution/cli/commands/compare.rb +68 -0
  11. data/lib/evilution/cli/parser/command_extractor.rb +2 -1
  12. data/lib/evilution/cli/parser/options_builder.rb +21 -1
  13. data/lib/evilution/cli/printers/compare.rb +159 -0
  14. data/lib/evilution/cli.rb +1 -0
  15. data/lib/evilution/compare/categorizer.rb +109 -0
  16. data/lib/evilution/compare/detector.rb +21 -0
  17. data/lib/evilution/compare/fingerprint.rb +83 -0
  18. data/lib/evilution/compare/invalid_input.rb +12 -0
  19. data/lib/evilution/compare/normalizer.rb +106 -0
  20. data/lib/evilution/compare/record.rb +16 -0
  21. data/lib/evilution/compare.rb +6 -0
  22. data/lib/evilution/config.rb +165 -3
  23. data/lib/evilution/example_filter.rb +143 -0
  24. data/lib/evilution/integration/base.rb +4 -155
  25. data/lib/evilution/integration/crash_detector.rb +5 -2
  26. data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
  27. data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
  28. data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
  29. data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
  30. data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
  31. data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
  32. data/lib/evilution/integration/loading.rb +6 -0
  33. data/lib/evilution/integration/minitest.rb +10 -5
  34. data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
  35. data/lib/evilution/integration/rspec.rb +82 -7
  36. data/lib/evilution/isolation/fork.rb +25 -0
  37. data/lib/evilution/load_path/subpath_resolver.rb +25 -0
  38. data/lib/evilution/load_path.rb +4 -0
  39. data/lib/evilution/mcp/info_tool.rb +77 -5
  40. data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
  41. data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
  42. data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
  43. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
  44. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
  45. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
  46. data/lib/evilution/mcp/mutate_tool.rb +34 -186
  47. data/lib/evilution/mutation.rb +43 -3
  48. data/lib/evilution/mutator/base.rb +39 -1
  49. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
  50. data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
  51. data/lib/evilution/parallel/work_queue.rb +149 -31
  52. data/lib/evilution/parallel_db_warning.rb +68 -0
  53. data/lib/evilution/reporter/cli.rb +37 -11
  54. data/lib/evilution/reporter/html/assets/style.css +17 -0
  55. data/lib/evilution/reporter/html/sections/file_section.rb +15 -0
  56. data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
  57. data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
  58. data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
  59. data/lib/evilution/reporter/html/templates/file_section.html.erb +3 -0
  60. data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
  61. data/lib/evilution/reporter/html/templates/summary_cards.html.erb +3 -0
  62. data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
  63. data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
  64. data/lib/evilution/reporter/json.rb +8 -2
  65. data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
  66. data/lib/evilution/reporter/suggestion/registry.rb +64 -0
  67. data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
  68. data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
  69. data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
  70. data/lib/evilution/reporter/suggestion.rb +8 -1327
  71. data/lib/evilution/result/mutation_result.rb +5 -1
  72. data/lib/evilution/result/summary.rb +13 -1
  73. data/lib/evilution/runner/baseline_runner.rb +23 -2
  74. data/lib/evilution/runner/isolation_resolver.rb +12 -1
  75. data/lib/evilution/runner/mutation_executor.rb +83 -13
  76. data/lib/evilution/runner/subject_pipeline.rb +18 -8
  77. data/lib/evilution/runner.rb +6 -0
  78. data/lib/evilution/source_ast_cache.rb +39 -0
  79. data/lib/evilution/spec_ast_cache.rb +166 -0
  80. data/lib/evilution/spec_resolver.rb +6 -1
  81. data/lib/evilution/spec_selector.rb +39 -0
  82. data/lib/evilution/temp_dir_tracker.rb +23 -3
  83. data/lib/evilution/version.rb +1 -1
  84. data/script/memory_check +7 -5
  85. metadata +46 -5
  86. data/lib/evilution/mcp/session_diff_tool.rb +0 -63
  87. data/lib/evilution/mcp/session_list_tool.rb +0 -50
  88. data/lib/evilution/mcp/session_show_tool.rb +0 -57
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::UnparseableDetails < Evilution::Reporter::HTML::Section
6
+ template "unparseable_details"
7
+
8
+ def self.render_if(unparseable)
9
+ return "" if unparseable.empty?
10
+
11
+ new(unparseable).render
12
+ end
13
+
14
+ def initialize(unparseable)
15
+ @unparseable = unparseable
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :unparseable
21
+
22
+ def sorted
23
+ unparseable.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sections"
4
+
5
+ class Evilution::Reporter::HTML::Sections::UnresolvedDetails < Evilution::Reporter::HTML::Section
6
+ template "unresolved_details"
7
+
8
+ def self.render_if(unresolved)
9
+ return "" if unresolved.empty?
10
+
11
+ new(unresolved).render
12
+ end
13
+
14
+ def initialize(unresolved)
15
+ @unresolved = unresolved
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :unresolved
21
+
22
+ def sorted
23
+ unresolved.sort_by { |r| [r.mutation.operator_name, r.mutation.line] }
24
+ end
25
+ end
@@ -6,4 +6,7 @@
6
6
  <div class="mutation-map"><%= map_html %></div>
7
7
  <%= survived_html %>
8
8
  <%= error_html %>
9
+ <%= neutral_html %>
10
+ <%= unresolved_html %>
11
+ <%= unparseable_html %>
9
12
  </section>
@@ -0,0 +1,14 @@
1
+ <div class="neutral-details">
2
+ <h3>Neutral (<%= neutral.length %>) — test infra error, excluded from score</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ <%- if r.error_class -%>
9
+ <span class="error-class">(<%= h(r.error_class) %>)</span>
10
+ <%- end -%>
11
+ </li>
12
+ <%- end -%>
13
+ </ul>
14
+ </div>
@@ -9,6 +9,9 @@
9
9
  <%- if @summary.unresolved.positive? -%>
10
10
  <div class="card"><span class="card-value"><%= @summary.unresolved %></span><span class="card-label">Unresolved</span></div>
11
11
  <%- end -%>
12
+ <%- if @summary.unparseable.positive? -%>
13
+ <div class="card"><span class="card-value"><%= @summary.unparseable %></span><span class="card-label">Unparseable</span></div>
14
+ <%- end -%>
12
15
  <%- if @summary.skipped.positive? -%>
13
16
  <div class="card"><span class="card-value"><%= @summary.skipped %></span><span class="card-label">Skipped</span></div>
14
17
  <%- end -%>
@@ -0,0 +1,11 @@
1
+ <div class="unparseable-details">
2
+ <h3>Unparseable (<%= unparseable.length %>) — mutated source did not parse</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ </li>
9
+ <%- end -%>
10
+ </ul>
11
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="unresolved-details">
2
+ <h3>Unresolved (<%= unresolved.length %>) — no test file resolved</h3>
3
+ <ul>
4
+ <%- sorted.each do |r| -%>
5
+ <li>
6
+ <span class="operator"><%= h(r.mutation.operator_name) %></span>
7
+ <span class="location"><%= h(r.mutation.file_path) %>:<%= r.mutation.line %></span>
8
+ </li>
9
+ <%- end -%>
10
+ </ul>
11
+ </div>
@@ -29,7 +29,8 @@ class Evilution::Reporter::JSON
29
29
  timed_out: map_details(summary.results.select(&:timeout?)),
30
30
  errors: map_details(summary.results.select(&:error?)),
31
31
  equivalent: map_details(summary.equivalent_results),
32
- unresolved: map_details(summary.unresolved_results)
32
+ unresolved: map_details(summary.unresolved_results),
33
+ unparseable: map_details(summary.unparseable_results)
33
34
  }
34
35
  append_disabled_to_report(report, summary)
35
36
  report
@@ -55,6 +56,7 @@ class Evilution::Reporter::JSON
55
56
  neutral: summary.neutral,
56
57
  equivalent: summary.equivalent,
57
58
  unresolved: summary.unresolved,
59
+ unparseable: summary.unparseable,
58
60
  score: summary.score.round(4),
59
61
  duration: summary.duration.round(4),
60
62
  killtime: summary.killtime.round(4),
@@ -78,7 +80,11 @@ class Evilution::Reporter::JSON
78
80
  duration: result.duration.round(4),
79
81
  diff: mutation.diff
80
82
  }
81
- detail[:suggestion] = @suggestion.suggestion_for(mutation) if result.status == :survived
83
+ if result.status == :survived
84
+ detail[:suggestion] = @suggestion.suggestion_for(mutation)
85
+ unified = mutation.unified_diff
86
+ detail[:unified_diff] = unified if unified
87
+ end
82
88
  detail[:test_command] = result.test_command if result.test_command
83
89
  append_memory_fields(detail, result)
84
90
  append_error_fields(detail, result)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../suggestion"
4
+
5
+ module Evilution::Reporter::Suggestion::DiffHelpers
6
+ module_function
7
+
8
+ def parse_method_name(subject_name)
9
+ subject_name.split(/[#.]/).last
10
+ end
11
+
12
+ def sanitize_method_name(name)
13
+ name.gsub(/[^a-zA-Z0-9_]/, "_").gsub(/_+/, "_").gsub(/\A_|_\z/, "")
14
+ end
15
+
16
+ def extract_diff_lines(diff)
17
+ lines = diff.split("\n")
18
+ original = lines.find { |l| l.start_with?("- ") }
19
+ mutated = lines.find { |l| l.start_with?("+ ") }
20
+ [clean_diff_line(original, "- "), clean_diff_line(mutated, "+ ")]
21
+ end
22
+
23
+ def clean_diff_line(line, prefix)
24
+ return nil if line.nil?
25
+
26
+ line.sub(/^#{Regexp.escape(prefix)}/, "").strip
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../suggestion"
4
+
5
+ # rubocop:disable Style/OneClassPerFile
6
+ module Evilution::Reporter::Suggestion::Templates
7
+ end
8
+
9
+ class Evilution::Reporter::Suggestion::Registry
10
+ # rubocop:enable Style/OneClassPerFile
11
+ def self.default
12
+ return @default if @default
13
+
14
+ require_relative "templates/generic"
15
+ require_relative "templates/rspec"
16
+ require_relative "templates/minitest"
17
+
18
+ registry = new
19
+ Evilution::Reporter::Suggestion::Templates::Generic::GENERIC_ENTRIES.each do |op, text|
20
+ registry.register_generic(op, text)
21
+ end
22
+ Evilution::Reporter::Suggestion::Templates::Rspec::RSPEC_ENTRIES.each do |op, blk|
23
+ registry.register_concrete(op, integration: :rspec, block: blk)
24
+ end
25
+ Evilution::Reporter::Suggestion::Templates::Minitest::MINITEST_ENTRIES.each do |op, blk|
26
+ registry.register_concrete(op, integration: :minitest, block: blk)
27
+ end
28
+
29
+ @default = registry
30
+ end
31
+
32
+ def self.reset!
33
+ @default = nil
34
+ end
35
+
36
+ def initialize
37
+ @generic = {}
38
+ @concrete = Hash.new { |h, k| h[k] = {} }
39
+ end
40
+
41
+ def register_generic(operator_name, text)
42
+ @generic[operator_name] = text
43
+ self
44
+ end
45
+
46
+ def register_concrete(operator_name, integration:, block:)
47
+ @concrete[integration][operator_name] = block
48
+ self
49
+ end
50
+
51
+ def generic(operator_name)
52
+ @generic[operator_name]
53
+ end
54
+
55
+ def concrete(operator_name, integration:)
56
+ @concrete.fetch(integration, {})[operator_name]
57
+ end
58
+
59
+ def each_generic_operator(&)
60
+ return @generic.each_key unless block_given?
61
+
62
+ @generic.each_key(&)
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../registry"
4
+
5
+ module Evilution::Reporter::Suggestion::Templates::Generic
6
+ GENERIC_ENTRIES = {
7
+ "comparison_replacement" => "Add a test for the boundary condition where the comparison operand equals the threshold exactly",
8
+ "arithmetic_replacement" => "Add a test that verifies the arithmetic result, not just truthiness of the outcome",
9
+ "boolean_operator_replacement" => "Add a test where only one of the boolean conditions is true to distinguish && from ||",
10
+ "boolean_literal_replacement" => "Add a test that exercises the false/true branch explicitly",
11
+ "nil_replacement" => "Add a test that asserts the return value is not nil",
12
+ "integer_literal" => "Add a test that checks the exact numeric value, not just > 0 or truthy",
13
+ "float_literal" => "Add a test that checks the exact floating-point value returned",
14
+ "string_literal" => "Add a test that asserts the string content, not just its presence",
15
+ "array_literal" => "Add a test that verifies the array contents or length",
16
+ "hash_literal" => "Add a test that verifies the hash keys and values",
17
+ "symbol_literal" => "Add a test that checks the exact symbol returned",
18
+ "conditional_negation" => "Add tests for both the true and false branches of this conditional",
19
+ "conditional_branch" => "Add a test that exercises the removed branch of this conditional",
20
+ "statement_deletion" => "Add a test that depends on the side effect of this statement",
21
+ "method_body_replacement" => "Add a test that checks the method's return value or side effects",
22
+ "negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
23
+ "return_value_removal" => "Add a test that uses the return value of this method",
24
+ "collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
25
+ "method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
26
+ "argument_removal" => "Add a test that verifies the correct arguments are passed to this method call",
27
+ "compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)",
28
+ "superclass_removal" => "Add a test that exercises inherited behavior from the superclass",
29
+ "mixin_removal" => "Add a test that exercises behavior provided by the included/extended module",
30
+ "local_variable_assignment" => "Add a test that depends on the assigned variable being stored, not just the value expression",
31
+ "instance_variable_write" => "Add a test that verifies the instance variable is set correctly, not just the return value",
32
+ "class_variable_write" => "Add a test that verifies the class variable is set correctly and affects shared state",
33
+ "global_variable_write" => "Add a test that verifies the global variable is set correctly, not just the value expression",
34
+ "rescue_removal" => "Add a test that triggers the rescued exception and verifies the rescue handler behavior",
35
+ "rescue_body_replacement" => "Add a test that triggers the rescued exception and verifies the rescue body produces the correct result",
36
+ "inline_rescue" => "Add a test that triggers the inline rescue and verifies the fallback value is used correctly",
37
+ "ensure_removal" => "Add a test that verifies the ensure cleanup code runs and its side effects are observable",
38
+ "break_statement" => "Add a test that verifies the break condition and the value returned when the loop exits early",
39
+ "next_statement" => "Add a test that verifies the next condition and the value yielded when the iteration skips",
40
+ "redo_statement" => "Add a test that verifies the redo restarts the iteration and the retry logic is necessary",
41
+ "bang_method" => "Add a test that distinguishes in-place mutation from copy semantics (bang vs non-bang)",
42
+ "bitwise_replacement" => "Add a test that checks the exact bitwise result to distinguish &, |, and ^ operators",
43
+ "bitwise_complement" => "Add a test that verifies the bitwise complement (~) result, not just the sign or magnitude",
44
+ "zsuper_removal" => "Add a test that verifies inherited behavior from super is needed, not just the subclass logic",
45
+ "explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters",
46
+ "index_to_fetch" => "Add a test that distinguishes [] (returns nil for missing keys) from .fetch (raises KeyError)",
47
+ "index_to_dig" => "Add a test that verifies chained [] access returns the correct nested value",
48
+ "index_assignment_removal" => "Add a test that verifies the []= assignment side effect is observable (the collection is modified)",
49
+ "pattern_matching_guard" => "Add a test with input that matches the pattern but fails the guard to verify filtering",
50
+ "pattern_matching_alternative" => "Add a test with input that matches only one specific alternative to verify each branch is reachable",
51
+ "pattern_matching_array" => "Add a test that verifies each element position in the array pattern matches the expected type or value",
52
+ "collection_return" => "Add a test that verifies the method returns a non-empty collection, not just any array or hash",
53
+ "scalar_return" => "Add a test that verifies the method returns a non-zero/non-empty scalar value, not just any type"
54
+ }.freeze
55
+ end