evilution 0.25.0 → 0.27.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +15 -0
  3. data/.claude/prompts/architect.md +14 -1
  4. data/.claude/skills/create-issue/SKILL.md +55 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/CHANGELOG.md +38 -0
  7. data/README.md +57 -3
  8. data/lib/evilution/ast/constant_names.rb +34 -0
  9. data/lib/evilution/cache.rb +2 -0
  10. data/lib/evilution/child_output.rb +24 -0
  11. data/lib/evilution/cli/commands/run.rb +9 -0
  12. data/lib/evilution/cli/commands/version.rb +2 -0
  13. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  14. data/lib/evilution/compare/invalid_input.rb +12 -0
  15. data/lib/evilution/compare.rb +1 -10
  16. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  17. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  18. data/lib/evilution/config/builders.rb +4 -0
  19. data/lib/evilution/config/env_loader.rb +12 -0
  20. data/lib/evilution/config/file_loader.rb +22 -0
  21. data/lib/evilution/config/sources.rb +14 -0
  22. data/lib/evilution/config/validators/base.rb +37 -0
  23. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  24. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  25. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  26. data/lib/evilution/config/validators/hooks.rb +12 -0
  27. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  28. data/lib/evilution/config/validators/integration.rb +11 -0
  29. data/lib/evilution/config/validators/isolation.rb +19 -0
  30. data/lib/evilution/config/validators/jobs.rb +9 -0
  31. data/lib/evilution/config/validators/preload.rb +13 -0
  32. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  33. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  34. data/lib/evilution/config/validators.rb +4 -0
  35. data/lib/evilution/config.rb +78 -268
  36. data/lib/evilution/feedback/detector.rb +15 -0
  37. data/lib/evilution/feedback/messages.rb +42 -0
  38. data/lib/evilution/feedback.rb +5 -0
  39. data/lib/evilution/integration/base.rb +4 -155
  40. data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
  41. data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
  42. data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
  43. data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
  44. data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
  45. data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
  46. data/lib/evilution/integration/loading.rb +6 -0
  47. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  48. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  49. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  50. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  51. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  52. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  53. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  54. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  55. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  58. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  59. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  60. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  61. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  62. data/lib/evilution/integration/rspec.rb +61 -232
  63. data/lib/evilution/isolation/fork.rb +7 -2
  64. data/lib/evilution/load_path/subpath_resolver.rb +25 -0
  65. data/lib/evilution/load_path.rb +4 -0
  66. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  67. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  68. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  69. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  70. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  71. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  72. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  73. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  75. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  76. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  77. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  78. data/lib/evilution/mcp/info_tool.rb +43 -261
  79. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  80. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  81. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  82. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  83. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  84. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  85. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  86. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  87. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  88. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  89. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  90. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  91. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  92. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  93. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  94. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  95. data/lib/evilution/parallel/work_queue.rb +42 -327
  96. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  97. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  98. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  99. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  100. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  101. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  102. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  103. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  104. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  105. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  106. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  107. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  108. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  109. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  110. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  111. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  112. data/lib/evilution/reporter/cli/pct.rb +9 -0
  113. data/lib/evilution/reporter/cli/section.rb +13 -0
  114. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  115. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  116. data/lib/evilution/reporter/cli.rb +79 -162
  117. data/lib/evilution/runner/isolation_resolver.rb +20 -2
  118. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  119. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  120. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  121. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  122. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  123. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  124. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  125. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  126. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  127. data/lib/evilution/runner/mutation_executor.rb +58 -289
  128. data/lib/evilution/runner/subject_pipeline.rb +18 -8
  129. data/lib/evilution/runner.rb +21 -0
  130. data/lib/evilution/version.rb +1 -1
  131. metadata +125 -5
  132. data/lib/evilution/mcp/session_diff_tool.rb +0 -63
  133. data/lib/evilution/mcp/session_list_tool.rb +0 -50
  134. data/lib/evilution/mcp/session_show_tool.rb +0 -57
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::ExampleTargetingFallback < Evilution::Config::Validators::Base
6
+ FALLBACKS = %i[full_file unresolved].freeze
7
+
8
+ def self.call(value)
9
+ unless value.is_a?(String) || value.is_a?(Symbol)
10
+ raise Evilution::ConfigError,
11
+ "example_targeting_fallback must be full_file or unresolved, got #{value.inspect}"
12
+ end
13
+
14
+ sym = value.to_sym
15
+ unless FALLBACKS.include?(sym)
16
+ raise Evilution::ConfigError,
17
+ "example_targeting_fallback must be full_file or unresolved, got #{sym.inspect}"
18
+ end
19
+
20
+ sym
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::FailFast < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+
9
+ coerce_positive_int!(value, name: "fail_fast")
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Hooks < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return {} if value.nil?
8
+ raise Evilution::ConfigError, "hooks must be a mapping of event names to file paths, got #{value.class}" unless value.is_a?(Hash)
9
+
10
+ value
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::IgnorePatterns < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ patterns = Array(value)
8
+ patterns.each do |pattern|
9
+ unless pattern.is_a?(String)
10
+ raise Evilution::ConfigError,
11
+ "ignore_patterns must be an array of strings, got #{pattern.class} (#{pattern.inspect})"
12
+ end
13
+ end
14
+ patterns
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
6
+ ALLOWED = %i[rspec minitest].freeze
7
+
8
+ def self.call(value)
9
+ coerce_symbol!(value, allowed: ALLOWED, name: "integration")
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Isolation < Evilution::Config::Validators::Base
6
+ ALLOWED = %i[auto fork in_process].freeze
7
+ MESSAGE = "isolation must be auto, fork, or in_process"
8
+
9
+ def self.call(value)
10
+ raise Evilution::ConfigError, "#{MESSAGE}, got nil" if value.nil?
11
+
12
+ raise Evilution::ConfigError, "#{MESSAGE}, got #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
13
+
14
+ sym = value.to_sym
15
+ return sym if ALLOWED.include?(sym)
16
+
17
+ raise Evilution::ConfigError, "#{MESSAGE}, got #{sym.inspect}"
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Jobs < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ coerce_positive_int!(value, name: "jobs")
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Preload < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+ return false if value == false
9
+ return value if value.is_a?(String)
10
+
11
+ raise Evilution::ConfigError, "preload must be nil, false, or a String path, got #{value.inspect}"
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return {} if value.nil?
8
+
9
+ raise Evilution::ConfigError, "spec_mappings must be a Hash, got #{value.class}" unless value.is_a?(Hash)
10
+
11
+ normalized = value.each_with_object({}) do |(source, specs), acc|
12
+ key = normalize_key(source)
13
+ acc[key] = normalize_value(key, specs)
14
+ end
15
+
16
+ warn_missing(normalized)
17
+ normalized
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ def normalize_key(source)
24
+ key = source.to_s
25
+ key = key.delete_prefix("#{Dir.pwd}/") if key.start_with?("/")
26
+ key.delete_prefix("./")
27
+ end
28
+
29
+ def normalize_value(source, specs)
30
+ case specs
31
+ when String then [specs]
32
+ when Array
33
+ specs.each do |entry|
34
+ unless entry.is_a?(String)
35
+ raise Evilution::ConfigError,
36
+ "spec_mappings[#{source.inspect}] entries must be string paths, got #{entry.class}"
37
+ end
38
+ end
39
+ specs
40
+ else
41
+ raise Evilution::ConfigError,
42
+ "spec_mappings[#{source.inspect}] must be a string or array of strings, got #{specs.class}"
43
+ end
44
+ end
45
+
46
+ def warn_missing(mappings)
47
+ mappings.each do |source, specs|
48
+ specs.each do |spec_path|
49
+ next if File.exist?(spec_path)
50
+
51
+ warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::SpecPattern < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+ return value if value.is_a?(String)
9
+
10
+ raise Evilution::ConfigError, "spec_pattern must be nil or a String glob, got #{value.class}"
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Config::Validators
4
+ end
@@ -8,40 +8,18 @@ class Evilution::Config
8
8
  CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
