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,535 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ConsensusEngine aggregates agent verdicts into final recommendations using decision matrix logic.
|
|
4
|
+
# It analyzes agent results, identifies risk factors, and determines the best course of action for test optimization.
|
|
5
|
+
module SpecScout
|
|
6
|
+
# Aggregates agent verdicts into final recommendations using decision matrix logic
|
|
7
|
+
class ConsensusEngine
|
|
8
|
+
attr_reader :agent_results, :profile_data
|
|
9
|
+
|
|
10
|
+
def initialize(agent_results, profile_data)
|
|
11
|
+
@agent_results = Array(agent_results).select(&:valid?)
|
|
12
|
+
@profile_data = profile_data
|
|
13
|
+
validate_inputs!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Generate final recommendation based on agent consensus
|
|
17
|
+
def generate_recommendation
|
|
18
|
+
return no_agents_recommendation if agent_results.empty?
|
|
19
|
+
|
|
20
|
+
consensus_data = analyze_consensus
|
|
21
|
+
action_data = determine_action(consensus_data)
|
|
22
|
+
confidence = calculate_final_confidence(consensus_data, action_data)
|
|
23
|
+
explanation = build_explanation(consensus_data, action_data)
|
|
24
|
+
|
|
25
|
+
Recommendation.new(
|
|
26
|
+
spec_location: profile_data.example_location,
|
|
27
|
+
action: action_data[:action],
|
|
28
|
+
from_value: action_data[:from_value],
|
|
29
|
+
to_value: action_data[:to_value],
|
|
30
|
+
confidence: confidence,
|
|
31
|
+
explanation: explanation,
|
|
32
|
+
agent_results: agent_results
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def validate_inputs!
|
|
39
|
+
raise ArgumentError, 'Profile data must be a ProfileData instance' unless profile_data.is_a?(ProfileData)
|
|
40
|
+
raise ArgumentError, 'Profile data must be valid' unless profile_data.valid?
|
|
41
|
+
|
|
42
|
+
agent_results.each do |result|
|
|
43
|
+
next if result.is_a?(AgentResult) && result.valid?
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, "Invalid agent result: #{result.inspect}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def analyze_consensus
|
|
50
|
+
# Group agents by their verdict types
|
|
51
|
+
verdict_groups = group_by_verdict_category
|
|
52
|
+
|
|
53
|
+
# Identify risk factors
|
|
54
|
+
risk_factors = identify_risk_factors
|
|
55
|
+
|
|
56
|
+
# Count agreement levels
|
|
57
|
+
agreement_analysis = analyze_agreement_patterns(verdict_groups)
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
verdict_groups: verdict_groups,
|
|
61
|
+
risk_factors: risk_factors,
|
|
62
|
+
agreement_analysis: agreement_analysis,
|
|
63
|
+
total_agents: agent_results.size,
|
|
64
|
+
high_confidence_agents: agent_results.count(&:high_confidence?),
|
|
65
|
+
medium_confidence_agents: agent_results.count(&:medium_confidence?),
|
|
66
|
+
low_confidence_agents: agent_results.count(&:low_confidence?)
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def group_by_verdict_category
|
|
71
|
+
optimization_verdicts = []
|
|
72
|
+
risk_verdicts = []
|
|
73
|
+
unclear_verdicts = []
|
|
74
|
+
|
|
75
|
+
agent_results.each do |result|
|
|
76
|
+
case result.verdict
|
|
77
|
+
when :db_unnecessary, :prefer_build_stubbed, :strategy_optimal
|
|
78
|
+
optimization_verdicts << result
|
|
79
|
+
when :safe_to_optimize, :potential_side_effects, :high_risk
|
|
80
|
+
risk_verdicts << result
|
|
81
|
+
when :db_unclear, :intent_unclear, :no_verdict
|
|
82
|
+
unclear_verdicts << result
|
|
83
|
+
else
|
|
84
|
+
# Categorize based on confidence and agent type
|
|
85
|
+
if result.high_confidence? || result.medium_confidence?
|
|
86
|
+
optimization_verdicts << result
|
|
87
|
+
else
|
|
88
|
+
unclear_verdicts << result
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
optimization: optimization_verdicts,
|
|
95
|
+
risk: risk_verdicts,
|
|
96
|
+
unclear: unclear_verdicts
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def identify_risk_factors
|
|
101
|
+
risk_factors = []
|
|
102
|
+
|
|
103
|
+
agent_results.each do |result|
|
|
104
|
+
risk_factors.concat(process_verdict_risk(result))
|
|
105
|
+
risk_factors.concat(process_metadata_risk(result))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
risk_factors
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def process_verdict_risk(result)
|
|
112
|
+
case result.verdict
|
|
113
|
+
when :high_risk
|
|
114
|
+
[{ type: :high_risk, agent: result.agent_name, confidence: result.confidence }]
|
|
115
|
+
when :potential_side_effects
|
|
116
|
+
[{ type: :potential_side_effects, agent: result.agent_name, confidence: result.confidence }]
|
|
117
|
+
else
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def process_metadata_risk(result)
|
|
123
|
+
return [] unless result.metadata.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
risks = []
|
|
126
|
+
risks << high_risk_score_metadata(result) if high_risk_score_metadata?(result)
|
|
127
|
+
risks << multiple_risk_factors_metadata(result) if multiple_risk_factors_metadata?(result)
|
|
128
|
+
risks.compact
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def high_risk_score_metadata?(result)
|
|
132
|
+
result.metadata[:risk_score] && result.metadata[:risk_score] > 4
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def high_risk_score_metadata(result)
|
|
136
|
+
{ type: :high_risk_score, agent: result.agent_name, score: result.metadata[:risk_score] }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def multiple_risk_factors_metadata?(result)
|
|
140
|
+
result.metadata[:total_risk_factors] && result.metadata[:total_risk_factors] > 2
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def multiple_risk_factors_metadata(result)
|
|
144
|
+
{ type: :multiple_risk_factors, agent: result.agent_name, count: result.metadata[:total_risk_factors] }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def analyze_agreement_patterns(verdict_groups)
|
|
148
|
+
optimization_agents = verdict_groups[:optimization]
|
|
149
|
+
risk_agents = verdict_groups[:risk]
|
|
150
|
+
unclear_agents = verdict_groups[:unclear]
|
|
151
|
+
|
|
152
|
+
# Count agents agreeing on optimization
|
|
153
|
+
optimization_agreement = count_optimization_agreement(optimization_agents)
|
|
154
|
+
|
|
155
|
+
# Check for conflicting signals
|
|
156
|
+
conflicts = detect_conflicts(optimization_agents, risk_agents)
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
optimization_agreement: optimization_agreement,
|
|
160
|
+
conflicts: conflicts,
|
|
161
|
+
optimization_count: optimization_agents.size,
|
|
162
|
+
risk_count: risk_agents.size,
|
|
163
|
+
unclear_count: unclear_agents.size,
|
|
164
|
+
strong_optimization_signals: optimization_agents.count do |a|
|
|
165
|
+
a.high_confidence? && optimization_verdict?(a.verdict)
|
|
166
|
+
end,
|
|
167
|
+
strong_risk_signals: risk_agents.count { |a| a.high_confidence? && risk_verdict?(a.verdict) },
|
|
168
|
+
agreement_count: optimization_agreement[:agreement_count] || 0,
|
|
169
|
+
most_common_verdict: optimization_agreement[:most_common_verdict]
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def count_optimization_agreement(optimization_agents)
|
|
174
|
+
verdict_counts = Hash.new(0)
|
|
175
|
+
|
|
176
|
+
optimization_agents.each do |agent|
|
|
177
|
+
# Normalize similar verdicts
|
|
178
|
+
normalized_verdict = normalize_verdict(agent.verdict)
|
|
179
|
+
verdict_counts[normalized_verdict] += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Find the most common optimization verdict
|
|
183
|
+
max_count = verdict_counts.values.max || 0
|
|
184
|
+
most_common_verdict = verdict_counts.key(max_count)
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
most_common_verdict: most_common_verdict,
|
|
188
|
+
agreement_count: max_count,
|
|
189
|
+
verdict_distribution: verdict_counts
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def normalize_verdict(verdict)
|
|
194
|
+
case verdict
|
|
195
|
+
when :db_unnecessary, :prefer_build_stubbed
|
|
196
|
+
:optimize_persistence
|
|
197
|
+
when :db_required, :create_required
|
|
198
|
+
:require_persistence
|
|
199
|
+
when :unit_test_behavior
|
|
200
|
+
:unit_test
|
|
201
|
+
when :integration_test_behavior
|
|
202
|
+
:integration_test
|
|
203
|
+
else
|
|
204
|
+
verdict
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def detect_conflicts(optimization_agents, risk_agents)
|
|
209
|
+
conflicts = []
|
|
210
|
+
|
|
211
|
+
# Conflict: optimization agents suggest changes but risk agents flag dangers
|
|
212
|
+
if optimization_agents.any?(&:high_confidence?) && risk_agents.any? { |a| a.verdict == :high_risk }
|
|
213
|
+
conflicts << {
|
|
214
|
+
type: :optimization_vs_high_risk,
|
|
215
|
+
optimization_agents: optimization_agents.select(&:high_confidence?).map(&:agent_name),
|
|
216
|
+
risk_agents: risk_agents.select { |a| a.verdict == :high_risk }.map(&:agent_name)
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Conflict: agents disagree on persistence requirements
|
|
221
|
+
db_unnecessary = optimization_agents.select { |a| a.verdict == :db_unnecessary }
|
|
222
|
+
db_required = optimization_agents.select { |a| a.verdict == :db_required }
|
|
223
|
+
|
|
224
|
+
if db_unnecessary.any? && db_required.any?
|
|
225
|
+
conflicts << {
|
|
226
|
+
type: :persistence_disagreement,
|
|
227
|
+
unnecessary_agents: db_unnecessary.map(&:agent_name),
|
|
228
|
+
required_agents: db_required.map(&:agent_name)
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
conflicts
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def determine_action(consensus_data)
|
|
236
|
+
agreement = consensus_data[:agreement_analysis]
|
|
237
|
+
risk_factors = consensus_data[:risk_factors]
|
|
238
|
+
|
|
239
|
+
# High risk scenarios - no action
|
|
240
|
+
return no_action_high_risk(risk_factors) if high_risk_scenario?(risk_factors)
|
|
241
|
+
|
|
242
|
+
# Strong agreement scenarios
|
|
243
|
+
return strong_recommendation_action(agreement, risk_factors) if strong_agreement?(agreement)
|
|
244
|
+
|
|
245
|
+
# Conflicting agents scenarios
|
|
246
|
+
return soft_suggestion_action(agreement, risk_factors) if conflicting_agents?(agreement)
|
|
247
|
+
|
|
248
|
+
# Unclear signals scenarios
|
|
249
|
+
no_action_unclear_signals(consensus_data)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def high_risk_scenario?(risk_factors)
|
|
253
|
+
risk_factors.any? { |factor| factor[:type] == :high_risk } ||
|
|
254
|
+
risk_factors.count { |factor| factor[:type] == :potential_side_effects } >= 2
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def strong_agreement?(agreement)
|
|
258
|
+
agreement[:agreement_count] >= 2 && agreement[:strong_optimization_signals] >= 1
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def conflicting_agents?(agreement)
|
|
262
|
+
agreement[:conflicts].any? ||
|
|
263
|
+
(agreement[:optimization_count] >= 1 && agreement[:risk_count] >= 1)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def no_action_high_risk(risk_factors)
|
|
267
|
+
high_risk_factors = risk_factors.select { |f| f[:type] == :high_risk }
|
|
268
|
+
risk_agents = high_risk_factors.map { |f| f[:agent] }.join(', ')
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
action: :no_action,
|
|
272
|
+
from_value: '',
|
|
273
|
+
to_value: '',
|
|
274
|
+
reason: :high_risk,
|
|
275
|
+
risk_agents: risk_agents
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def strong_recommendation_action(agreement, risk_factors)
|
|
280
|
+
most_common = agreement[:most_common_verdict]
|
|
281
|
+
|
|
282
|
+
case most_common
|
|
283
|
+
when :optimize_persistence
|
|
284
|
+
factory_optimization_action(risk_factors)
|
|
285
|
+
when :require_persistence
|
|
286
|
+
maintain_persistence_action
|
|
287
|
+
else
|
|
288
|
+
review_action(most_common)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def factory_optimization_action(risk_factors)
|
|
293
|
+
# Check if we have specific factory data to make concrete recommendations
|
|
294
|
+
factory_data = extract_factory_data
|
|
295
|
+
|
|
296
|
+
if factory_data && risk_factors.none?
|
|
297
|
+
{
|
|
298
|
+
action: :replace_factory_strategy,
|
|
299
|
+
from_value: factory_data[:from_value],
|
|
300
|
+
to_value: factory_data[:to_value],
|
|
301
|
+
reason: :optimization_agreement
|
|
302
|
+
}
|
|
303
|
+
else
|
|
304
|
+
{
|
|
305
|
+
action: :avoid_db_persistence,
|
|
306
|
+
from_value: 'create strategy',
|
|
307
|
+
to_value: 'build_stubbed strategy',
|
|
308
|
+
reason: :optimization_agreement
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def maintain_persistence_action
|
|
314
|
+
{
|
|
315
|
+
action: :no_action,
|
|
316
|
+
from_value: '',
|
|
317
|
+
to_value: '',
|
|
318
|
+
reason: :persistence_required
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def review_action(verdict)
|
|
323
|
+
{
|
|
324
|
+
action: :review_test_intent,
|
|
325
|
+
from_value: '',
|
|
326
|
+
to_value: '',
|
|
327
|
+
reason: :review_needed,
|
|
328
|
+
verdict: verdict
|
|
329
|
+
}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def soft_suggestion_action(agreement, _risk_factors)
|
|
333
|
+
# Generate soft suggestions when agents conflict
|
|
334
|
+
if agreement[:conflicts].any? { |c| c[:type] == :optimization_vs_high_risk }
|
|
335
|
+
{
|
|
336
|
+
action: :assess_risk_factors,
|
|
337
|
+
from_value: '',
|
|
338
|
+
to_value: '',
|
|
339
|
+
reason: :conflicting_risk_assessment
|
|
340
|
+
}
|
|
341
|
+
elsif agreement[:conflicts].any? { |c| c[:type] == :persistence_disagreement }
|
|
342
|
+
{
|
|
343
|
+
action: :review_test_intent,
|
|
344
|
+
from_value: '',
|
|
345
|
+
to_value: '',
|
|
346
|
+
reason: :conflicting_persistence_needs
|
|
347
|
+
}
|
|
348
|
+
else
|
|
349
|
+
{
|
|
350
|
+
action: :no_action,
|
|
351
|
+
from_value: '',
|
|
352
|
+
to_value: '',
|
|
353
|
+
reason: :conflicting_signals
|
|
354
|
+
}
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def no_action_unclear_signals(consensus_data)
|
|
359
|
+
{
|
|
360
|
+
action: :no_action,
|
|
361
|
+
from_value: '',
|
|
362
|
+
to_value: '',
|
|
363
|
+
reason: :unclear_signals,
|
|
364
|
+
unclear_count: consensus_data[:agreement_analysis][:unclear_count]
|
|
365
|
+
}
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def extract_factory_data
|
|
369
|
+
# Look for factory-specific recommendations in agent results
|
|
370
|
+
factory_agents = agent_results.select { |r| r.agent_name == :factory }
|
|
371
|
+
return nil if factory_agents.empty?
|
|
372
|
+
|
|
373
|
+
factory_agent = factory_agents.first
|
|
374
|
+
return nil unless factory_agent.verdict == :prefer_build_stubbed
|
|
375
|
+
|
|
376
|
+
# Extract factory information from profile data
|
|
377
|
+
return nil unless profile_data.factories.is_a?(Hash) && profile_data.factories.any?
|
|
378
|
+
|
|
379
|
+
factory_name, factory_info = profile_data.factories.first
|
|
380
|
+
return nil unless factory_info.is_a?(Hash) && factory_info[:strategy] == :create
|
|
381
|
+
|
|
382
|
+
{
|
|
383
|
+
from_value: "create(:#{factory_name})",
|
|
384
|
+
to_value: "build_stubbed(:#{factory_name})"
|
|
385
|
+
}
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def calculate_final_confidence(consensus_data, action_data)
|
|
389
|
+
return :low if action_data[:action] == :no_action
|
|
390
|
+
|
|
391
|
+
agreement = consensus_data[:agreement_analysis]
|
|
392
|
+
risk_factors = consensus_data[:risk_factors]
|
|
393
|
+
|
|
394
|
+
# Start with base confidence from agreement strength
|
|
395
|
+
base_confidence = calculate_base_confidence(agreement)
|
|
396
|
+
|
|
397
|
+
# Apply risk-based downgrading
|
|
398
|
+
apply_risk_downgrading(base_confidence, risk_factors)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def calculate_base_confidence(agreement)
|
|
402
|
+
strong_signals = agreement[:strong_optimization_signals] || 0
|
|
403
|
+
agreement_count = agreement[:agreement_count] || 0
|
|
404
|
+
|
|
405
|
+
return :high if high_confidence?(strong_signals, agreement_count)
|
|
406
|
+
return :medium if medium_confidence?(strong_signals, agreement_count)
|
|
407
|
+
|
|
408
|
+
:low
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def high_confidence?(strong_signals, agreement_count)
|
|
412
|
+
(strong_signals >= 2 && agreement_count >= 3) ||
|
|
413
|
+
(strong_signals >= 2 && agreement_count >= 2)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def medium_confidence?(strong_signals, agreement_count)
|
|
417
|
+
(strong_signals >= 1 && agreement_count >= 2) ||
|
|
418
|
+
(agreement_count >= 2)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def apply_risk_downgrading(base_confidence, risk_factors)
|
|
422
|
+
return base_confidence if risk_factors.empty?
|
|
423
|
+
|
|
424
|
+
# High risk factors force low confidence
|
|
425
|
+
return :low if risk_factors.any? { |f| f[:type] == :high_risk }
|
|
426
|
+
|
|
427
|
+
# Multiple potential side effects downgrade confidence
|
|
428
|
+
potential_side_effects = risk_factors.count { |f| f[:type] == :potential_side_effects }
|
|
429
|
+
if potential_side_effects >= 2
|
|
430
|
+
return base_confidence == :high ? :medium : :low
|
|
431
|
+
elsif potential_side_effects >= 1
|
|
432
|
+
return base_confidence == :high ? :medium : base_confidence
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
base_confidence
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def build_explanation(consensus_data, action_data)
|
|
439
|
+
explanation = []
|
|
440
|
+
|
|
441
|
+
# Add agent summary
|
|
442
|
+
explanation << build_agent_summary(consensus_data)
|
|
443
|
+
|
|
444
|
+
# Add consensus analysis
|
|
445
|
+
explanation << build_consensus_analysis(consensus_data)
|
|
446
|
+
|
|
447
|
+
# Add action reasoning
|
|
448
|
+
explanation << build_action_reasoning(action_data)
|
|
449
|
+
|
|
450
|
+
# Add risk factors if present
|
|
451
|
+
explanation << build_risk_explanation(consensus_data[:risk_factors]) if consensus_data[:risk_factors].any?
|
|
452
|
+
|
|
453
|
+
explanation.compact
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def build_agent_summary(consensus_data)
|
|
457
|
+
total = consensus_data[:total_agents]
|
|
458
|
+
high_conf = consensus_data[:high_confidence_agents]
|
|
459
|
+
medium_conf = consensus_data[:medium_confidence_agents]
|
|
460
|
+
low_conf = consensus_data[:low_confidence_agents]
|
|
461
|
+
|
|
462
|
+
"Analyzed #{total} agent(s): #{high_conf} high confidence, #{medium_conf} medium confidence, #{low_conf} low confidence"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def build_consensus_analysis(consensus_data)
|
|
466
|
+
agreement = consensus_data[:agreement_analysis]
|
|
467
|
+
|
|
468
|
+
if agreement[:agreement_count] >= 2
|
|
469
|
+
"#{agreement[:agreement_count]} agent(s) agree on #{agreement[:most_common_verdict]} approach"
|
|
470
|
+
elsif agreement[:conflicts].any?
|
|
471
|
+
conflict_types = agreement[:conflicts].map { |c| c[:type] }.join(', ')
|
|
472
|
+
"Conflicting agent opinions detected: #{conflict_types}"
|
|
473
|
+
else
|
|
474
|
+
'No clear consensus among agents'
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def build_action_reasoning(action_data)
|
|
479
|
+
case action_data[:reason]
|
|
480
|
+
when :optimization_agreement
|
|
481
|
+
'Strong agreement supports optimization recommendation'
|
|
482
|
+
when :high_risk
|
|
483
|
+
"High risk factors prevent optimization (flagged by: #{action_data[:risk_agents]})"
|
|
484
|
+
when :conflicting_risk_assessment
|
|
485
|
+
'Risk assessment conflicts with optimization signals - manual review recommended'
|
|
486
|
+
when :conflicting_persistence_needs
|
|
487
|
+
'Agents disagree on persistence requirements - review test intent'
|
|
488
|
+
when :conflicting_signals
|
|
489
|
+
'Mixed signals from agents - no clear action'
|
|
490
|
+
when :unclear_signals
|
|
491
|
+
"Insufficient clear signals for recommendation (#{action_data[:unclear_count]} unclear)"
|
|
492
|
+
when :persistence_required
|
|
493
|
+
'Analysis indicates database persistence is necessary'
|
|
494
|
+
when :review_needed
|
|
495
|
+
"Test intent review recommended based on #{action_data[:verdict]} signals"
|
|
496
|
+
else
|
|
497
|
+
'Action determined based on agent consensus'
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def build_risk_explanation(risk_factors)
|
|
502
|
+
return nil if risk_factors.empty?
|
|
503
|
+
|
|
504
|
+
risk_summary = risk_factors.group_by { |f| f[:type] }
|
|
505
|
+
.map { |type, factors| "#{factors.size} #{humanize_symbol(type)}" }
|
|
506
|
+
.join(', ')
|
|
507
|
+
|
|
508
|
+
"Risk factors detected: #{risk_summary}"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def humanize_symbol(symbol)
|
|
512
|
+
symbol.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def no_agents_recommendation
|
|
516
|
+
Recommendation.new(
|
|
517
|
+
spec_location: profile_data.example_location,
|
|
518
|
+
action: :no_action,
|
|
519
|
+
from_value: '',
|
|
520
|
+
to_value: '',
|
|
521
|
+
confidence: :low,
|
|
522
|
+
explanation: ['No valid agent results available for analysis'],
|
|
523
|
+
agent_results: []
|
|
524
|
+
)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def optimization_verdict?(verdict)
|
|
528
|
+
%i[db_unnecessary prefer_build_stubbed optimize_persistence].include?(verdict)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def risk_verdict?(verdict)
|
|
532
|
+
%i[safe_to_optimize potential_side_effects high_risk].include?(verdict)
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecScout
|
|
4
|
+
# Enforcement mode handler for CI-friendly enforcement configuration
|
|
5
|
+
# Handles high-confidence failure conditions and graceful enforcement
|
|
6
|
+
class EnforcementHandler
|
|
7
|
+
class EnforcementFailureError < StandardError
|
|
8
|
+
attr_reader :recommendation, :confidence_level
|
|
9
|
+
|
|
10
|
+
def initialize(message, recommendation = nil, confidence_level = nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@recommendation = recommendation
|
|
13
|
+
@confidence_level = confidence_level
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Check if enforcement mode is enabled
|
|
22
|
+
def enforcement_enabled?
|
|
23
|
+
@config.enforcement_mode?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Determine if a recommendation should cause enforcement failure
|
|
27
|
+
def should_fail?(recommendation)
|
|
28
|
+
return false unless enforcement_enabled?
|
|
29
|
+
return false unless recommendation
|
|
30
|
+
|
|
31
|
+
recommendation.confidence == case @config.fail_on_high_confidence
|
|
32
|
+
when true
|
|
33
|
+
:high
|
|
34
|
+
else
|
|
35
|
+
# Default enforcement: fail on high confidence recommendations
|
|
36
|
+
:high
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Handle enforcement for a recommendation
|
|
41
|
+
def handle_enforcement(recommendation, profile_data = nil)
|
|
42
|
+
return { should_fail: false, exit_code: 0 } unless enforcement_enabled?
|
|
43
|
+
|
|
44
|
+
if should_fail?(recommendation)
|
|
45
|
+
handle_enforcement_failure(recommendation, profile_data)
|
|
46
|
+
else
|
|
47
|
+
handle_enforcement_success(recommendation)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Generate CI-friendly exit codes
|
|
52
|
+
def exit_code_for_recommendation(recommendation)
|
|
53
|
+
return 0 unless enforcement_enabled?
|
|
54
|
+
|
|
55
|
+
case recommendation&.confidence
|
|
56
|
+
when :high
|
|
57
|
+
should_fail?(recommendation) ? 1 : 0
|
|
58
|
+
when :medium
|
|
59
|
+
0 # Medium confidence never fails in enforcement mode
|
|
60
|
+
when :low
|
|
61
|
+
0 # Low confidence never fails in enforcement mode
|
|
62
|
+
else
|
|
63
|
+
0 # Unknown confidence defaults to success
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Format enforcement message for CI output
|
|
68
|
+
def format_enforcement_message(recommendation, profile_data = nil)
|
|
69
|
+
return nil unless enforcement_enabled? && recommendation
|
|
70
|
+
|
|
71
|
+
if should_fail?(recommendation)
|
|
72
|
+
format_failure_message(recommendation, profile_data)
|
|
73
|
+
else
|
|
74
|
+
format_success_message(recommendation)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if enforcement mode is configured correctly for CI
|
|
79
|
+
def ci_friendly?
|
|
80
|
+
return true unless enforcement_enabled?
|
|
81
|
+
|
|
82
|
+
# CI-friendly enforcement should:
|
|
83
|
+
# 1. Only fail on high confidence recommendations
|
|
84
|
+
# 2. Provide clear exit codes
|
|
85
|
+
# 3. Have structured output
|
|
86
|
+
@config.fail_on_high_confidence &&
|
|
87
|
+
(@config.output_format == :json || @config.console_output?)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Validate enforcement configuration
|
|
91
|
+
def validate_enforcement_config!
|
|
92
|
+
return unless enforcement_enabled?
|
|
93
|
+
|
|
94
|
+
unless ci_friendly?
|
|
95
|
+
warn 'Warning: Enforcement mode may not be CI-friendly. Consider enabling fail_on_high_confidence and structured output.'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return unless @config.auto_apply_enabled?
|
|
99
|
+
|
|
100
|
+
raise EnforcementFailureError,
|
|
101
|
+
'Enforcement mode with auto-apply is dangerous and not recommended for CI environments'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get enforcement status summary
|
|
105
|
+
def enforcement_status
|
|
106
|
+
{
|
|
107
|
+
enabled: enforcement_enabled?,
|
|
108
|
+
fail_on_high_confidence: @config.fail_on_high_confidence,
|
|
109
|
+
ci_friendly: ci_friendly?,
|
|
110
|
+
auto_apply_disabled: !@config.auto_apply_enabled?,
|
|
111
|
+
output_format: @config.output_format
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def handle_enforcement_failure(recommendation, profile_data)
|
|
118
|
+
message = format_failure_message(recommendation, profile_data)
|
|
119
|
+
|
|
120
|
+
if @config.console_output?
|
|
121
|
+
puts "\n❌ Enforcement Mode: Action Required"
|
|
122
|
+
puts message
|
|
123
|
+
puts "\nThis recommendation requires immediate attention."
|
|
124
|
+
puts 'Exit code: 1'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
should_fail: true,
|
|
129
|
+
exit_code: 1,
|
|
130
|
+
enforcement_message: message,
|
|
131
|
+
recommendation: recommendation
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_enforcement_success(recommendation)
|
|
136
|
+
message = format_success_message(recommendation)
|
|
137
|
+
|
|
138
|
+
if @config.console_output? && recommendation
|
|
139
|
+
puts "\n✅ Enforcement Mode: Recommendation Noted"
|
|
140
|
+
puts message
|
|
141
|
+
puts 'Exit code: 0'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
should_fail: false,
|
|
146
|
+
exit_code: 0,
|
|
147
|
+
enforcement_message: message,
|
|
148
|
+
recommendation: recommendation
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def format_failure_message(recommendation, profile_data)
|
|
153
|
+
lines = []
|
|
154
|
+
lines << 'High confidence recommendation requires action:'
|
|
155
|
+
lines << " Location: #{recommendation.spec_location}"
|
|
156
|
+
lines << " Action: #{recommendation.action}"
|
|
157
|
+
|
|
158
|
+
if recommendation.from_value && recommendation.to_value
|
|
159
|
+
lines << " Change: #{recommendation.from_value} → #{recommendation.to_value}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
lines << " Confidence: #{recommendation.confidence.to_s.upcase}"
|
|
163
|
+
lines << " Explanation: #{Array(recommendation.explanation).join('; ')}"
|
|
164
|
+
|
|
165
|
+
lines << " Runtime: #{profile_data.runtime_ms}ms" if profile_data
|
|
166
|
+
|
|
167
|
+
lines.join("\n")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_success_message(recommendation)
|
|
171
|
+
return 'No high confidence recommendations found.' unless recommendation
|
|
172
|
+
|
|
173
|
+
lines = []
|
|
174
|
+
lines << "Recommendation noted (#{recommendation.confidence} confidence):"
|
|
175
|
+
lines << " Location: #{recommendation.spec_location}"
|
|
176
|
+
lines << " Action: #{recommendation.action}"
|
|
177
|
+
lines << ' No immediate action required in enforcement mode.'
|
|
178
|
+
|
|
179
|
+
lines.join("\n")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|