rspec-signal 0.1.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +672 -0
  5. data/exe/rspec-signal +11 -0
  6. data/exe/rspec-signal-parallel +52 -0
  7. data/lib/rspec/signal/backtrace/classifier.rb +88 -0
  8. data/lib/rspec/signal/backtrace/frame.rb +31 -0
  9. data/lib/rspec/signal/backtrace/parser.rb +61 -0
  10. data/lib/rspec/signal/backtrace/reducer.rb +244 -0
  11. data/lib/rspec/signal/cluster.rb +68 -0
  12. data/lib/rspec/signal/clusterer.rb +49 -0
  13. data/lib/rspec/signal/configuration.rb +144 -0
  14. data/lib/rspec/signal/failure.rb +62 -0
  15. data/lib/rspec/signal/failure_builder.rb +224 -0
  16. data/lib/rspec/signal/fingerprint.rb +56 -0
  17. data/lib/rspec/signal/formatter.rb +207 -0
  18. data/lib/rspec/signal/group.rb +61 -0
  19. data/lib/rspec/signal/grouper.rb +28 -0
  20. data/lib/rspec/signal/html_summary.rb +224 -0
  21. data/lib/rspec/signal/integrations/capybara.rb +102 -0
  22. data/lib/rspec/signal/message.rb +161 -0
  23. data/lib/rspec/signal/parallel_merger.rb +125 -0
  24. data/lib/rspec/signal/parallel_run.rb +56 -0
  25. data/lib/rspec/signal/project.rb +158 -0
  26. data/lib/rspec/signal/redactor.rb +91 -0
  27. data/lib/rspec/signal/report.rb +88 -0
  28. data/lib/rspec/signal/reporters/full_output.rb +38 -0
  29. data/lib/rspec/signal/reporters/json_report.rb +20 -0
  30. data/lib/rspec/signal/reporters/markdown.rb +271 -0
  31. data/lib/rspec/signal/reporters/related_failures.rb +101 -0
  32. data/lib/rspec/signal/symptom.rb +22 -0
  33. data/lib/rspec/signal/symptoms/exception_class.rb +46 -0
  34. data/lib/rspec/signal/symptoms/http_status.rb +98 -0
  35. data/lib/rspec/signal/symptoms/record.rb +56 -0
  36. data/lib/rspec/signal/symptoms/route.rb +45 -0
  37. data/lib/rspec/signal/symptoms/ruby_error.rb +55 -0
  38. data/lib/rspec/signal/symptoms/selector.rb +72 -0
  39. data/lib/rspec/signal/symptoms.rb +42 -0
  40. data/lib/rspec/signal/version.rb +7 -0
  41. data/lib/rspec/signal/writer.rb +88 -0
  42. data/lib/rspec/signal.rb +156 -0
  43. data/lib/rspec-signal.rb +3 -0
  44. metadata +117 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Signal