9
9
 
10
10
  DEFAULTS = {
11
- timeout: 30,
12
- format: :text,
13
- target: nil,
14
- min_score: 0.0,
15
- integration: :rspec,
16
- verbose: false,
17
- quiet: false,
18
- jobs: 1,
19
- fail_fast: nil,
20
- baseline: true,
21
- isolation: :auto,
22
- incremental: false,
23
- suggest_tests: false,
24
- progress: true,
25
- save_session: false,
26
- line_ranges: {},
27
- spec_files: [],
28
- ignore_patterns: [],
29
- show_disabled: false,
30
- baseline_session: nil,
31
- skip_heredoc_literals: false,
32
- related_specs_heuristic: false,
33
- fallback_to_full_suite: false,
34
- preload: nil,
35
- spec_mappings: {},
36
- spec_pattern: nil,
37
- example_targeting: true,
11
+ timeout: 30, format: :text, target: nil, min_score: 0.0, integration: :rspec,
12
+ verbose: false, quiet: false, jobs: 1, fail_fast: nil, baseline: true,
13
+ isolation: :auto, incremental: false, suggest_tests: false, progress: true,
14
+ save_session: false, line_ranges: {}, spec_files: [], ignore_patterns: [],
15
+ show_disabled: false, baseline_session: nil, skip_heredoc_literals: false,
16
+ related_specs_heuristic: false, fallback_to_full_suite: false, preload: nil,
17
+ spec_mappings: {}, spec_pattern: nil, example_targeting: true,
38
18
  example_targeting_fallback: :full_file,
39
- example_targeting_cache: { max_files: 50, max_blocks: 10_000 }
19
+ example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
20
+ quiet_children: false, quiet_children_dir: "tmp/evilution_children"
40
21
  }.freeze
