evilution 0.26.0 → 0.28.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -4,6 +4,8 @@ require "fileutils"
4
4
  require "tmpdir"
5
5
  require_relative "../memory"
6
6
  require_relative "../temp_dir_tracker"
7
+ require_relative "../child_output"
8
+ require_relative "../process_cleanup"
7
9
 
8
10
  require_relative "../isolation"
9
11
 
@@ -48,19 +50,23 @@ class Evilution::Isolation::Fork
48
50
  read_io&.close
49
51
  write_io&.close
50
52
  ensure_reaped(pid)
51
- restore_original_source(mutation)
53
+ restore_original_source
52
54
  FileUtils.rm_rf(sandbox_dir) if sandbox_dir
53
55
  end
54
56
 
55
57
  private
56
58
 
57
- def restore_original_source(mutation) # rubocop:disable Lint/UnusedMethodArgument
59
+ def restore_original_source
58
60
  Evilution::TempDirTracker.cleanup_all
59
61
  end
60
62
 
61
63
  def suppress_child_output
62
- $stdout.reopen(File::NULL, "w")
63
- $stderr.reopen(File::NULL, "w")
64
+ if Evilution::ChildOutput.log_dir
65
+ Evilution::ChildOutput.redirect!
66
+ else
67
+ $stdout.reopen(File::NULL, "w")
68
+ $stderr.reopen(File::NULL, "w")
69
+ end
64
70
  end
65
71
 
66
72
  def execute_in_child(mutation, test_command)
@@ -83,7 +89,7 @@ class Evilution::Isolation::Fork
83
89
  if data.empty?
84
90
  { timeout: false, passed: false, error: "empty result from child" }
85
91
  else
86
- { timeout: false }.merge(Marshal.load(data)) # rubocop:disable Security/MarshalLoad
92
+ { timeout: false }.merge(Marshal.load(data))
87
93
  end
88
94
  else
89
95
  terminate_child(pid)
@@ -108,7 +114,7 @@ class Evilution::Isolation::Fork
108
114
  end
109
115
 
110
116
  def terminate_child(pid)
111
- ::Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
117
+ Evilution::ProcessCleanup.safe_kill("TERM", pid)
112
118
  _, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
113
119
  return if status
114
120
 
@@ -116,8 +122,8 @@ class Evilution::Isolation::Fork
116
122
  _, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
117
123
  return if status
118
124
 
119
- ::Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
120
- ::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
125
+ Evilution::ProcessCleanup.safe_kill("KILL", pid)
126
+ Evilution::ProcessCleanup.safe_wait(pid)
121
127
  end
122
128
 
123
129
  def classify_status(result)
@@ -138,11 +144,15 @@ class Evilution::Isolation::Fork
138
144
  status: status,
139
145
  duration: duration,
140
146
  test_command: result[:test_command],
141
- child_rss_kb: result[:child_rss_kb],
142
- parent_rss_kb: parent_rss_kb,
143
- error_message: result[:error],
144
- error_class: result[:error_class],
145
- error_backtrace: result[:error_backtrace]
147
+ memory: Evilution::Result::MemoryStats.from_fields(
148
+ child_rss_kb: result[:child_rss_kb],
149
+ parent_rss_kb: parent_rss_kb
150
+ ),
151
+ error: Evilution::Result::ErrorInfo.from_fields(
152
+ message: result[:error],
153
+ klass: result[:error_class],
154
+ backtrace: result[:error_backtrace]
155
+ )
146
156
  )
147
157
  end
148
158
  end
@@ -80,12 +80,16 @@ class Evilution::Isolation::InProcess
80
80
  status: status,
81
81
  duration: duration,
82
82
  test_command: result[:test_command],
83
- child_rss_kb: rss_after,
84
- memory_delta_kb: memory_delta_kb,
85
- parent_rss_kb: rss_before,
86
- error_message: result[:error],
87
- error_class: result[:error_class],
88
- error_backtrace: result[:error_backtrace]
83
+ memory: Evilution::Result::MemoryStats.from_fields(
84
+ child_rss_kb: rss_after,
85
+ memory_delta_kb: memory_delta_kb,
86
+ parent_rss_kb: rss_before
87
+ ),
88
+ error: Evilution::Result::ErrorInfo.from_fields(
89
+ message: result[:error],
90
+ klass: result[:error_class],
91
+ backtrace: result[:error_backtrace]
92
+ )
89
93
  )
