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,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
|