41
22
 
42
- EXAMPLE_TARGETING_FALLBACKS = %i[full_file unresolved].freeze
43
- private_constant :EXAMPLE_TARGETING_FALLBACKS
44
-
45
23
  attr_reader :target_files, :timeout, :format,
46
24
  :target, :min_score, :integration, :verbose, :quiet,
47
25
  :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
@@ -50,12 +28,11 @@ class Evilution::Config
50
28
  :skip_heredoc_literals, :related_specs_heuristic,
51
29
  :fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
52
30
  :example_targeting, :example_targeting_fallback, :example_targeting_cache,
53
- :spec_selector
31
+ :spec_selector, :quiet_children, :quiet_children_dir
54
32
 
55
33
  def initialize(**options)
56
- file_options = options.delete(:skip_config_file) ? {} : load_config_file
57
- env_options = load_env_options
58
- merged = DEFAULTS.merge(file_options).merge(env_options).merge(options)
34
+ skip_file = options.delete(:skip_config_file) ? true : false
35
+ merged = Sources.merge(explicit: options, skip_file: skip_file)
59
36
  assign_attributes(merged)
60
37
  freeze
61
38
  end
@@ -125,18 +102,7 @@ class Evilution::Config
125
102
  end
126
103
 
127
104
  def self.file_options
128
- CONFIG_FILES.each do |path|
129
- next unless File.exist?(path)
130
-
131
- data = YAML.safe_load_file(path, symbolize_names: true)
132
- return data.is_a?(Hash) ? data : {}
133
- rescue Psych::SyntaxError, Psych::DisallowedClass => e
134
- raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
135
- rescue SystemCallError => e
136
- raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
137
- end
138
-
139
- {}
105
+ FileLoader.load
140
106
  end
141
107
 
142
108
  # Generates a default config file template.
@@ -185,9 +151,10 @@ class Evilution::Config
185
151
  # fallback_to_full_suite: false
186
152
 
187
153
  # Preload file required in the parent process before forking workers.
188
- # For Rails projects, spec/rails_helper.rb or test/test_helper.rb is
189
- # auto-detected when isolation resolves to :fork. Set to false to disable.
190
- # preload: spec/rails_helper.rb # or test/test_helper.rb
154
+ # For Rails projects, the autodetect chain tries (in order):
155
+ # spec/rails_helper.rb -> spec/spec_helper.rb -> test/test_helper.rb
156
+ # when isolation resolves to :fork. Set to false to disable.
157
+ # preload: spec/rails_helper.rb # or spec/spec_helper.rb, test/test_helper.rb
191
158
 
192
159
  # Hooks: Ruby files returning a Proc, keyed by lifecycle event
193
160
  # hooks:
@@ -221,234 +188,77 @@ class Evilution::Config
221
188
 
222
189
  private
223
190
 
