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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +10 -0
  3. data/.idea/Projects.iml +41 -0
  4. data/.idea/copilot.data.migration.ask2agent.xml +6 -0
  5. data/.idea/modules.xml +8 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rspec_status +236 -0
  8. data/Gemfile +11 -0
  9. data/Gemfile.lock +72 -0
  10. data/LICENSE +21 -0
  11. data/README.md +433 -0
  12. data/Rakefile +12 -0
  13. data/examples/README.md +321 -0
  14. data/examples/best_practices.md +401 -0
  15. data/examples/configurations/basic_config.rb +24 -0
  16. data/examples/configurations/ci_config.rb +35 -0
  17. data/examples/configurations/conservative_config.rb +32 -0
  18. data/examples/configurations/development_config.rb +37 -0
  19. data/examples/configurations/performance_focused_config.rb +38 -0
  20. data/examples/output_formatter_demo.rb +67 -0
  21. data/examples/sample_outputs/console_output_high_confidence.txt +27 -0
  22. data/examples/sample_outputs/console_output_medium_confidence.txt +27 -0
  23. data/examples/sample_outputs/console_output_no_action.txt +27 -0
  24. data/examples/sample_outputs/console_output_risk_detected.txt +27 -0
  25. data/examples/sample_outputs/json_output_high_confidence.json +108 -0
  26. data/examples/sample_outputs/json_output_no_action.json +108 -0
  27. data/examples/workflows/basic_workflow.md +159 -0
  28. data/examples/workflows/ci_integration.md +372 -0
  29. data/exe/spec_scout +7 -0
  30. data/lib/spec_scout/agent_result.rb +44 -0
  31. data/lib/spec_scout/agents/database_agent.rb +113 -0
  32. data/lib/spec_scout/agents/factory_agent.rb +179 -0
  33. data/lib/spec_scout/agents/intent_agent.rb +223 -0
  34. data/lib/spec_scout/agents/risk_agent.rb +290 -0
  35. data/lib/spec_scout/base_agent.rb +72 -0
  36. data/lib/spec_scout/cli.rb +158 -0
  37. data/lib/spec_scout/configuration.rb +162 -0
  38. data/lib/spec_scout/consensus_engine.rb +535 -0
  39. data/lib/spec_scout/enforcement_handler.rb +182 -0
  40. data/lib/spec_scout/output_formatter.rb +307 -0
  41. data/lib/spec_scout/profile_data.rb +37 -0
  42. data/lib/spec_scout/profile_normalizer.rb +238 -0
  43. data/lib/spec_scout/recommendation.rb +62 -0
  44. data/lib/spec_scout/safety_validator.rb +127 -0
  45. data/lib/spec_scout/spec_scout.rb +519 -0
  46. data/lib/spec_scout/testprof_integration.rb +206 -0
  47. data/lib/spec_scout/version.rb +5 -0
  48. data/lib/spec_scout.rb +43 -0
  49. 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