spec_scout 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.
- checksums.yaml +7 -0
- data/.idea/.gitignore +10 -0
- data/.idea/Projects.iml +41 -0
- data/.idea/copilot.data.migration.ask2agent.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec_status +236 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +72 -0
- data/LICENSE +21 -0
- data/README.md +433 -0
- data/Rakefile +12 -0
- data/examples/README.md +321 -0
- data/examples/best_practices.md +401 -0
- data/examples/configurations/basic_config.rb +24 -0
- data/examples/configurations/ci_config.rb +35 -0
- data/examples/configurations/conservative_config.rb +32 -0
- data/examples/configurations/development_config.rb +37 -0
- data/examples/configurations/performance_focused_config.rb +38 -0
- data/examples/output_formatter_demo.rb +67 -0
- data/examples/sample_outputs/console_output_high_confidence.txt +27 -0
- data/examples/sample_outputs/console_output_medium_confidence.txt +27 -0
- data/examples/sample_outputs/console_output_no_action.txt +27 -0
- data/examples/sample_outputs/console_output_risk_detected.txt +27 -0
- data/examples/sample_outputs/json_output_high_confidence.json +108 -0
- data/examples/sample_outputs/json_output_no_action.json +108 -0
- data/examples/workflows/basic_workflow.md +159 -0
- data/examples/workflows/ci_integration.md +372 -0
- data/exe/spec_scout +7 -0
- data/lib/spec_scout/agent_result.rb +44 -0
- data/lib/spec_scout/agents/database_agent.rb +113 -0
- data/lib/spec_scout/agents/factory_agent.rb +179 -0
- data/lib/spec_scout/agents/intent_agent.rb +223 -0
- data/lib/spec_scout/agents/risk_agent.rb +290 -0
- data/lib/spec_scout/base_agent.rb +72 -0
- data/lib/spec_scout/cli.rb +158 -0
- data/lib/spec_scout/configuration.rb +162 -0
- data/lib/spec_scout/consensus_engine.rb +535 -0
- data/lib/spec_scout/enforcement_handler.rb +182 -0
- data/lib/spec_scout/output_formatter.rb +307 -0
- data/lib/spec_scout/profile_data.rb +37 -0
- data/lib/spec_scout/profile_normalizer.rb +238 -0
- data/lib/spec_scout/recommendation.rb +62 -0
- data/lib/spec_scout/safety_validator.rb +127 -0
- data/lib/spec_scout/spec_scout.rb +519 -0
- data/lib/spec_scout/testprof_integration.rb +206 -0
- data/lib/spec_scout/version.rb +5 -0
- data/lib/spec_scout.rb +43 -0
- metadata +166 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module SpecScout
|
|
6
|
+
# Safety validation module to ensure no spec file mutations during analysis
|
|
7
|
+
# and prevent auto-application of code changes by default
|
|
8
|
+
class SafetyValidator
|
|
9
|
+
class SafetyViolationError < StandardError; end
|
|
10
|
+
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
@monitored_files = Set.new
|
|
14
|
+
@original_file_states = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Monitor spec files to ensure they are not modified during analysis
|
|
18
|
+
def monitor_spec_files(spec_paths)
|
|
19
|
+
return unless @config.enabled?
|
|
20
|
+
|
|
21
|
+
spec_paths = Array(spec_paths).compact
|
|
22
|
+
spec_paths.each do |path|
|
|
23
|
+
next unless File.exist?(path)
|
|
24
|
+
|
|
25
|
+
@monitored_files.add(path)
|
|
26
|
+
@original_file_states[path] = {
|
|
27
|
+
mtime: File.mtime(path),
|
|
28
|
+
size: File.size(path),
|
|
29
|
+
checksum: file_checksum(path)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Validate that no monitored files have been modified
|
|
35
|
+
def validate_no_mutations!
|
|
36
|
+
return unless @config.enabled?
|
|
37
|
+
|
|
38
|
+
violations = []
|
|
39
|
+
|
|
40
|
+
@monitored_files.each do |path|
|
|
41
|
+
next unless File.exist?(path)
|
|
42
|
+
|
|
43
|
+
original_state = @original_file_states[path]
|
|
44
|
+
current_state = {
|
|
45
|
+
mtime: File.mtime(path),
|
|
46
|
+
size: File.size(path),
|
|
47
|
+
checksum: file_checksum(path)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
violations << "File modified during analysis: #{path}" if file_modified?(original_state, current_state)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return if violations.empty?
|
|
54
|
+
|
|
55
|
+
raise SafetyViolationError, "Safety violation detected:\n#{violations.join("\n")}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Ensure no auto-application of code changes
|
|
59
|
+
def prevent_auto_application!
|
|
60
|
+
return unless @config.enabled?
|
|
61
|
+
|
|
62
|
+
return unless @config.auto_apply_enabled?
|
|
63
|
+
|
|
64
|
+
raise SafetyViolationError,
|
|
65
|
+
'Auto-application of code changes is not allowed by default. Use explicit configuration to enable.'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Validate non-blocking operation mode
|
|
69
|
+
def validate_non_blocking_mode!
|
|
70
|
+
return unless @config.enabled?
|
|
71
|
+
|
|
72
|
+
# In non-blocking mode, we should never exit with failure unless explicitly configured
|
|
73
|
+
return unless @config.blocking_mode_enabled?
|
|
74
|
+
|
|
75
|
+
raise SafetyViolationError,
|
|
76
|
+
'Blocking mode is not allowed by default. Use enforcement mode configuration to enable.'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if the system is operating in safe mode
|
|
80
|
+
def safe_mode?
|
|
81
|
+
@config.enabled? &&
|
|
82
|
+
!@config.auto_apply_enabled? &&
|
|
83
|
+
!@config.blocking_mode_enabled?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get safety status report
|
|
87
|
+
def safety_status
|
|
88
|
+
{
|
|
89
|
+
safe_mode: safe_mode?,
|
|
90
|
+
monitored_files: @monitored_files.size,
|
|
91
|
+
auto_apply_disabled: !@config.auto_apply_enabled?,
|
|
92
|
+
non_blocking_mode: !@config.blocking_mode_enabled?,
|
|
93
|
+
mutations_detected: mutations_detected?
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def file_checksum(path)
|
|
100
|
+
require 'digest'
|
|
101
|
+
Digest::SHA256.file(path).hexdigest
|
|
102
|
+
rescue StandardError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def file_modified?(original, current)
|
|
107
|
+
original[:mtime] != current[:mtime] ||
|
|
108
|
+
original[:size] != current[:size] ||
|
|
109
|
+
(original[:checksum] && current[:checksum] && original[:checksum] != current[:checksum])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def mutations_detected?
|
|
113
|
+
@monitored_files.any? do |path|
|
|
114
|
+
next false unless File.exist?(path)
|
|
115
|
+
|
|
116
|
+
original_state = @original_file_states[path]
|
|
117
|
+
current_state = {
|
|
118
|
+
mtime: File.mtime(path),
|
|
119
|
+
size: File.size(path),
|
|
120
|
+
checksum: file_checksum(path)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
file_modified?(original_state, current_state)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecScout
|
|
4
|
+
# Main orchestration class that coordinates TestProf integration, agents, and consensus
|
|
5
|
+
class SpecScout
|
|
6
|
+
attr_reader :config, :safety_validator, :enforcement_handler
|
|
7
|
+
|
|
8
|
+
def initialize(config = nil)
|
|
9
|
+
@config = config || ::SpecScout.configuration
|
|
10
|
+
@config.validate!
|
|
11
|
+
@safety_validator = SafetyValidator.new(@config)
|
|
12
|
+
@enforcement_handler = EnforcementHandler.new(@config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main entry point for both CLI and programmatic execution
|
|
16
|
+
def analyze(spec_location = nil)
|
|
17
|
+
log_debug("Starting SpecScout analysis for: #{spec_location || 'all specs'}")
|
|
18
|
+
|
|
19
|
+
return disabled_result unless @config.enabled?
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
# Validate enforcement configuration
|
|
23
|
+
log_debug('Validating enforcement configuration')
|
|
24
|
+
@enforcement_handler.validate_enforcement_config!
|
|
25
|
+
|
|
26
|
+
# Perform safety validations before analysis
|
|
27
|
+
log_debug('Performing safety validations')
|
|
28
|
+
perform_safety_validations(spec_location)
|
|
29
|
+
|
|
30
|
+
# Execute TestProf profiling if enabled
|
|
31
|
+
log_debug('Executing TestProf profiling')
|
|
32
|
+
profile_data = execute_profiling(spec_location)
|
|
33
|
+
return no_profile_data_result unless profile_data
|
|
34
|
+
|
|
35
|
+
# Run agent analysis
|
|
36
|
+
log_debug('Running agent analysis')
|
|
37
|
+
agent_results = run_agents(profile_data)
|
|
38
|
+
|
|
39
|
+
# Check if we have any successful agent results
|
|
40
|
+
successful_results = agent_results.reject { |result| result.verdict == :agent_failed }
|
|
41
|
+
return no_agents_result if successful_results.empty?
|
|
42
|
+
|
|
43
|
+
# Generate consensus recommendation
|
|
44
|
+
log_debug('Generating consensus recommendation')
|
|
45
|
+
recommendation = generate_recommendation(agent_results, profile_data)
|
|
46
|
+
|
|
47
|
+
# Validate no mutations occurred during analysis
|
|
48
|
+
log_debug('Validating no mutations occurred')
|
|
49
|
+
@safety_validator.validate_no_mutations!
|
|
50
|
+
|
|
51
|
+
# Handle enforcement mode
|
|
52
|
+
log_debug('Handling enforcement mode')
|
|
53
|
+
enforcement_result = @enforcement_handler.handle_enforcement(
|
|
54
|
+
recommendation[:recommendation],
|
|
55
|
+
profile_data
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
final_result = recommendation.merge(enforcement_result)
|
|
59
|
+
log_debug('SpecScout analysis completed successfully')
|
|
60
|
+
final_result
|
|
61
|
+
rescue SafetyValidator::SafetyViolationError => e
|
|
62
|
+
log_error("Safety violation detected: #{e.message}")
|
|
63
|
+
handle_safety_violation(e)
|
|
64
|
+
rescue EnforcementHandler::EnforcementFailureError => e
|
|
65
|
+
log_error("Enforcement failure: #{e.message}")
|
|
66
|
+
handle_enforcement_error(e)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
log_error("Unexpected error during analysis: #{e.message}")
|
|
69
|
+
log_debug("Full backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
|
|
70
|
+
handle_error(e)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# CLI execution mode
|
|
75
|
+
def self.run_cli(args = ARGV)
|
|
76
|
+
begin
|
|
77
|
+
config = parse_cli_args(args)
|
|
78
|
+
scout = new(config)
|
|
79
|
+
|
|
80
|
+
puts '🔍 SpecScout starting analysis...' if config.console_output? && config.debug_mode?
|
|
81
|
+
|
|
82
|
+
result = scout.analyze
|
|
83
|
+
|
|
84
|
+
# Output results
|
|
85
|
+
if result[:recommendation] && result[:profile_data]
|
|
86
|
+
formatter = OutputFormatter.new(result[:recommendation], result[:profile_data])
|
|
87
|
+
output = config.json_output? ? formatter.format_json : formatter.format_recommendation
|
|
88
|
+
puts output
|
|
89
|
+
elsif result[:disabled]
|
|
90
|
+
puts 'SpecScout is disabled' if config.console_output?
|
|
91
|
+
elsif result[:no_profile_data]
|
|
92
|
+
puts 'No profile data available - ensure TestProf is properly configured' if config.console_output?
|
|
93
|
+
elsif result[:no_agents]
|
|
94
|
+
puts 'No agents produced results - check agent configuration' if config.console_output?
|
|
95
|
+
elsif result[:error]
|
|
96
|
+
puts "Analysis failed: #{result[:error].message}" if config.console_output?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Handle enforcement mode output
|
|
100
|
+
puts result[:enforcement_message] if result[:enforcement_message] && !config.json_output?
|
|
101
|
+
|
|
102
|
+
# Handle safety violations
|
|
103
|
+
puts "🚨 Safety violation: #{result[:safety_violation]}" if result[:safety_violation] && !config.json_output?
|
|
104
|
+
|
|
105
|
+
# Handle enforcement errors
|
|
106
|
+
puts "⚠️ Enforcement error: #{result[:enforcement_error]}" if result[:enforcement_error] && !config.json_output?
|
|
107
|
+
|
|
108
|
+
# Exit with appropriate code for CI
|
|
109
|
+
exit_code = result[:exit_code] || (result[:should_fail] ? 1 : 0)
|
|
110
|
+
|
|
111
|
+
puts "🏁 SpecScout completed with exit code: #{exit_code}" if config.console_output? && config.debug_mode?
|
|
112
|
+
exit(exit_code)
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
warn "🚨 SpecScout CLI failed: #{e.message}"
|
|
115
|
+
warn "Backtrace: #{e.backtrace.join("\n")}" if ENV['SPEC_SCOUT_DEBUG']
|
|
116
|
+
exit(1)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Programmatic execution mode
|
|
123
|
+
def self.analyze_spec(spec_location = nil, config = nil)
|
|
124
|
+
scout = new(config)
|
|
125
|
+
scout.analyze(spec_location)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def perform_safety_validations(spec_location)
|
|
131
|
+
# Prevent auto-application of code changes by default
|
|
132
|
+
@safety_validator.prevent_auto_application!
|
|
133
|
+
|
|
134
|
+
# Validate non-blocking operation mode
|
|
135
|
+
@safety_validator.validate_non_blocking_mode!
|
|
136
|
+
|
|
137
|
+
# Monitor spec files to detect mutations
|
|
138
|
+
spec_paths = collect_spec_paths(spec_location)
|
|
139
|
+
@safety_validator.monitor_spec_files(spec_paths)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def collect_spec_paths(spec_location)
|
|
143
|
+
return [] unless spec_location
|
|
144
|
+
|
|
145
|
+
if File.directory?(spec_location)
|
|
146
|
+
Dir.glob(File.join(spec_location, '**', '*_spec.rb'))
|
|
147
|
+
elsif File.file?(spec_location)
|
|
148
|
+
[spec_location]
|
|
149
|
+
else
|
|
150
|
+
# Try to find spec files in common locations
|
|
151
|
+
spec_dirs = %w[spec test]
|
|
152
|
+
spec_files = []
|
|
153
|
+
|
|
154
|
+
spec_dirs.each do |dir|
|
|
155
|
+
next unless Dir.exist?(dir)
|
|
156
|
+
|
|
157
|
+
spec_files.concat(Dir.glob(File.join(dir, '**', '*_spec.rb')))
|
|
158
|
+
spec_files.concat(Dir.glob(File.join(dir, '**', '*_test.rb')))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
spec_files
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def handle_safety_violation(error)
|
|
166
|
+
warn "🚨 Safety Violation: #{error.message}" if @config.console_output?
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
recommendation: nil,
|
|
170
|
+
profile_data: nil,
|
|
171
|
+
agent_results: [],
|
|
172
|
+
safety_violation: error.message,
|
|
173
|
+
should_fail: true, # Safety violations should always fail
|
|
174
|
+
exit_code: 1
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_enforcement_error(error)
|
|
179
|
+
warn "⚠️ Enforcement Error: #{error.message}" if @config.console_output?
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
recommendation: error.recommendation,
|
|
183
|
+
profile_data: nil,
|
|
184
|
+
agent_results: [],
|
|
185
|
+
enforcement_error: error.message,
|
|
186
|
+
should_fail: true,
|
|
187
|
+
exit_code: 1
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def execute_profiling(spec_location)
|
|
192
|
+
return nil unless @config.test_prof_enabled?
|
|
193
|
+
|
|
194
|
+
log_debug("Starting TestProf integration for: #{spec_location || 'all specs'}")
|
|
195
|
+
|
|
196
|
+
integration = TestProfIntegration.new(@config)
|
|
197
|
+
profile_data = integration.execute_profiling(spec_location)
|
|
198
|
+
|
|
199
|
+
if profile_data.nil? || profile_data.empty?
|
|
200
|
+
log_debug('No profile data returned from TestProf integration')
|
|
201
|
+
return nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
log_debug("TestProf data extracted successfully: #{profile_data.keys}")
|
|
205
|
+
|
|
206
|
+
normalizer = ProfileNormalizer.new
|
|
207
|
+
normalized_data = normalizer.normalize(profile_data, build_example_context(spec_location))
|
|
208
|
+
|
|
209
|
+
log_debug('Profile data normalized successfully')
|
|
210
|
+
normalized_data
|
|
211
|
+
rescue TestProfIntegration::TestProfError => e
|
|
212
|
+
log_error("TestProf integration failed: #{e.message}")
|
|
213
|
+
nil
|
|
214
|
+
rescue ProfileNormalizer::NormalizationError => e
|
|
215
|
+
log_error("Profile data normalization failed: #{e.message}")
|
|
216
|
+
nil
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
log_error("Unexpected error during profiling: #{e.message}")
|
|
219
|
+
log_debug("Backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def run_agents(profile_data)
|
|
224
|
+
log_debug("Starting agent analysis with #{@config.enabled_agents.size} enabled agents")
|
|
225
|
+
agent_results = []
|
|
226
|
+
|
|
227
|
+
# Run each enabled agent
|
|
228
|
+
@config.enabled_agents.each do |agent_name|
|
|
229
|
+
next unless @config.agent_enabled?(agent_name)
|
|
230
|
+
|
|
231
|
+
log_debug("Running #{agent_name} agent")
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
agent = create_agent(agent_name, profile_data)
|
|
235
|
+
result = agent.evaluate
|
|
236
|
+
|
|
237
|
+
# Validate agent result structure
|
|
238
|
+
validate_agent_result(result, agent_name)
|
|
239
|
+
|
|
240
|
+
# Handle both Hash and AgentResult objects
|
|
241
|
+
if result.is_a?(AgentResult)
|
|
242
|
+
agent_results << result
|
|
243
|
+
else
|
|
244
|
+
# Convert Hash to AgentResult for consistency
|
|
245
|
+
agent_result = AgentResult.new(
|
|
246
|
+
agent_name: agent_name,
|
|
247
|
+
verdict: result[:verdict],
|
|
248
|
+
confidence: result[:confidence],
|
|
249
|
+
reasoning: result[:reasoning],
|
|
250
|
+
metadata: result[:metadata] || {}
|
|
251
|
+
)
|
|
252
|
+
agent_results << agent_result
|
|
253
|
+
end
|
|
254
|
+
log_debug("#{agent_name} agent completed: #{result[:verdict]} (#{result[:confidence]})")
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
log_error("Agent #{agent_name} failed: #{e.message}")
|
|
257
|
+
log_debug("Agent #{agent_name} backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
|
|
258
|
+
|
|
259
|
+
# Create a failed agent result for debugging
|
|
260
|
+
failed_result = AgentResult.new(
|
|
261
|
+
agent_name: agent_name,
|
|
262
|
+
verdict: :agent_failed,
|
|
263
|
+
confidence: :none,
|
|
264
|
+
reasoning: "Agent execution failed: #{e.message}",
|
|
265
|
+
metadata: { error: e.message, failed_at: Time.now }
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Only include failed results in debug mode
|
|
269
|
+
agent_results << failed_result if debug_enabled?
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
log_debug("Agent analysis completed: #{agent_results.size} successful results")
|
|
274
|
+
agent_results
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def create_agent(agent_name, profile_data)
|
|
278
|
+
case agent_name
|
|
279
|
+
when :database
|
|
280
|
+
Agents::DatabaseAgent.new(profile_data)
|
|
281
|
+
when :factory
|
|
282
|
+
Agents::FactoryAgent.new(profile_data)
|
|
283
|
+
when :intent
|
|
284
|
+
Agents::IntentAgent.new(profile_data)
|
|
285
|
+
when :risk
|
|
286
|
+
Agents::RiskAgent.new(profile_data)
|
|
287
|
+
else
|
|
288
|
+
raise ArgumentError, "Unknown agent: #{agent_name}"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def generate_recommendation(agent_results, profile_data)
|
|
293
|
+
log_debug("Generating consensus from #{agent_results.size} agent results")
|
|
294
|
+
|
|
295
|
+
begin
|
|
296
|
+
consensus = ConsensusEngine.new(agent_results, profile_data)
|
|
297
|
+
recommendation = consensus.generate_recommendation
|
|
298
|
+
|
|
299
|
+
log_debug("Consensus generated: #{recommendation[:action]} (#{recommendation[:confidence]})")
|
|
300
|
+
|
|
301
|
+
{
|
|
302
|
+
recommendation: recommendation,
|
|
303
|
+
profile_data: profile_data,
|
|
304
|
+
agent_results: agent_results
|
|
305
|
+
}
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
log_error("Consensus generation failed: #{e.message}")
|
|
308
|
+
log_debug("Consensus backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
|
|
309
|
+
|
|
310
|
+
# Return a fallback result
|
|
311
|
+
{
|
|
312
|
+
recommendation: create_fallback_recommendation(e),
|
|
313
|
+
profile_data: profile_data,
|
|
314
|
+
agent_results: agent_results
|
|
315
|
+
}
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def handle_error(error)
|
|
320
|
+
error_message = "SpecScout analysis failed: #{error.message}"
|
|
321
|
+
|
|
322
|
+
if @config.console_output?
|
|
323
|
+
warn error_message
|
|
324
|
+
warn "Error type: #{error.class.name}"
|
|
325
|
+
|
|
326
|
+
if debug_enabled?
|
|
327
|
+
warn 'Full backtrace:'
|
|
328
|
+
warn error.backtrace.join("\n")
|
|
329
|
+
else
|
|
330
|
+
warn 'Run with SPEC_SCOUT_DEBUG=true for full backtrace'
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
{
|
|
335
|
+
recommendation: nil,
|
|
336
|
+
profile_data: nil,
|
|
337
|
+
agent_results: [],
|
|
338
|
+
error: error,
|
|
339
|
+
error_message: error_message,
|
|
340
|
+
should_fail: false, # Don't fail by default on unexpected errors
|
|
341
|
+
exit_code: 0
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def disabled_result
|
|
346
|
+
{
|
|
347
|
+
recommendation: nil,
|
|
348
|
+
profile_data: nil,
|
|
349
|
+
agent_results: [],
|
|
350
|
+
disabled: true,
|
|
351
|
+
should_fail: false,
|
|
352
|
+
exit_code: 0
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def no_profile_data_result
|
|
357
|
+
{
|
|
358
|
+
recommendation: nil,
|
|
359
|
+
profile_data: nil,
|
|
360
|
+
agent_results: [],
|
|
361
|
+
no_profile_data: true,
|
|
362
|
+
should_fail: false,
|
|
363
|
+
exit_code: 0
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def no_agents_result
|
|
368
|
+
{
|
|
369
|
+
recommendation: nil,
|
|
370
|
+
profile_data: nil,
|
|
371
|
+
agent_results: [],
|
|
372
|
+
no_agents: true,
|
|
373
|
+
should_fail: false,
|
|
374
|
+
exit_code: 0
|
|
375
|
+
}
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Parse CLI arguments into configuration
|
|
379
|
+
def self.parse_cli_args(args)
|
|
380
|
+
config = ::SpecScout.configuration.dup
|
|
381
|
+
|
|
382
|
+
i = 0
|
|
383
|
+
while i < args.length
|
|
384
|
+
case args[i]
|
|
385
|
+
when '--disable'
|
|
386
|
+
config.enable = false
|
|
387
|
+
when '--no-testprof'
|
|
388
|
+
config.use_test_prof = false
|
|
389
|
+
when '--enforce'
|
|
390
|
+
config.enforcement_mode = true
|
|
391
|
+
when '--fail-on-high-confidence'
|
|
392
|
+
config.fail_on_high_confidence = true
|
|
393
|
+
when '--auto-apply'
|
|
394
|
+
config.auto_apply_enabled = true
|
|
395
|
+
when '--blocking-mode'
|
|
396
|
+
config.blocking_mode_enabled = true
|
|
397
|
+
when '--output'
|
|
398
|
+
i += 1
|
|
399
|
+
config.output_format = args[i] if i < args.length
|
|
400
|
+
when '--enable-agent'
|
|
401
|
+
i += 1
|
|
402
|
+
config.enable_agent(args[i]) if i < args.length
|
|
403
|
+
when '--disable-agent'
|
|
404
|
+
i += 1
|
|
405
|
+
config.disable_agent(args[i]) if i < args.length
|
|
406
|
+
when '--help', '-h'
|
|
407
|
+
print_help
|
|
408
|
+
exit(0)
|
|
409
|
+
end
|
|
410
|
+
i += 1
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
config.validate!
|
|
414
|
+
config
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def self.print_help
|
|
418
|
+
puts <<~HELP
|
|
419
|
+
SpecScout - Intelligent test optimization advisor
|
|
420
|
+
|
|
421
|
+
Usage: spec_scout [options]
|
|
422
|
+
|
|
423
|
+
Options:
|
|
424
|
+
--disable Disable SpecScout analysis
|
|
425
|
+
--no-testprof Disable TestProf integration
|
|
426
|
+
--enforce Enable enforcement mode (fail on high confidence)
|
|
427
|
+
--fail-on-high-confidence Fail on high confidence recommendations
|
|
428
|
+
--auto-apply Enable auto-application of code changes (UNSAFE)
|
|
429
|
+
--blocking-mode Enable blocking operation mode
|
|
430
|
+
--output FORMAT Output format (console, json)
|
|
431
|
+
--enable-agent AGENT Enable specific agent (database, factory, intent, risk)
|
|
432
|
+
--disable-agent AGENT Disable specific agent
|
|
433
|
+
--help, -h Show this help message
|
|
434
|
+
|
|
435
|
+
Safety Options:
|
|
436
|
+
By default, SpecScout operates in safe mode:
|
|
437
|
+
- No spec files are modified during analysis
|
|
438
|
+
- No code changes are auto-applied
|
|
439
|
+
- Non-blocking operation (recommendations only)
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
spec_scout # Run with default settings (safe mode)
|
|
443
|
+
spec_scout --enforce # Enable enforcement mode
|
|
444
|
+
spec_scout --output json # JSON output
|
|
445
|
+
spec_scout --disable-agent risk # Disable risk agent
|
|
446
|
+
spec_scout --auto-apply --enforce # UNSAFE: Enable auto-application
|
|
447
|
+
HELP
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Build example context for profile normalization
|
|
451
|
+
def build_example_context(spec_location)
|
|
452
|
+
context = {}
|
|
453
|
+
|
|
454
|
+
if spec_location
|
|
455
|
+
context[:location] = spec_location
|
|
456
|
+
context[:file_path] = spec_location
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
context
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Validate agent result structure
|
|
463
|
+
def validate_agent_result(result, agent_name)
|
|
464
|
+
# Handle both Hash and AgentResult objects
|
|
465
|
+
if result.is_a?(AgentResult)
|
|
466
|
+
# AgentResult objects are already validated
|
|
467
|
+
true
|
|
468
|
+
elsif result.is_a?(Hash)
|
|
469
|
+
# Validate Hash format (for backward compatibility)
|
|
470
|
+
required_keys = %i[verdict confidence reasoning]
|
|
471
|
+
missing_keys = required_keys - result.keys
|
|
472
|
+
|
|
473
|
+
unless missing_keys.empty?
|
|
474
|
+
raise ArgumentError, "Agent #{agent_name} result missing required keys: #{missing_keys}"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Validate confidence levels
|
|
478
|
+
valid_confidence_levels = %i[high medium low none]
|
|
479
|
+
unless valid_confidence_levels.include?(result[:confidence])
|
|
480
|
+
raise ArgumentError, "Agent #{agent_name} returned invalid confidence level: #{result[:confidence]}"
|
|
481
|
+
end
|
|
482
|
+
else
|
|
483
|
+
raise ArgumentError, "Agent #{agent_name} must return a Hash or AgentResult, got #{result.class}"
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Create fallback recommendation when consensus fails
|
|
488
|
+
def create_fallback_recommendation(error)
|
|
489
|
+
Recommendation.new(
|
|
490
|
+
spec_location: 'unknown',
|
|
491
|
+
action: :no_action,
|
|
492
|
+
from_value: nil,
|
|
493
|
+
to_value: nil,
|
|
494
|
+
confidence: :none,
|
|
495
|
+
explanation: "Unable to generate recommendation due to consensus engine failure: #{error.message}",
|
|
496
|
+
agent_results: []
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Logging helpers
|
|
501
|
+
def log_debug(message)
|
|
502
|
+
return unless debug_enabled?
|
|
503
|
+
|
|
504
|
+
return unless @config.console_output?
|
|
505
|
+
|
|
506
|
+
puts "[DEBUG] SpecScout: #{message}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def log_error(message)
|
|
510
|
+
return unless @config.console_output?
|
|
511
|
+
|
|
512
|
+
warn "[ERROR] SpecScout: #{message}"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def debug_enabled?
|
|
516
|
+
ENV['SPEC_SCOUT_DEBUG'] == 'true' || @config.debug_mode?
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|