224
- def validate_fail_fast(value)
225
- return nil if value.nil?
226
-
227
- value = Integer(value)
228
- raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
229
-
230
- value
231
- rescue ::ArgumentError, ::TypeError
232
- raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
233
- end
234
-
235
- def assign_attributes(merged) # rubocop:disable Metrics/AbcSize
236
- @target_files = Array(merged[:target_files])
237
- @timeout = merged[:timeout]
238
- @format = merged[:format].to_sym
239
- @target = merged[:target]
240
- @min_score = merged[:min_score].to_f
241
- @integration = validate_integration(merged[:integration])
242
- @verbose = merged[:verbose]
243
- @quiet = merged[:quiet]
244
- @jobs = validate_jobs(merged[:jobs])
245
- @fail_fast = validate_fail_fast(merged[:fail_fast])
246
- @baseline = merged[:baseline]
247
- @isolation = validate_isolation(merged[:isolation])
248
- @incremental = merged[:incremental]
249
- @suggest_tests = merged[:suggest_tests]
250
- @progress = merged[:progress]
251
- @save_session = merged[:save_session]
252
- @line_ranges = merged[:line_ranges] || {}
253
- @spec_files = Array(merged[:spec_files])
254
- @ignore_patterns = validate_ignore_patterns(merged[:ignore_patterns])
255
- @show_disabled = merged[:show_disabled]
256
- @baseline_session = merged[:baseline_session]
257
- @skip_heredoc_literals = merged[:skip_heredoc_literals]
258
- @related_specs_heuristic = merged[:related_specs_heuristic]
259
- @fallback_to_full_suite = merged[:fallback_to_full_suite]
260
- @hooks = validate_hooks(merged[:hooks])
261
- @preload = validate_preload(merged[:preload])
262
- @spec_mappings = validate_spec_mappings(merged[:spec_mappings])
263
- @spec_pattern = validate_spec_pattern(merged[:spec_pattern])
191
+ def assign_attributes(merged)
192
+ assign_simple_attributes(merged)
193
+ assign_validated_attributes(merged)
264
194
  assign_example_targeting(merged)
