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,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_agent'
4
+
5
+ module SpecScout
6
+ module Agents
7
+ # Agent that evaluates FactoryBot strategy appropriateness and recommends optimizations
8
+ class FactoryAgent < BaseAgent
9
+ # Verdict types for factory strategy optimization
10
+ VERDICTS = {
11
+ prefer_build_stubbed: :prefer_build_stubbed,
12
+ create_required: :create_required,
13
+ strategy_optimal: :strategy_optimal
14
+ }.freeze
15
+
16
+ def evaluate
17
+ return no_factory_data_result unless factories_present?
18
+
19
+ factory_analysis = analyze_factory_usage
20
+
21
+ verdict, confidence, reasoning = determine_recommendation(factory_analysis)
22
+
23
+ create_result(
24
+ verdict: verdict,
25
+ confidence: confidence,
26
+ reasoning: reasoning,
27
+ metadata: factory_analysis
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def analyze_factory_usage
34
+ create_count = 0
35
+ build_stubbed_count = 0
36
+ total_factories = 0
37
+ factory_details = {}
38
+ association_access_detected = false
39
+
40
+ profile_data.factories.each do |factory_name, factory_data|
41
+ next unless factory_data.is_a?(Hash)
42
+
43
+ strategy = factory_data[:strategy] || :unknown
44
+ count = factory_data[:count] || 0
45
+ associations = factory_data[:associations] || []
46
+
47
+ total_factories += count
48
+ factory_details[factory_name] = factory_data
49
+
50
+ # Check for association access patterns
51
+ association_access_detected = true if associations.any? || association_indicators?(factory_data)
52
+
53
+ case strategy
54
+ when :create
55
+ create_count += count
56
+ when :build_stubbed
57
+ build_stubbed_count += count
58
+ end
59
+ end
60
+
61
+ {
62
+ create_count: create_count,
63
+ build_stubbed_count: build_stubbed_count,
64
+ total_factories: total_factories,
65
+ factory_details: factory_details,
66
+ database_writes: profile_data.db[:inserts] || 0,
67
+ association_access_detected: association_access_detected
68
+ }
69
+ end
70
+
71
+ def determine_recommendation(analysis)
72
+ create_count = analysis[:create_count]
73
+ build_stubbed_count = analysis[:build_stubbed_count]
74
+ database_writes = analysis[:database_writes]
75
+ association_access = analysis[:association_access_detected]
76
+
77
+ if prefer_build_stubbed?(create_count, database_writes, association_access)
78
+ prefer_build_stubbed_result(create_count)
79
+ elsif create_required_association?(create_count, association_access)
80
+ create_required_association_result
81
+ elsif create_required_db?(create_count, database_writes)
82
+ create_required_db_result(database_writes, create_count)
83
+ elsif strategy_optimal?(build_stubbed_count, create_count)
84
+ strategy_optimal_result(build_stubbed_count)
85
+ else
86
+ mixed_usage_result
87
+ end
88
+ end
89
+
90
+ def prefer_build_stubbed?(create_count, database_writes, association_access)
91
+ create_count.positive? && database_writes.zero? && !association_access
92
+ end
93
+
94
+ def create_required_association?(create_count, association_access)
95
+ create_count.positive? && association_access
96
+ end
97
+
98
+ def create_required_db?(create_count, database_writes)
99
+ create_count.positive? && database_writes.positive?
100
+ end
101
+
102
+ def strategy_optimal?(build_stubbed_count, create_count)
103
+ build_stubbed_count.positive? && create_count.zero?
104
+ end
105
+
106
+ def prefer_build_stubbed_result(create_count)
107
+ [
108
+ VERDICTS[:prefer_build_stubbed],
109
+ :medium,
110
+ "Using create strategy (#{create_count} factories) but no database writes or association access detected. " \
111
+ 'Consider using build_stubbed for better performance.'
112
+ ]
113
+ end
114
+
115
+ def create_required_association_result
116
+ [
117
+ VERDICTS[:create_required],
118
+ :medium,
119
+ 'Using create strategy with association access patterns detected. ' \
120
+ 'Factory persistence may be necessary for association handling.'
121
+ ]
122
+ end
123
+
124
+ def create_required_db_result(database_writes, _create_count)
125
+ [
126
+ VERDICTS[:create_required],
127
+ :medium,
128
+ "Using create strategy with database writes (#{database_writes} inserts). " \
129
+ 'Factory persistence appears necessary.'
130
+ ]
131
+ end
132
+
133
+ def strategy_optimal_result(build_stubbed_count)
134
+ [
135
+ VERDICTS[:strategy_optimal],
136
+ :high,
137
+ "Already using build_stubbed strategy (#{build_stubbed_count} factories). " \
138
+ 'Factory strategy is optimized.'
139
+ ]
140
+ end
141
+
142
+ def mixed_usage_result
143
+ [
144
+ VERDICTS[:strategy_optimal],
145
+ :low,
146
+ 'Mixed factory usage pattern. Current strategy appears reasonable.'
147
+ ]
148
+ end
149
+
150
+ def no_factory_data_result
151
+ create_result(
152
+ verdict: VERDICTS[:strategy_optimal],
153
+ confidence: :low,
154
+ reasoning: 'No factory usage data available for analysis.',
155
+ metadata: { no_data: true }
156
+ )
157
+ end
158
+
159
+ # Helper method to detect association access patterns in factory data
160
+ def association_indicators?(factory_data)
161
+ traits_with_association?(factory_data) ||
162
+ attributes_with_foreign_key?(factory_data) ||
163
+ build_strategy_with_associations?(factory_data)
164
+ end
165
+
166
+ def traits_with_association?(factory_data)
167
+ factory_data[:traits]&.any? { |trait| trait.to_s.include?('with_') }
168
+ end
169
+
170
+ def attributes_with_foreign_key?(factory_data)
171
+ factory_data[:attributes]&.keys&.any? { |attr| attr.to_s.end_with?('_id') }
172
+ end
173
+
174
+ def build_strategy_with_associations?(factory_data)
175
+ factory_data[:build_strategy] == :create && factory_data[:associations_count].to_i.positive?
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_agent'
4
+
5
+ module SpecScout
6
+ module Agents
7
+ # Agent that classifies test intent and behavior patterns to determine
8
+ # if tests are behaving as unit or integration tests
9
+ class IntentAgent < BaseAgent
10
+ # Verdict types for test intent classification
11
+ VERDICTS = {
12
+ unit_test_behavior: :unit_test_behavior,
13
+ integration_test_behavior: :integration_test_behavior,
14
+ intent_unclear: :intent_unclear
15
+ }.freeze
16
+
17
+ # File path patterns that indicate different test types
18
+ UNIT_TEST_PATTERNS = [
19
+ %r{spec/models/},
20
+ %r{spec/lib/},
21
+ %r{spec/services/},
22
+ %r{spec/helpers/},
23
+ %r{spec/presenters/},
24
+ %r{spec/decorators/},
25
+ %r{spec/serializers/},
26
+ %r{spec/validators/},
27
+ %r{spec/concerns/},
28
+ %r{spec/jobs/}
29
+ ].freeze
30
+
31
+ INTEGRATION_TEST_PATTERNS = [
32
+ %r{spec/features/},
33
+ %r{spec/integration/},
34
+ %r{spec/system/},
35
+ %r{spec/requests/},
36
+ %r{spec/controllers/},
37
+ %r{spec/routing/},
38
+ %r{spec/views/},
39
+ %r{spec/mailers/}
40
+ ].freeze
41
+
42
+ def evaluate
43
+ return no_location_data_result if profile_data.example_location.empty?
44
+
45
+ file_location_signal = analyze_file_location
46
+ runtime_behavior_signal = analyze_runtime_behavior
47
+ database_usage_signal = analyze_database_usage
48
+ factory_usage_signal = analyze_factory_usage
49
+
50
+ verdict, confidence, reasoning = determine_intent(
51
+ file_location: file_location_signal,
52
+ runtime_behavior: runtime_behavior_signal,
53
+ database_usage: database_usage_signal,
54
+ factory_usage: factory_usage_signal
55
+ )
56
+
57
+ create_result(
58
+ verdict: verdict,
59
+ confidence: confidence,
60
+ reasoning: reasoning,
61
+ metadata: {
62
+ file_location_signal: file_location_signal,
63
+ runtime_behavior_signal: runtime_behavior_signal,
64
+ database_usage_signal: database_usage_signal,
65
+ factory_usage_signal: factory_usage_signal,
66
+ runtime_ms: profile_data.runtime_ms
67
+ }
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ def analyze_file_location
74
+ location = profile_data.example_location.downcase
75
+
76
+ if UNIT_TEST_PATTERNS.any? { |pattern| location.match?(pattern) }
77
+ :unit_test_location
78
+ elsif INTEGRATION_TEST_PATTERNS.any? { |pattern| location.match?(pattern) }
79
+ :integration_test_location
80
+ else
81
+ :unclear_location
82
+ end
83
+ end
84
+
85
+ def analyze_runtime_behavior
86
+ runtime = profile_data.runtime_ms
87
+
88
+ case runtime
89
+ when 0..10
90
+ :fast_execution # Typical unit test speed
91
+ when 11..100
92
+ :moderate_execution # Could be either
93
+ else
94
+ :slow_execution # Likely integration test
95
+ end
96
+ end
97
+
98
+ def analyze_database_usage
99
+ return :no_database_data unless database_operations_present?
100
+
101
+ total_queries = profile_data.db[:total_queries] || 0
102
+ profile_data.db[:inserts] || 0
103
+
104
+ case total_queries
105
+ when 0..2
106
+ :minimal_database # Unit test pattern
107
+ when 3..10
108
+ :moderate_database # Could be either
109
+ else
110
+ :heavy_database # Integration test pattern
111
+ end
112
+ end
113
+
114
+ def analyze_factory_usage
115
+ return :no_factory_data unless factories_present?
116
+
117
+ factory_count = profile_data.factories.values.sum { |factory| factory[:count] || 0 }
118
+ create_usage = profile_data.factories.values.count { |factory| factory[:strategy] == :create }
119
+
120
+ if factory_count <= 1 && create_usage.zero?
121
+ :minimal_factories # Unit test pattern
122
+ elsif factory_count > 5 || create_usage > 3
123
+ :heavy_factories # Integration test pattern
124
+ else
125
+ :moderate_factories # Could be either
126
+ end
127
+ end
128
+
129
+ def determine_intent(file_location:, runtime_behavior:, database_usage:, factory_usage:)
130
+ unit_signals = count_unit_signals(file_location, runtime_behavior, database_usage, factory_usage)
131
+ integration_signals = count_integration_signals(file_location, runtime_behavior, database_usage, factory_usage)
132
+
133
+ if unit_signals >= 3
134
+ unit_test_result(unit_signals, integration_signals)
135
+ elsif integration_signals >= 3
136
+ integration_test_result(unit_signals, integration_signals)
137
+ elsif unit_signals >= 2 && integration_signals.zero?
138
+ unit_test_result_medium_confidence(unit_signals, integration_signals)
139
+ elsif integration_signals >= 2 && unit_signals.zero?
140
+ integration_test_result_medium_confidence(unit_signals, integration_signals)
141
+ elsif unit_signals > integration_signals && unit_signals >= 1
142
+ unit_test_result_medium_confidence(unit_signals, integration_signals)
143
+ elsif integration_signals > unit_signals && integration_signals >= 1
144
+ integration_test_result_medium_confidence(unit_signals, integration_signals)
145
+ else
146
+ unclear_intent_result(unit_signals, integration_signals)
147
+ end
148
+ end
149
+
150
+ def count_unit_signals(file_location, runtime_behavior, database_usage, factory_usage)
151
+ signals = 0
152
+ signals += 1 if file_location == :unit_test_location
153
+ signals += 1 if runtime_behavior == :fast_execution
154
+ signals += 1 if database_usage == :minimal_database
155
+ signals += 1 if factory_usage == :minimal_factories
156
+ signals
157
+ end
158
+
159
+ def count_integration_signals(file_location, runtime_behavior, database_usage, factory_usage)
160
+ signals = 0
161
+ signals += 1 if file_location == :integration_test_location
162
+ signals += 1 if runtime_behavior == :slow_execution
163
+ signals += 1 if database_usage == :heavy_database
164
+ signals += 1 if factory_usage == :heavy_factories
165
+ signals
166
+ end
167
+
168
+ def unit_test_result(unit_signals, integration_signals)
169
+ [
170
+ VERDICTS[:unit_test_behavior],
171
+ :high,
172
+ "Strong unit test behavior detected (#{unit_signals} unit signals, #{integration_signals} integration signals). " \
173
+ 'Test appears to focus on isolated component behavior.'
174
+ ]
175
+ end
176
+
177
+ def integration_test_result(unit_signals, integration_signals)
178
+ [
179
+ VERDICTS[:integration_test_behavior],
180
+ :high,
181
+ "Strong integration test behavior detected (#{integration_signals} integration signals, #{unit_signals} unit signals). " \
182
+ 'Test appears to cross integration boundaries.'
183
+ ]
184
+ end
185
+
186
+ def unit_test_result_medium_confidence(unit_signals, integration_signals)
187
+ [
188
+ VERDICTS[:unit_test_behavior],
189
+ :medium,
190
+ "Likely unit test behavior (#{unit_signals} unit signals, #{integration_signals} integration signals). " \
191
+ 'Some mixed signals present but unit test patterns dominate.'
192
+ ]
193
+ end
194
+
195
+ def integration_test_result_medium_confidence(unit_signals, integration_signals)
196
+ [
197
+ VERDICTS[:integration_test_behavior],
198
+ :medium,
199
+ "Likely integration test behavior (#{integration_signals} integration signals, #{unit_signals} unit signals). " \
200
+ 'Some mixed signals present but integration test patterns dominate.'
201
+ ]
202
+ end
203
+
204
+ def unclear_intent_result(unit_signals, integration_signals)
205
+ [
206
+ VERDICTS[:intent_unclear],
207
+ :low,
208
+ "Mixed behavioral signals detected (#{unit_signals} unit signals, #{integration_signals} integration signals). " \
209
+ 'Unable to clearly classify test intent.'
210
+ ]
211
+ end
212
+
213
+ def no_location_data_result
214
+ create_result(
215
+ verdict: VERDICTS[:intent_unclear],
216
+ confidence: :low,
217
+ reasoning: 'No spec location data available for intent analysis.',
218
+ metadata: { no_data: true }
219
+ )
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_agent'
4
+
5
+ module SpecScout
6
+ module Agents
7
+ # Agent that identifies potentially unsafe optimization scenarios by detecting
8
+ # after_commit callbacks, complex callback chains, and side-effect indicators
9
+ class RiskAgent < BaseAgent
10
+ # Verdict types for risk assessment
11
+ VERDICTS = {
12
+ safe_to_optimize: :safe_to_optimize,
13
+ potential_side_effects: :potential_side_effects,
14
+ high_risk: :high_risk
15
+ }.freeze
16
+
17
+ # Event patterns that indicate potential side effects
18
+ SIDE_EFFECT_EVENT_PATTERNS = [
19
+ /after_commit/i,
20
+ /after_create/i,
21
+ /after_update/i,
22
+ /after_save/i,
23
+ /after_destroy/i,
24
+ /callback/i,
25
+ /mailer/i,
26
+ /job/i,
27
+ /queue/i,
28
+ /background/i,
29
+ /sidekiq/i,
30
+ /resque/i,
31
+ /delayed_job/i
32
+ ].freeze
33
+
34
+ # Metadata keys that might indicate side effects
35
+ SIDE_EFFECT_METADATA_KEYS = %i[
36
+ callbacks
37
+ after_commit
38
+ after_create
39
+ after_update
40
+ after_save
41
+ mailers
42
+ jobs
43
+ background_jobs
44
+ side_effects
45
+ external_calls
46
+ api_calls
47
+ webhooks
48
+ ].freeze
49
+
50
+ # Factory patterns that suggest complex object creation with potential callbacks
51
+ COMPLEX_FACTORY_PATTERNS = [
52
+ /with_.*callback/i,
53
+ /with_.*job/i,
54
+ /with_.*mailer/i,
55
+ /with_.*notification/i,
56
+ /with_.*webhook/i,
57
+ /published/i,
58
+ /activated/i,
59
+ /confirmed/i
60
+ ].freeze
61
+
62
+ def evaluate
63
+ callback_indicators = detect_callback_indicators
64
+ side_effect_indicators = detect_side_effect_indicators
65
+ complex_chain_indicators = detect_complex_callback_chains
66
+ factory_risk_indicators = detect_factory_risk_patterns
67
+
68
+ risk_score = calculate_risk_score(
69
+ callback_indicators: callback_indicators,
70
+ side_effect_indicators: side_effect_indicators,
71
+ complex_chain_indicators: complex_chain_indicators,
72
+ factory_risk_indicators: factory_risk_indicators
73
+ )
74
+
75
+ verdict, confidence, reasoning = determine_risk_level(
76
+ risk_score: risk_score,
77
+ callback_indicators: callback_indicators,
78
+ side_effect_indicators: side_effect_indicators,
79
+ complex_chain_indicators: complex_chain_indicators,
80
+ factory_risk_indicators: factory_risk_indicators
81
+ )
82
+
83
+ create_result(
84
+ verdict: verdict,
85
+ confidence: confidence,
86
+ reasoning: reasoning,
87
+ metadata: {
88
+ risk_score: risk_score,
89
+ callback_indicators: callback_indicators,
90
+ side_effect_indicators: side_effect_indicators,
91
+ complex_chain_indicators: complex_chain_indicators,
92
+ factory_risk_indicators: factory_risk_indicators,
93
+ total_risk_factors: callback_indicators.size + side_effect_indicators.size +
94
+ complex_chain_indicators.size + factory_risk_indicators.size
95
+ }
96
+ )
97
+ end
98
+
99
+ private
100
+
101
+ def detect_callback_indicators
102
+ indicators = []
103
+
104
+ # Check events data for callback-related patterns
105
+ if profile_data.events.is_a?(Hash)
106
+ profile_data.events.each do |event_name, event_data|
107
+ event_string = "#{event_name} #{event_data}".downcase
108
+ SIDE_EFFECT_EVENT_PATTERNS.each do |pattern|
109
+ if event_string.match?(pattern)
110
+ indicators << { type: :event_pattern, pattern: pattern.source, event: event_name }
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Check metadata for callback indicators
117
+ if profile_data.metadata.is_a?(Hash)
118
+ SIDE_EFFECT_METADATA_KEYS.each do |key|
119
+ if profile_data.metadata.key?(key) && profile_data.metadata[key]
120
+ indicators << { type: :metadata_key, key: key, value: profile_data.metadata[key] }
121
+ end
122
+ end
123
+ end
124
+
125
+ indicators
126
+ end
127
+
128
+ def detect_side_effect_indicators
129
+ indicators = []
130
+
131
+ # High database write activity might indicate complex side effects
132
+ if database_operations_present?
133
+ inserts = profile_data.db[:inserts] || 0
134
+ updates = profile_data.db[:updates] || 0
135
+ deletes = profile_data.db[:deletes] || 0
136
+
137
+ total_writes = inserts + updates + deletes
138
+ indicators << { type: :high_db_writes, count: total_writes } if total_writes > 5
139
+ end
140
+
141
+ # Multiple factory creation might indicate complex object graphs with callbacks
142
+ if factories_present?
143
+ total_create_factories = profile_data.factories.values.sum do |factory_data|
144
+ factory_data[:strategy] == :create ? (factory_data[:count] || 0) : 0
145
+ end
146
+
147
+ if total_create_factories > 3
148
+ indicators << { type: :multiple_create_factories, count: total_create_factories }
149
+ end
150
+ end
151
+
152
+ # Long runtime might indicate complex processing with side effects
153
+ indicators << { type: :long_runtime, runtime_ms: profile_data.runtime_ms } if profile_data.runtime_ms > 500
154
+
155
+ indicators
156
+ end
157
+
158
+ def detect_complex_callback_chains
159
+ indicators = []
160
+
161
+ # Check for multiple event types which might indicate callback chains
162
+ if profile_data.events.is_a?(Hash) && profile_data.events.size > 3
163
+ indicators << { type: :multiple_events, count: profile_data.events.size }
164
+ end
165
+
166
+ # Check for nested or chained operations in metadata
167
+ if profile_data.metadata.is_a?(Hash) && (profile_data.metadata[:nested_operations] || profile_data.metadata[:chained_callbacks])
168
+ indicators << { type: :nested_operations, metadata: profile_data.metadata }
169
+ end
170
+
171
+ indicators
172
+ end
173
+
174
+ def detect_factory_risk_patterns
175
+ indicators = []
176
+
177
+ return indicators unless factories_present?
178
+
179
+ profile_data.factories.each do |factory_name, factory_data|
180
+ next unless factory_data.is_a?(Hash)
181
+
182
+ # Check factory traits for risky patterns
183
+ if factory_data[:traits].is_a?(Array)
184
+ factory_data[:traits].each do |trait|
185
+ trait_string = trait.to_s
186
+ COMPLEX_FACTORY_PATTERNS.each do |pattern|
187
+ next unless trait_string.match?(pattern)
188
+
189
+ indicators << {
190
+ type: :risky_factory_trait,
191
+ factory: factory_name,
192
+ trait: trait,
193
+ pattern: pattern.source
194
+ }
195
+ end
196
+ end
197
+ end
198
+
199
+ # Check for factories with many associations (potential callback triggers)
200
+ associations_count = factory_data[:associations]&.size || 0
201
+ next unless associations_count > 2
202
+
203
+ indicators << {
204
+ type: :complex_associations,
205
+ factory: factory_name,
206
+ associations_count: associations_count
207
+ }
208
+ end
209
+
210
+ indicators
211
+ end
212
+
213
+ def calculate_risk_score(callback_indicators:, side_effect_indicators:, complex_chain_indicators:,
214
+ factory_risk_indicators:)
215
+ score = 0
216
+
217
+ # Weight different types of risk indicators
218
+ score += callback_indicators.size * 2 # Callback indicators are high risk
219
+ score += side_effect_indicators.size * 2 # Side effect indicators are medium risk
220
+ score += complex_chain_indicators.size * 2 # Complex chains are medium risk
221
+ score += factory_risk_indicators.size * 1 # Factory risks are lower but still concerning
222
+
223
+ score
224
+ end
225
+
226
+ def determine_risk_level(risk_score:, callback_indicators:, side_effect_indicators:, complex_chain_indicators:,
227
+ factory_risk_indicators:)
228
+ total_indicators = callback_indicators.size + side_effect_indicators.size +
229
+ complex_chain_indicators.size + factory_risk_indicators.size
230
+
231
+ if risk_score >= 8 || callback_indicators.size >= 3
232
+ high_risk_result(risk_score, total_indicators)
233
+ elsif risk_score >= 4 || callback_indicators.size >= 2 || total_indicators >= 4
234
+ potential_side_effects_result(risk_score, total_indicators)
235
+ elsif risk_score >= 2 || total_indicators >= 2
236
+ potential_side_effects_medium_confidence_result(risk_score, total_indicators)
237
+ elsif risk_score >= 1 || total_indicators >= 1
238
+ potential_side_effects_low_confidence_result(risk_score, total_indicators)
239
+ else
240
+ safe_to_optimize_result
241
+ end
242
+ end
243
+
244
+ def high_risk_result(risk_score, total_indicators)
245
+ [
246
+ VERDICTS[:high_risk],
247
+ :high,
248
+ "High risk optimization scenario detected (risk score: #{risk_score}, #{total_indicators} risk factors). " \
249
+ 'Strong indicators of callbacks or side effects present. Optimization not recommended.'
250
+ ]
251
+ end
252
+
253
+ def potential_side_effects_result(risk_score, total_indicators)
254
+ [
255
+ VERDICTS[:potential_side_effects],
256
+ :medium,
257
+ "Potential side effects detected (risk score: #{risk_score}, #{total_indicators} risk factors). " \
258
+ 'Some indicators suggest callbacks or side effects may be present. Proceed with caution.'
259
+ ]
260
+ end
261
+
262
+ def potential_side_effects_medium_confidence_result(risk_score, total_indicators)
263
+ [
264
+ VERDICTS[:potential_side_effects],
265
+ :medium,
266
+ "Potential side effects detected (risk score: #{risk_score}, #{total_indicators} risk factors). " \
267
+ 'Multiple indicators suggest callbacks or side effects may be present. Proceed with caution.'
268
+ ]
269
+ end
270
+
271
+ def potential_side_effects_low_confidence_result(risk_score, total_indicators)
272
+ [
273
+ VERDICTS[:potential_side_effects],
274
+ :low,
275
+ "Minor risk indicators detected (risk score: #{risk_score}, #{total_indicators} risk factors). " \
276
+ 'Weak signals suggest potential side effects. Optimization likely safe but monitor carefully.'
277
+ ]
278
+ end
279
+
280
+ def safe_to_optimize_result
281
+ [
282
+ VERDICTS[:safe_to_optimize],
283
+ :high,
284
+ 'No risk factors detected. No indicators of callbacks, side effects, or complex chains found. ' \
285
+ 'Optimization appears safe to proceed.'
286
+ ]
287
+ end
288
+ end
289
+ end
290
+ end