5
+ module Symptoms
6
+ # Two Ruby-level messages specific enough to cluster on: a method called
7
+ # on the wrong thing, and a constant that was never defined.
8
+ #
9
+ # `undefined method 'progress' for nil` in six specs is one nil, not six
10
+ # bugs. Both the method and the receiver are part of the key, so
11
+ # `#name for nil` and `#total for nil` stay apart.
12
+ module RubyError
13
+ # Ruby 3.4 quotes with `'x'`, earlier versions with `` `x' ``.
14
+ UNDEFINED_METHOD = /undefined method [`'"](?<name>[^'"`]+)['"`] for (?:an instance of )?(?<receiver>\S+)/
15
+ UNINITIALIZED = /uninitialized constant (?<constant>[A-Z]\w*(?:::\w+)*)/
16
+
17
+ module_function
18
+
19
+ def call(_failure, text)
20
+ undefined_method(text) || uninitialized_constant(text)
21
+ end
22
+
23
+ def undefined_method(text)
24
+ match = UNDEFINED_METHOD.match(text)
25
+ return nil unless match
26
+
27
+ name = match[:name]
28
+ receiver = receiver_name(match[:receiver])
29
+ Symptom.new(kind: :undefined_method, key: "undefined-method:#{name}:#{receiver}",
30
+ label: "undefined method `#{name}` on #{receiver}",
31
+ detail: "undefined `#{name}` for #{receiver}")
32
+ end
33
+
34
+ def uninitialized_constant(text)
35
+ match = UNINITIALIZED.match(text)
36
+ return nil unless match
37
+
38
+ constant = match[:constant]
39
+ Symptom.new(kind: :missing_constant, key: "missing-constant:#{constant}",
40
+ label: "uninitialized constant `#{constant}`", detail: "uninitialized #{constant}")
41
+ end
42
+
43
+ # `nil:NilClass`, `#<User:0x00007f9a>` and `an instance of User` all
44
+ # name a class; use it, so two receivers of the same class cluster.
45
+ def receiver_name(raw)
46
+ text = raw.to_s
47
+ return "nil" if text.start_with?("nil")
48
+ return ::Regexp.last_match(1) if text =~ /\A#<([A-Z]\w*(?:::\w+)*)/
49
+
50
+ text.split(":").first.to_s
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Signal
5
+ module Symptoms
6
+ # A DOM node or piece of page text Capybara could not find.
7
+ #
8
+ # The same selector missing from four pages is one missing partial, one
9
+ # renamed `data-testid`, one component that never mounted. Capybara says
10
+ # so two different ways depending on whether you used `find` or
11
+ # `have_css`, and those two produce different exception classes and
12
+ # different messages, so they are correctly different signatures -- but
13
+ # they are obviously the same symptom.
14
+ #
15
+ # The selector itself is the cluster key, compared exactly. Two different
16
+ # selectors never meet.
17
+ module Selector
18
+ TYPES = "css|xpath|field|link or button|link|button|select box|checkbox|" \
19
+ "radio button|file field|fillable field|element|selector|table"
20
+ # The selector as Capybara printed it: inspected, or a bare token.
21
+ TARGET = /"(?:[^"\\]|\\.)*"|\S+/
22
+ VISIBILITY = "(?:visible |invisible )?"
23
+
24
+ # `find(...)` and friends raise Capybara::ElementNotFound.
25
+ UNABLE = /Unable to find #{VISIBILITY}(?<type>#{TYPES})\s+(?<target>#{TARGET})/i
26
+ # `expect(page).to have_css(...)` fails the matcher instead, with a
27
+ # different class and a different sentence for the same missing node.
28
+ NO_MATCHES = Regexp.new("expected to find #{VISIBILITY}(?<type>#{TYPES})\\s+(?<target>#{TARGET})" \
29
+ ".{0,200}?but there were no matches", Regexp::IGNORECASE)
30
+ # Page text is not a selector, but it goes missing for the same reasons.
31
+ TEXT = /(?:Unable to find|expected to find) text (?<target>#{TARGET})/i
32
+
33
+ module_function
34
+
35
+ def call(_failure, text)
36
+ match = NO_MATCHES.match(text) || UNABLE.match(text)
37
+ return missing_text(text) unless match
38
+
39
+ target = unquote(match[:target])
40
+ return nil if target.empty?
41
+
42
+ type = match[:type].to_s.downcase
43
+ Symptom.new(kind: :selector, key: "selector:#{type}:#{target}",
44
+ label: "missing #{type} selector `#{safe(target)}`",
45
+ detail: "no match for #{type} #{target}")
46
+ end
47
+
48
+ def missing_text(text)
49
+ match = TEXT.match(text)
50
+ return nil unless match
51
+
52
+ target = unquote(match[:target])
53
+ return nil if target.empty?
54
+
55
+ Symptom.new(kind: :missing_text, key: "text:#{target}",
56
+ label: "missing page text `#{safe(target)}`",
57
+ detail: "page did not contain \"#{target}\"")
58
+ end
59
+
60
+ # Capybara prints the selector with `inspect`, so a `[data-testid="x"]`
61
+ # arrives with its inner quotes escaped.
62
+ def unquote(raw)
63
+ text = raw.to_s
64
+ text = text[1..-2].to_s.gsub('\\"', '"').gsub("\\\\", "\\") if text.start_with?('"')
65
+ text.gsub(/\s+/, " ").strip
66
+ end
67
+
68
+ def safe(target) = target.tr("`", "'")
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "symptoms/http_status"
4
+ require_relative "symptoms/route"
5
+ require_relative "symptoms/selector"
6
+ require_relative "symptoms/record"
7
+ require_relative "symptoms/ruby_error"
8
+ require_relative "symptoms/exception_class"
9
+
10
+ module RSpec
11
+ module Signal
12
+ # The symptom extractors, in the order they are tried.
13
+ #
14
+ # A failure takes the first symptom that matches and no more, so it belongs
15
+ # to at most one related cluster. The order is significance, not
16
+ # convenience: a 404 in a request spec is a better organising fact than the
17
+ # exception class that carried it, and the exception class is a worse one
18
+ # than everything above it, which is why it is last.
19
+ module Symptoms
20
+ EXTRACTORS = [HttpStatus, Route, Selector, Record, RubyError, ExceptionClass].freeze
21
+
22
+ module_function
23
+
24
+ # @param failure [Failure]
25
+ # @return [Symptom, nil]
26
+ def for(failure)
27
+ text = one_line(failure.message.text)
28
+ EXTRACTORS.each do |extractor|
29
+ symptom = extractor.call(failure, text)
30
+ return symptom if symptom
31
+ end
32
+ nil
33
+ rescue StandardError
34
+ nil
35
+ end
36
+
37
+ # Extractor patterns are written against a single line: RSpec wraps the
38
+ # same sentence differently depending on matcher and terminal width.
39
+ def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Signal
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RSpec
6
+ module Signal
7
+ # Puts artifacts on disk.
8
+ #
9
+ # A run with no failures removes artifacts from previous runs, so an agent
10
+ # can never be handed a stale report that describes failures you already
11
+ # fixed.
12
+ class Writer
13
+ SIGNAL = "signal.md"
14
+ JSON = "signal.json"
15
+ FULL = "full.txt"
16
+ MANAGED = [SIGNAL, JSON, FULL].freeze
17
+
18
+ Result = Struct.new(:summary_path, :written, :cleaned, keyword_init: true)
19
+
20
+ def initialize(config)
21
+ @config = config
22
+ end
23
+
24
+ def dir = @config.output_path
25
+
26
+ def write(report)
27
+ return clean if report.failures.empty? && report.errors_outside_examples.zero?
28
+
29
+ FileUtils.mkdir_p(dir)
30
+ write_gitignore
31
+
32
+ markdown = Reporters::Markdown.new(report, @config).render
33
+ written = [write_file(SIGNAL, markdown)]
34
+ written.concat(optional_artifacts(report))
35
+
36
+ stale = MANAGED - written.map { |path| File.basename(path) }
37
+ Result.new(summary_path: File.join(dir, SIGNAL), written: written, cleaned: remove(stale))
38
+ end
39
+
40
+ # Relative to the project root when possible, because that is what you
41
+ # type and what an agent resolves.
42
+ def relative(path)
43
+ root = "#{@config.root}/"
44
+ path.start_with?(root) ? path[root.length..] : path
45
+ end
46
+
47
+ private
48
+
49
+ def optional_artifacts(report)
50
+ written = []
51
+ written << write_file(JSON, Reporters::JsonReport.new(report, @config).render) if @config.write_json
52
+ written << write_file(FULL, Reporters::FullOutput.new(report, @config).render) if @config.write_full
53
+ written
54
+ end
55
+
56
+ def clean
57
+ Result.new(summary_path: nil, written: [], cleaned: remove(MANAGED))
58
+ end
59
+
60
+ def remove(names)
61
+ names.filter_map do |name|
62
+ path = File.join(dir, name)
63
+ next unless File.file?(path)
64
+
65
+ File.delete(path)
66
+ path
67
+ end
68
+ end
69
+
70
+ def write_file(name, contents)
71
+ path = File.join(dir, name)
72
+ File.write(path, contents)
73
+ path
74
+ end
75
+
76
+ # Failure artifacts routinely contain application data. Keeping them out of
77
+ # version control by default is cheap insurance.
78
+ def write_gitignore
79
+ return unless @config.write_gitignore
80
+
81
+ path = File.join(dir, ".gitignore")
82
+ return if File.exist?(path)
83
+
84
+ File.write(path, "# Written by rspec-signal. Artifacts can contain application data.\n*\n")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+
5
+ require_relative "signal/version"
6
+ require_relative "signal/project"
7
+ require_relative "signal/backtrace/frame"
8
+ require_relative "signal/backtrace/parser"
9
+ require_relative "signal/backtrace/classifier"
10
+ require_relative "signal/backtrace/reducer"
11
+ require_relative "signal/redactor"
12
+ require_relative "signal/html_summary"
13
+ require_relative "signal/message"
14
+ require_relative "signal/fingerprint"
15
+ require_relative "signal/symptom"
16
+ require_relative "signal/symptoms"
17
+ require_relative "signal/failure"
18
+ require_relative "signal/group"
19
+ require_relative "signal/grouper"
20
+ require_relative "signal/cluster"
21
+ require_relative "signal/clusterer"
22
+ require_relative "signal/report"
23
+ require_relative "signal/configuration"
24
+ require_relative "signal/reporters/related_failures"
25
+ require_relative "signal/reporters/markdown"
26
+ require_relative "signal/reporters/json_report"
27
+ require_relative "signal/reporters/full_output"
28
+ require_relative "signal/writer"
29
+ require_relative "signal/parallel_run"
30
+ require_relative "signal/parallel_merger"
31
+ require_relative "signal/failure_builder"
32
+ require_relative "signal/formatter"
33
+ require_relative "signal/integrations/capybara"
34
+
35
+ module RSpec
36
+ # Turns noisy RSpec failures into compact, high-signal diagnostic artifacts
37
+ # designed to be handed to an AI coding agent.
38
+ module Signal
39
+ class << self
40
+ def configuration
41
+ @configuration ||= Configuration.new
42
+ end
43
+
44
+ def configure
45
+ yield configuration if block_given?
46
+ configuration.reset_memoized!
47
+ configuration
48
+ end
49
+
50
+ # Registers the formatter and optional integrations.
51
+ #
52
+ # Called automatically when you `require "rspec/signal"` inside an
53
+ # `RSpec.configure` block or from `spec_helper.rb`. Safe to call twice.
54
+ #
55
+ # @return [Boolean] whether anything was installed
56
+ def install!(rspec_config = ::RSpec.configuration)
57
+ return false if @installed
58
+
59
+ @installed = true
60
+ return false unless configuration.enabled?
61
+
62
+ quiet_mode? # Capture CLI intent before RSpec finishes parsing ARGV.
63
+ loader = rspec_config.formatter_loader
64
+ unless loader.formatters.any?(Formatter)
65
+ rspec_config.add_formatter(Formatter)
66
+ @auto_installed = true
67
+ end
68
+
69
+ install_integrations!(rspec_config)
70
+ true
71
+ rescue StandardError => e
72
+ warn "rspec-signal: could not install (#{e.class}: #{e.message})"
73
+ false
74
+ end
75
+
76
+ def installed? = !!@installed
77
+
78
+ # True when we added the formatter ourselves rather than the user asking
79
+ # for it with `--format`. Only then do we put RSpec's default formatter
80
+ # back, because only then did we displace it.
81
+ def auto_installed? = !!@auto_installed
82
+
83
+ # Explicit formatter selection happens after --require files are loaded,
84
+ # so install! may initially look automatic. Detect the user's intent from
85
+ # the original CLI arguments (or the wrapper's explicit marker) at start
86
+ # time, after RSpec has finished configuring its formatter loader.
87
+ def quiet_mode?
88
+ return true if ENV["RSPEC_SIGNAL_QUIET"] == "1"
89
+ return @quiet_mode_requested if defined?(@quiet_mode_requested)
90
+
91
+ @quiet_mode_requested = formatter_arguments.intersect?(["RSpec::Signal::Formatter", "signal"])
92
+ end
93
+
94
+ # Adding any formatter suppresses RSpec's default one. When rspec-signal
95
+ # installed itself the user did not ask for that, so restore normal
96
+ # terminal feedback.
97
+ def restore_default_formatter!(rspec_config = ::RSpec.configuration)
98
+ loader = rspec_config.formatter_loader
99
+ return if loader.formatters.any? { |formatter| primary_output_formatter?(formatter) }
100
+
101
+ before = loader.formatters.dup
102
+ loader.add(loader.default_formatter, rspec_config.output_stream)
103
+ (loader.formatters - before).each do |formatter|
104
+ formatter.start(::RSpec::Core::Notifications::StartNotification.new(0, 0)) if formatter.respond_to?(:start)
105
+ end
106
+ rescue StandardError
107
+ nil
108
+ end
109
+
110
+ # Versions worth recording in the report header.
111
+ def environment
112
+ versions = { "ruby" => RUBY_VERSION, "rspec" => ::RSpec::Core::Version::STRING }
113
+ versions["rails"] = ::Rails::VERSION::STRING if defined?(::Rails::VERSION::STRING)
114
+ versions["capybara"] = ::Capybara::VERSION if defined?(::Capybara::VERSION)
115
+ versions
116
+ rescue StandardError
117
+ { "ruby" => RUBY_VERSION }
118
+ end
119
+
120
+ # @api private Test support.
121
+ def reset!
122
+ @installed = false
123
+ @auto_installed = false
124
+ remove_instance_variable(:@quiet_mode_requested) if defined?(@quiet_mode_requested)
125
+ @configuration = nil
126
+ end
127
+
128
+ private
129
+
130
+ def formatter_arguments
131
+ ARGV.each_with_index.filter_map do |argument, index|
132
+ next ARGV[index + 1] if ["--format", "-f"].include?(argument)
133
+ next ::Regexp.last_match(1) if argument =~ /\A--format=(.+)\z/
134
+
135
+ argument[2..] if argument.start_with?("-f") && argument.length > 2
136
+ end
137
+ end
138
+
139
+ def install_integrations!(rspec_config)
140
+ return unless configuration.capture_capybara
141
+
142
+ Integrations::Capybara.install!(rspec_config, configuration)
143
+ end
144
+
145
+ # Formatters that give the developer their normal terminal feedback.
146
+ def primary_output_formatter?(formatter)
147
+ return false if formatter.is_a?(Formatter)
148
+
149
+ name = formatter.class.name.to_s
150
+ !name.end_with?("DeprecationFormatter", "FallbackMessageFormatter", "ProfileFormatter")
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ RSpec::Signal.install! if defined?(RSpec) && RSpec.respond_to?(:configuration)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec/signal"
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-signal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chad Snow
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.10'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.10'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ description: |
34
+ rspec-signal is a deterministic context-reduction layer between RSpec and an AI
35
+ coding agent. It collapses framework and runtime backtrace plumbing, keeps
36
+ first-party frames plus the small amount of library context that explains the
37
+ failing operation, groups repeated failures into distinct signatures, and writes
38
+ a compact Markdown report you can hand straight to a coding agent.
39
+ email:
40
+ - chaddsnow@gmail.com
41
+ executables:
42
+ - rspec-signal
43
+ - rspec-signal-parallel
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE
49
+ - README.md
50
+ - exe/rspec-signal
51
+ - exe/rspec-signal-parallel
52
+ - lib/rspec-signal.rb
53
+ - lib/rspec/signal.rb
54
+ - lib/rspec/signal/backtrace/classifier.rb
55
+ - lib/rspec/signal/backtrace/frame.rb
56
+ - lib/rspec/signal/backtrace/parser.rb
57
+ - lib/rspec/signal/backtrace/reducer.rb
58
+ - lib/rspec/signal/cluster.rb
59
+ - lib/rspec/signal/clusterer.rb
60
+ - lib/rspec/signal/configuration.rb
61
+ - lib/rspec/signal/failure.rb
62
+ - lib/rspec/signal/failure_builder.rb
63
+ - lib/rspec/signal/fingerprint.rb
64
+ - lib/rspec/signal/formatter.rb
65
+ - lib/rspec/signal/group.rb
66
+ - lib/rspec/signal/grouper.rb
67
+ - lib/rspec/signal/html_summary.rb
68
+ - lib/rspec/signal/integrations/capybara.rb
69
+ - lib/rspec/signal/message.rb
70
+ - lib/rspec/signal/parallel_merger.rb
71
+ - lib/rspec/signal/parallel_run.rb
72
+ - lib/rspec/signal/project.rb
73
+ - lib/rspec/signal/redactor.rb
74
+ - lib/rspec/signal/report.rb
75
+ - lib/rspec/signal/reporters/full_output.rb
76
+ - lib/rspec/signal/reporters/json_report.rb
77
+ - lib/rspec/signal/reporters/markdown.rb
78
+ - lib/rspec/signal/reporters/related_failures.rb
79
+ - lib/rspec/signal/symptom.rb
80
+ - lib/rspec/signal/symptoms.rb
81
+ - lib/rspec/signal/symptoms/exception_class.rb
82
+ - lib/rspec/signal/symptoms/http_status.rb
83
+ - lib/rspec/signal/symptoms/record.rb
84
+ - lib/rspec/signal/symptoms/route.rb
85
+ - lib/rspec/signal/symptoms/ruby_error.rb
86
+ - lib/rspec/signal/symptoms/selector.rb
87
+ - lib/rspec/signal/version.rb
88
+ - lib/rspec/signal/writer.rb
89
+ homepage: https://github.com/SilenceDogood1984/rspec-signal
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ source_code_uri: https://github.com/SilenceDogood1984/rspec-signal
94
+ changelog_uri: https://github.com/SilenceDogood1984/rspec-signal/blob/main/CHANGELOG.md
95
+ bug_tracker_uri: https://github.com/SilenceDogood1984/rspec-signal/issues
96
+ rubygems_mfa_required: 'true'
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 3.1.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 3.5.22
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Turn noisy RSpec failures into compact, high-signal reports for AI coding
116
+ agents.
117
+ test_files: []