265
- @spec_selector = build_spec_selector
266
- end
267
-
268
- def assign_example_targeting(merged)
269
- @example_targeting = merged[:example_targeting] ? true : false
270
- @example_targeting_fallback = validate_example_targeting_fallback(merged[:example_targeting_fallback])
271
- @example_targeting_cache = validate_example_targeting_cache(merged[:example_targeting_cache])
272
- end
273
-
274
- def build_spec_selector
275
- Evilution::SpecSelector.new(
195
+ @spec_selector = Builders::SpecSelector.call(
276
196
  spec_files: @spec_files,
277
197
  spec_mappings: @spec_mappings,
278
198
  spec_pattern: @spec_pattern,
279
- spec_resolver: build_spec_resolver
199
+ integration: @integration
280
200
  )
281
201
  end
282
202
 
283
- def build_spec_resolver
284
- case @integration
285
- when :minitest
286
- Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
287
- else
288
- Evilution::SpecResolver.new
289
- end
290
- end
291
-
292
- def validate_spec_mappings(value)
293
- return {} if value.nil?
294
-
295
- raise Evilution::ConfigError, "spec_mappings must be a Hash, got #{value.class}" unless value.is_a?(Hash)
296
-
297
- normalized = value.each_with_object({}) do |(source, specs), acc|
298
- key = normalize_spec_mappings_key(source)
299
- acc[key] = normalize_spec_mappings_value(key, specs)
300
- end
301
-
302
- warn_missing_spec_mappings(normalized)
303
- normalized
304
- end
305
-
306
- def normalize_spec_mappings_key(source)
307
- key = source.to_s
308
- key = key.delete_prefix("#{Dir.pwd}/") if key.start_with?("/")
309
- key.delete_prefix("./")
310
- end
311
-
312
- def normalize_spec_mappings_value(source, specs)
313
- case specs
314
- when String then [specs]
315
- when Array
316
- specs.each do |entry|
317
- unless entry.is_a?(String)
318
- raise Evilution::ConfigError,
319
- "spec_mappings[#{source.inspect}] entries must be string paths, got #{entry.class}"
320
- end
321
- end
322
- specs
323
- else
324
- raise Evilution::ConfigError,
325
- "spec_mappings[#{source.inspect}] must be a string or array of strings, got #{specs.class}"
326
- end
327
- end
328
-
329
- def warn_missing_spec_mappings(mappings)
330
- mappings.each do |source, specs|
331
- specs.each do |spec_path|
332
- next if File.exist?(spec_path)
333
-
334
- warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
335
- end
336
- end
337
- end
338
-
339
- def validate_spec_pattern(value)
340
- return nil if value.nil?
341
- return value if value.is_a?(String)
342
-
343
- raise Evilution::ConfigError, "spec_pattern must be nil or a String glob, got #{value.class}"
344
- end
345
-
346
- def validate_preload(value)
347
- return nil if value.nil?
348
- return false if value == false
349
- return value if value.is_a?(String)
350
-
351
- raise Evilution::ConfigError, "preload must be nil, false, or a String path, got #{value.inspect}"
352
- end
353
-
354
- def validate_integration(value)
355
- raise Evilution::ConfigError, "integration must be rspec or minitest, got nil" if value.nil?
356
-
357
- value = value.to_sym
358
- unless %i[rspec minitest].include?(value)
359
- raise Evilution::ConfigError,
360
- "integration must be rspec or minitest, got #{value.inspect}"
361
- end
362
-
363
- value
364
- end
365
-
366
- def validate_isolation(value)
367
- raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got nil" if value.nil?
368
-
369
- value = value.to_sym
370
- raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got #{value.inspect}" unless %i[auto fork
371
- in_process].include?(value)
372
-
373
- value
374
- end
375
-
376
- def validate_jobs(value)
377
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
378
-
379
- value = Integer(value)
380
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
381
-
382
- value
383
- rescue ::ArgumentError, ::TypeError
384
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}"
385
- end
386
-
387
- def validate_ignore_patterns(value)
388
- patterns = Array(value)
389
- patterns.each do |pattern|
390
- unless pattern.is_a?(String)
391
- raise Evilution::ConfigError,
392
- "ignore_patterns must be an array of strings, got #{pattern.class} (#{pattern.inspect})"
393
- end
394
- end
395
- patterns
396
- end
397
-
398
- def validate_example_targeting_fallback(value)
399
- unless value.is_a?(String) || value.is_a?(Symbol)
400
- raise Evilution::ConfigError,
401
- "example_targeting_fallback must be full_file or unresolved, got #{value.inspect}"
402
- end
403
-
404
- sym = value.to_sym
405
- unless EXAMPLE_TARGETING_FALLBACKS.include?(sym)
406
- raise Evilution::ConfigError,
407
- "example_targeting_fallback must be full_file or unresolved, got #{sym.inspect}"
408
- end
409
-
410
- sym
411
- end
412
-
413
- def validate_example_targeting_cache(value)
414
- raise Evilution::ConfigError, "example_targeting_cache must be a Hash, got #{value.class}" unless value.is_a?(Hash)
415
-
416
- normalized = value.each_with_object({}) do |(k, v), acc|
417
- unless k.is_a?(String) || k.is_a?(Symbol)
418
- raise Evilution::ConfigError,
419
- "example_targeting_cache keys must be Strings or Symbols, got #{k.inspect}"
420
- end
421
- acc[k.to_sym] = v
422
- end
423
- merged = DEFAULTS[:example_targeting_cache].merge(normalized)
424
- validate_positive_int!(merged, :max_files)
425
- validate_positive_int!(merged, :max_blocks)
426
- merged
427
- end
428
-
429
- def validate_positive_int!(cache, key)
430
- v = cache[key]
431
- return if v.is_a?(Integer) && v >= 1
432
-
433
- raise Evilution::ConfigError,
434
- "example_targeting_cache.#{key} must be a positive integer, got #{v.inspect}"
203
+ def assign_simple_attributes(merged)
204
+ @target_files = Array(merged[:target_files])
205
+ @timeout = merged[:timeout]
206
+ @format = merged[:format].to_sym
207
+ @target = merged[:target]
208
+ @min_score = merged[:min_score].to_f
209
+ @verbose = merged[:verbose]
210
+ @quiet = merged[:quiet]
211
+ @baseline = merged[:baseline]
212
+ @incremental = merged[:incremental]
213
+ @suggest_tests = merged[:suggest_tests]
214
+ @progress = merged[:progress]
215
+ @save_session = merged[:save_session]
216
+ @line_ranges = merged[:line_ranges] || {}
217
+ @spec_files = Array(merged[:spec_files])
218
+ @show_disabled = merged[:show_disabled]
219
+ @baseline_session = merged[:baseline_session]
220
+ @skip_heredoc_literals = merged[:skip_heredoc_literals]
221
+ @related_specs_heuristic = merged[:related_specs_heuristic]
222
+ @fallback_to_full_suite = merged[:fallback_to_full_suite]
223
+ @quiet_children = merged[:quiet_children]
224
+ @quiet_children_dir = merged[:quiet_children_dir]
435
225
  end
436
226
 