90
94
  end
91
95
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../actions"
4
+ require_relative "../response_formatter"
5
+
6
+ class Evilution::MCP::InfoTool::Actions::Base
7
+ def self.call(**)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ class << self
12
+ private
13
+
14
+ def success(payload)
15
+ Evilution::MCP::InfoTool::ResponseFormatter.success(payload)
16
+ end
17
+
18
+ def config_error(message)
19
+ Evilution::MCP::InfoTool::ResponseFormatter.error("config_error", message)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../../config"
5
+ require_relative "../../../version"
6
+
7
+ class Evilution::MCP::InfoTool::Actions::Environment < Evilution::MCP::InfoTool::Actions::Base
8
+ def self.call(**)
9
+ config = Evilution::Config.new(skip_config_file: false)
10
+ config_file = Evilution::Config::CONFIG_FILES.find { |path| File.exist?(path) }
11
+
12
+ success(
13
+ "version" => Evilution::VERSION,
14
+ "ruby" => RUBY_VERSION,
15
+ "config_file" => config_file,
16
+ "settings" => settings(config)
17
+ )
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ def settings(config)
24
+ {
25
+ "timeout" => config.timeout,
26
+ "format" => config.format,
27
+ "integration" => config.integration,
28
+ "jobs" => config.jobs,
29
+ "isolation" => config.isolation,
30
+ "baseline" => config.baseline,
31
+ "incremental" => config.incremental,
32
+ "fail_fast" => config.fail_fast,
33
+ "min_score" => config.min_score,
34
+ "suggest_tests" => config.suggest_tests,
35
+ "save_session" => config.save_session,
36
+ "target" => config.target,
37
+ "skip_heredoc_literals" => config.skip_heredoc_literals,
38
+ "ignore_patterns" => config.ignore_patterns
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../../version"
5
+ require_relative "../../../feedback"
6
+ require_relative "../../../feedback/messages"
7
+
8
+ class Evilution::MCP::InfoTool::Actions::Feedback < Evilution::MCP::InfoTool::Actions::Base
9
+ def self.call(**)
10
+ success(
11
+ "discussion_url" => Evilution::Feedback::DISCUSSION_URL,
12
+ "version" => Evilution::VERSION,
13
+ "guidance_for_agent" => Evilution::Feedback::Messages.info_guidance
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../status_glossary"
5
+
6
+ class Evilution::MCP::InfoTool::Actions::Statuses < Evilution::MCP::InfoTool::Actions::Base
7
+ def self.call(**)
8
+ success("statuses" => Evilution::MCP::InfoTool::StatusGlossary.entries)
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../config_factory"
5
+ require_relative "../../../runner"
6
+ require_relative "../../../mutator/registry"
7
+ require_relative "../../../ast/pattern/filter"
8
+
9
+ class Evilution::MCP::InfoTool::Actions::Subjects < Evilution::MCP::InfoTool::Actions::Base
10
+ def self.call(files: nil, line_ranges: nil, target: nil, integration: nil, skip_config: nil, **)
11
+ return config_error("files is required") if files.nil? || files.empty?
12
+
13
+ config = Evilution::MCP::InfoTool::ConfigFactory.subjects(
14
+ files: files, line_ranges: line_ranges,
15
+ target: target, integration: integration, skip_config: skip_config
16
+ )
17
+ runner = Evilution::Runner.new(config: config)
18
+ subjects = runner.parse_and_filter_subjects
19
+
20
+ registry = Evilution::Mutator::Registry.default
21
+ filter = build_subject_filter(config)
22
+ operator_options = { skip_heredoc_literals: config.skip_heredoc_literals? }
23
+
24
+ entries = subjects.map do |subj|
25
+ count = registry.mutations_for(subj, filter: filter, operator_options: operator_options).length
26
+ { "name" => subj.name, "file" => subj.file_path, "line" => subj.line_number, "mutations" => count }
27
+ ensure
28
+ subj.release_node!
29
+ end
30
+
31
+ success(
32
+ "subjects" => entries,
33
+ "total_subjects" => entries.length,
34
+ "total_mutations" => entries.sum { |e| e["mutations"] }
35
+ )
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ def build_subject_filter(config)
42
+ return nil if config.ignore_patterns.empty?
43
+
44
+ Evilution::AST::Pattern::Filter.new(config.ignore_patterns)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../config_factory"
5
+ require_relative "../../../runner"
6
+ require_relative "../../../spec_resolver"
7
+
8
+ class Evilution::MCP::InfoTool::Actions::Tests < Evilution::MCP::InfoTool::Actions::Base
9
+ def self.call(files: nil, spec: nil, integration: nil, skip_config: nil, **)
10
+ return config_error("files is required") if files.nil? || files.empty?
11
+
12
+ config = Evilution::MCP::InfoTool::ConfigFactory.tests(
13
+ files: files, spec: spec, integration: integration, skip_config: skip_config
14
+ )
15
+ return explicit_specs_response(files, config.spec_files) if config.spec_files.any?
16
+
17
+ resolver = resolver_for(config.integration)
18
+ resolved, unresolved = resolve_specs(files, resolver)
19
+ success(
20
+ "specs" => resolved,
21
+ "unresolved" => unresolved,
22
+ "total_sources" => files.length,
23
+ "total_specs" => resolved.map { |r| r["spec"] }.uniq.length
24
+ )
25
+ end
26
+
27
+ class << self
28
+ private
29
+
30
+ def resolver_for(integration)
31
+ integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
32
+ return Evilution::SpecResolver.new unless integration_class
33
+
34
+ integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
35
+ end
36
+
37
+ def explicit_specs_response(files, spec_files)
38
+ success(
39
+ "specs" => spec_files.map { |f| { "source" => nil, "spec" => f } },
40
+ "unresolved" => [],
41
+ "total_sources" => files.length,
42
+ "total_specs" => spec_files.uniq.length
43
+ )
44
+ end
45
+
46
+ def resolve_specs(files, resolver)
47
+ resolved = []
48
+ unresolved = []
49
+ files.each do |source|
50
+ found = resolver.call(source)
51
+ if found
52
+ resolved << { "source" => source, "spec" => found }
53
+ else
54
+ unresolved << source
55
+ end
56
+ end
57
+ [resolved, unresolved]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require_relative "../../mcp"
5
+
6
+ class Evilution::MCP::InfoTool < MCP::Tool; end unless defined?(Evilution::MCP::InfoTool)
7
+
8
+ module Evilution::MCP::InfoTool::Actions
9
+ end
10
+
11
+ unless defined?(Evilution::MCP::InfoTool::Actions::Base)
12
+ class Evilution::MCP::InfoTool::Actions::Base # rubocop:disable Lint/EmptyClass
13
+ end
14
+ end
15
+
16
+ require_relative "../info_tool"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../info_tool"
4
+ require_relative "../../config"
5
+
6
+ module Evilution::MCP::InfoTool::ConfigFactory
7
+ module_function
8
+
9
+ def subjects(files:, line_ranges:, target:, integration:, skip_config:)
10
+ opts = { target_files: files, line_ranges: line_ranges || {} }
11
+ opts[:skip_config_file] = true if skip_config
12
+ opts[:target] = target if target
13
+ opts[:integration] = integration if integration
14
+ Evilution::Config.new(**opts)
15
+ end
16
+
17
+ def tests(files:, spec:, integration:, skip_config:)
18
+ opts = { target_files: files }
19
+ opts[:skip_config_file] = true if skip_config
20
+ opts[:spec_files] = spec if spec
21
+ opts[:integration] = integration if integration
22
+ Evilution::Config.new(**opts)
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../info_tool"
4
+
5
+ module Evilution::MCP::InfoTool::ErrorMapper
6
+ module_function
7
+
8
+ def type_for(error)
9
+ case error
10
+ when Evilution::ConfigError then "config_error"
11
+ when Evilution::ParseError then "parse_error"
12
+ else "runtime_error"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../info_tool"
4
+
5
+ module Evilution::MCP::InfoTool::RequestParser
6
+ module_function
7
+
8
+ def parse_files(raw_files)
9
+ files = []
10
+ ranges = {}
11
+
12
+ raw_files.each do |arg|
13
+ file, range_str = arg.split(":", 2)
14
+ files << file
15
+ ranges[file] = parse_line_range(range_str) if range_str
16
+ end
17
+
18
+ [files, ranges]
19
+ end
20
+
21
+ def parse_line_range(str)
22
+ if str.include?("-")
23
+ start_str, end_str = str.split("-", 2)
24
+ start_line = Integer(start_str)
25
+ end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
26
+ start_line..end_line
27
+ else
28
+ line = Integer(str)
29
+ line..line
30
+ end
31
+ rescue ArgumentError, TypeError
32
+ raise Evilution::ParseError, "invalid line range: #{str.inspect}"
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../info_tool"
5
+ require_relative "error_mapper"
6
+
7
+ module Evilution::MCP::InfoTool::ResponseFormatter
8
+ module_function
9
+
10
+ def success(payload)
11
+ ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(payload) }])
12
+ end
13
+
14
+ def error(type, message)
15
+ ::MCP::Tool::Response.new(
16
+ [{ type: "text", text: ::JSON.generate({ error: { type: type, message: message } }) }],
17
+ error: true
18
+ )
19
+ end
20
+
21
+ def error_for(exception)
22
+ error(Evilution::MCP::InfoTool::ErrorMapper.type_for(exception), exception.message)
23
+ end
24
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../info_tool"
4
+ require_relative "../../result/mutation_result"
5
+
6
+ module Evilution::MCP::InfoTool::StatusGlossary
7
+ ENTRIES = [
8
+ {
9
+ "status" => "killed",
10
+ "meaning" => "A test failed when the mutation was applied — the test suite caught the mutation. " \
11
+ "This is the desired outcome.",
12
+ "counted_in_score" => true
13
+ },
14
+ {
15
+ "status" => "survived",
16
+ "meaning" => "No test failed when the mutation was applied — gap in coverage. " \
17
+ "The test suite did not detect the behavioral change.",
18
+ "counted_in_score" => true
19
+ },
20
+ {
21
+ "status" => "timeout",
22
+ "meaning" => "Test run exceeded the configured per-mutation timeout. " \
23
+ "Treated like survived for scoring (counted in the denominator); " \
24
+ "may indicate an infinite loop introduced by the mutation.",
25
+ "counted_in_score" => true
26
+ },
27
+ {
28
+ "status" => "error",
29
+ "meaning" => "Mutation execution raised an unexpected error (syntax error at load time, " \
30
+ "boot failure, test-infrastructure crash). The mutation could not be evaluated.",
31
+ "counted_in_score" => false
32
+ },
33
+ {
34
+ "status" => "neutral",
35
+ "meaning" => "Baseline tests already failed before the mutation was applied — pre-existing " \
36
+ "test-suite problem (flaky spec, infra collision, fixture setup failure). " \
37
+ "Not a meaningful mutation signal.",
38
+ "counted_in_score" => false
39
+ },
40
+ {
41
+ "status" => "equivalent",
42
+ "meaning" => "Mutation is provably identical to the original source " \
43
+ "(e.g. a no-op replacement that the parser or evaluator treats as semantically equal).",
44
+ "counted_in_score" => false
45
+ },
46
+ {
47
+ "status" => "unresolved",
48
+ "meaning" => "No spec/test file resolved for the mutated source — coverage gap, not a failure. " \
49
+ "The file has no corresponding test file the resolver could locate.",
50
+ "counted_in_score" => false
51
+ },
52
+ {
53
+ "status" => "unparseable",
54
+ "meaning" => "Mutated source failed to parse (e.g. dangling heredoc after method_body_replacement). " \
55
+ "Short-circuited before execution; no test run was attempted.",
56
+ "counted_in_score" => false
57
+ }
58
+ ].each(&:freeze).freeze
59
+
60
+ module_function
61
+
62
+ def entries
63
+ check_drift!
64
+ ENTRIES
65
+ end
66
+
67
+ def check_drift!
68
+ defined_statuses = Evilution::Result::MutationResult::STATUSES.map(&:to_s).sort
69
+ documented = ENTRIES.map { |s| s["status"] }.sort
70
+ return if defined_statuses == documented
71
+
72
+ missing = (defined_statuses - documented) + (documented - defined_statuses)
73
+ raise Evilution::Error, "status glossary drift: #{missing.inspect}"
74
+ end
75
+ end