437
- def load_env_options
438
- opts = {}
439
- val = ENV.fetch("EV_DISABLE_EXAMPLE_TARGETING", nil)
440
- opts[:example_targeting] = false if val && !val.empty? && val != "0"
441
- opts
227
+ def assign_validated_attributes(merged)
228
+ @integration = Validators::Integration.call(merged[:integration])
229
+ @jobs = Validators::Jobs.call(merged[:jobs])
230
+ @fail_fast = Validators::FailFast.call(merged[:fail_fast])
231
+ @isolation = Validators::Isolation.call(merged[:isolation])
232
+ @ignore_patterns = Validators::IgnorePatterns.call(merged[:ignore_patterns])
233
+ @hooks = Validators::Hooks.call(merged[:hooks])
234
+ @preload = Validators::Preload.call(merged[:preload])
235
+ @spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
236
+ @spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
442
237
  end
443
238
 
444
- def validate_hooks(value)
445
- return {} if value.nil?
446
- raise Evilution::ConfigError, "hooks must be a mapping of event names to file paths, got #{value.class}" unless value.is_a?(Hash)
447
-
448
- value
449
- end
450
-
451
- def load_config_file
452
- self.class.file_options
239
+ def assign_example_targeting(merged)
240
+ @example_targeting = merged[:example_targeting] ? true : false
241
+ @example_targeting_fallback = Validators::ExampleTargetingFallback.call(merged[:example_targeting_fallback])
242
+ @example_targeting_cache = Validators::ExampleTargetingCache.call(merged[:example_targeting_cache])
453
243
  end
454
244
  end
245
+
246
+ require_relative "config/file_loader"
247
+ require_relative "config/env_loader"
248
+ require_relative "config/sources"
249
+ require_relative "config/validators"
250
+ require_relative "config/validators/base"
251
+ require_relative "config/validators/integration"
252
+ require_relative "config/validators/isolation"
253
+ require_relative "config/validators/jobs"
254
+ require_relative "config/validators/fail_fast"
255
+ require_relative "config/validators/preload"
256
+ require_relative "config/validators/hooks"
257
+ require_relative "config/validators/ignore_patterns"
258
+ require_relative "config/validators/spec_pattern"
259
+ require_relative "config/validators/spec_mappings"
260
+ require_relative "config/validators/example_targeting_fallback"
261
+ require_relative "config/validators/example_targeting_cache"
262
+ require_relative "config/builders"
263
+ require_relative "config/builders/spec_resolver"
264
+ require_relative "config/builders/spec_selector"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../feedback"
4
+
5
+ module Evilution::Feedback::Detector
6
+ module_function
7
+
8
+ def friction?(summary)
9
+ return false if summary.nil?
10
+
11
+ summary.errors.positive? ||
12
+ summary.unparseable.positive? ||
13
+ summary.unresolved.positive?
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../feedback"
4
+ require_relative "../version"
5
+
6
+ module Evilution::Feedback::Messages
7
+ module_function
8
+
9
+ def cli_footer
10
+ "> Hit friction with evilution v#{Evilution::VERSION}? " \
11
+ "Share details: #{Evilution::Feedback::DISCUSSION_URL}"
12
+ end
13
+
14
+ def mcp_hint
15
+ "Feedback welcome on errors, usage problems, friction, or missing " \
16
+ "capabilities you wish were there. Do NOT post on the user's " \
17
+ "behalf without explicit user permission. Show the user exactly " \
18
+ "what you would post and only submit after their explicit approval. " \
19
+ "Never include secrets, env vars, project name, file paths, source " \
20
+ "code, or class/method names from user code."
21
+ end
22
+
23
+ def info_guidance
24
+ <<~GUIDANCE.chomp
25
+ The maintainer welcomes feedback in four cases:
26
+ 1. Errors raised by evilution
27
+ 2. Usage problems (confusing behavior, unexpected output)
28
+ 3. Friction (slow runs, awkward workflow, hard-to-interpret output)
29
+ 4. Missing capabilities or features you wish were there
30
+
31
+ CONSENT (required):
32
+ Never post on the user's behalf without explicit user permission.
33
+ Show the user exactly what you would post. Submit only after the
34
+ user explicitly approves.
35
+
36
+ PRIVACY (required):
37
+ Never include secrets, tokens, environment variables, the project
38
+ name, absolute or project-relative file paths, source code, or
39
+ class/method names from user code. The feedback channel is public.
40
+ GUIDANCE
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Feedback
4
+ DISCUSSION_URL = "https://github.com/marinazzio/evilution/discussions"
5
+ end