decision_agent 0.3.0 → 1.0.1
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 +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Simulation
|
|
5
|
+
# Analyzer for quantifying rule change impact
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
|
+
class ImpactAnalyzer
|
|
8
|
+
attr_reader :version_manager
|
|
9
|
+
|
|
10
|
+
def initialize(version_manager: nil)
|
|
11
|
+
@version_manager = version_manager || Versioning::VersionManager.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Analyze impact of a proposed rule change
|
|
15
|
+
# @param baseline_version [String, Integer, Hash] Baseline rule version
|
|
16
|
+
# @param proposed_version [String, Integer, Hash] Proposed rule version
|
|
17
|
+
# @param test_data [Array<Hash>] Test contexts to evaluate
|
|
18
|
+
# @param options [Hash] Analysis options
|
|
19
|
+
# - :parallel [Boolean] Use parallel execution (default: true)
|
|
20
|
+
# - :thread_count [Integer] Number of threads (default: 4)
|
|
21
|
+
# - :calculate_risk [Boolean] Calculate risk score (default: true)
|
|
22
|
+
# @return [Hash] Impact analysis report
|
|
23
|
+
def analyze(baseline_version:, proposed_version:, test_data:, options: {})
|
|
24
|
+
options = {
|
|
25
|
+
parallel: true,
|
|
26
|
+
thread_count: 4,
|
|
27
|
+
calculate_risk: true
|
|
28
|
+
}.merge(options)
|
|
29
|
+
|
|
30
|
+
baseline_agent = build_agent_from_version(baseline_version)
|
|
31
|
+
proposed_agent = build_agent_from_version(proposed_version)
|
|
32
|
+
|
|
33
|
+
# Execute both versions on test data
|
|
34
|
+
results = execute_comparison(test_data, baseline_agent, proposed_agent, options)
|
|
35
|
+
|
|
36
|
+
# Build impact report
|
|
37
|
+
build_impact_report(results, options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Calculate risk score for a rule change
|
|
41
|
+
# @param results [Array<Hash>] Comparison results
|
|
42
|
+
# @return [Float] Risk score between 0.0 (low risk) and 1.0 (high risk)
|
|
43
|
+
def calculate_risk_score(results)
|
|
44
|
+
return 0.0 if results.empty?
|
|
45
|
+
|
|
46
|
+
total = results.size
|
|
47
|
+
decision_changes = results.count { |r| r[:decision_changed] }
|
|
48
|
+
large_confidence_shifts = results.count { |r| (r[:confidence_delta] || 0).abs > 0.2 }
|
|
49
|
+
rejections_increased = count_rejection_increases(results)
|
|
50
|
+
|
|
51
|
+
# Risk factors
|
|
52
|
+
change_rate = decision_changes.to_f / total
|
|
53
|
+
confidence_volatility = large_confidence_shifts.to_f / total
|
|
54
|
+
rejection_risk = rejections_increased.to_f / total
|
|
55
|
+
|
|
56
|
+
# Weighted risk score
|
|
57
|
+
risk_score = (
|
|
58
|
+
(change_rate * 0.4) +
|
|
59
|
+
(confidence_volatility * 0.3) +
|
|
60
|
+
(rejection_risk * 0.3)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
[risk_score, 1.0].min # Cap at 1.0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def build_agent_from_version(version)
|
|
69
|
+
version_hash = resolve_version(version)
|
|
70
|
+
evaluators = build_evaluators_from_version(version_hash)
|
|
71
|
+
Agent.new(
|
|
72
|
+
evaluators: evaluators,
|
|
73
|
+
scoring_strategy: Scoring::WeightedAverage.new,
|
|
74
|
+
audit_adapter: Audit::NullAdapter.new
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def resolve_version(version)
|
|
79
|
+
case version
|
|
80
|
+
when String, Integer
|
|
81
|
+
version_data = @version_manager.get_version(version_id: version)
|
|
82
|
+
raise VersionComparisonError, "Version not found: #{version}" unless version_data
|
|
83
|
+
|
|
84
|
+
version_data
|
|
85
|
+
when Hash
|
|
86
|
+
version
|
|
87
|
+
else
|
|
88
|
+
raise VersionComparisonError, "Invalid version format: #{version.class}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_evaluators_from_version(version)
|
|
93
|
+
content = version[:content] || version["content"]
|
|
94
|
+
raise VersionComparisonError, "Version has no content" unless content
|
|
95
|
+
|
|
96
|
+
if content.is_a?(Hash) && content[:evaluators]
|
|
97
|
+
build_evaluators_from_config(content[:evaluators])
|
|
98
|
+
elsif content.is_a?(Hash) && (content[:rules] || content["rules"])
|
|
99
|
+
[Evaluators::JsonRuleEvaluator.new(rules_json: content)]
|
|
100
|
+
else
|
|
101
|
+
raise VersionComparisonError, "Cannot build evaluators from version content"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_evaluators_from_config(configs)
|
|
106
|
+
Array(configs).map do |config|
|
|
107
|
+
case config[:type] || config["type"]
|
|
108
|
+
when "json_rule"
|
|
109
|
+
Evaluators::JsonRuleEvaluator.new(rules_json: config[:rules] || config["rules"])
|
|
110
|
+
when "dmn"
|
|
111
|
+
model = config[:model] || config["model"]
|
|
112
|
+
decision_id = config[:decision_id] || config["decision_id"]
|
|
113
|
+
Evaluators::DmnEvaluator.new(model: model, decision_id: decision_id)
|
|
114
|
+
else
|
|
115
|
+
raise VersionComparisonError, "Unknown evaluator type: #{config[:type]}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def execute_comparison(test_data, baseline_agent, proposed_agent, options)
|
|
121
|
+
results = []
|
|
122
|
+
mutex = Mutex.new
|
|
123
|
+
|
|
124
|
+
if options[:parallel] && test_data.size > 1
|
|
125
|
+
execute_parallel(test_data, baseline_agent, proposed_agent, options, mutex) do |result|
|
|
126
|
+
mutex.synchronize { results << result }
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
test_data.each do |context|
|
|
130
|
+
result = execute_single_comparison(context, baseline_agent, proposed_agent)
|
|
131
|
+
results << result
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
results
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def execute_parallel(test_data, baseline_agent, proposed_agent, options, _mutex)
|
|
139
|
+
thread_count = [options[:thread_count], test_data.size].min
|
|
140
|
+
queue = Queue.new
|
|
141
|
+
test_data.each { |c| queue << c }
|
|
142
|
+
|
|
143
|
+
threads = Array.new(thread_count) do
|
|
144
|
+
Thread.new do
|
|
145
|
+
loop do
|
|
146
|
+
context = begin
|
|
147
|
+
queue.pop(true)
|
|
148
|
+
rescue StandardError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
break unless context
|
|
152
|
+
|
|
153
|
+
result = execute_single_comparison(context, baseline_agent, proposed_agent)
|
|
154
|
+
yield result
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
threads.each(&:join)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def execute_single_comparison(context, baseline_agent, proposed_agent)
|
|
163
|
+
ctx = context.is_a?(Context) ? context : Context.new(context)
|
|
164
|
+
|
|
165
|
+
baseline_metrics = measure_decision_metrics(ctx, baseline_agent, :baseline)
|
|
166
|
+
proposed_metrics = measure_decision_metrics(ctx, proposed_agent, :proposed)
|
|
167
|
+
delta_metrics = calculate_decision_delta(baseline_metrics, proposed_metrics)
|
|
168
|
+
|
|
169
|
+
build_comparison_result(ctx, baseline_metrics, proposed_metrics, delta_metrics)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def measure_decision_metrics(context, agent, _label)
|
|
173
|
+
start_time = Time.now
|
|
174
|
+
begin
|
|
175
|
+
decision = agent.decide(context: context)
|
|
176
|
+
duration_ms = (Time.now - start_time) * 1000
|
|
177
|
+
evaluations_count = decision.evaluations&.size || 0
|
|
178
|
+
{ decision: decision, duration_ms: duration_ms, evaluations_count: evaluations_count }
|
|
179
|
+
rescue NoEvaluationsError
|
|
180
|
+
duration_ms = (Time.now - start_time) * 1000
|
|
181
|
+
{ decision: nil, duration_ms: duration_ms, evaluations_count: 0 }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def calculate_decision_delta(baseline_metrics, proposed_metrics)
|
|
186
|
+
baseline_decision = baseline_metrics[:decision]
|
|
187
|
+
proposed_decision = proposed_metrics[:decision]
|
|
188
|
+
|
|
189
|
+
decision_changed, confidence_delta = if baseline_decision.nil? && proposed_decision.nil?
|
|
190
|
+
[false, 0]
|
|
191
|
+
elsif baseline_decision.nil?
|
|
192
|
+
[true, proposed_decision.confidence]
|
|
193
|
+
elsif proposed_decision.nil?
|
|
194
|
+
[true, -baseline_decision.confidence]
|
|
195
|
+
else
|
|
196
|
+
[
|
|
197
|
+
baseline_decision.decision != proposed_decision.decision,
|
|
198
|
+
proposed_decision.confidence - baseline_decision.confidence
|
|
199
|
+
]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
baseline_duration = baseline_metrics[:duration_ms]
|
|
203
|
+
proposed_duration = proposed_metrics[:duration_ms]
|
|
204
|
+
performance_delta_ms = proposed_duration - baseline_duration
|
|
205
|
+
performance_delta_percent = baseline_duration.positive? ? (performance_delta_ms / baseline_duration * 100) : 0
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
decision_changed: decision_changed,
|
|
209
|
+
confidence_delta: confidence_delta,
|
|
210
|
+
performance_delta_ms: performance_delta_ms,
|
|
211
|
+
performance_delta_percent: performance_delta_percent
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def build_comparison_result(context, baseline_metrics, proposed_metrics, delta_metrics)
|
|
216
|
+
baseline_decision = baseline_metrics[:decision]
|
|
217
|
+
proposed_decision = proposed_metrics[:decision]
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
context: context.to_h,
|
|
221
|
+
baseline_decision: baseline_decision&.decision,
|
|
222
|
+
baseline_confidence: baseline_decision&.confidence || 0,
|
|
223
|
+
baseline_duration_ms: baseline_metrics[:duration_ms],
|
|
224
|
+
baseline_evaluations_count: baseline_metrics[:evaluations_count],
|
|
225
|
+
proposed_decision: proposed_decision&.decision,
|
|
226
|
+
proposed_confidence: proposed_decision&.confidence || 0,
|
|
227
|
+
proposed_duration_ms: proposed_metrics[:duration_ms],
|
|
228
|
+
proposed_evaluations_count: proposed_metrics[:evaluations_count],
|
|
229
|
+
decision_changed: delta_metrics[:decision_changed],
|
|
230
|
+
confidence_delta: delta_metrics[:confidence_delta],
|
|
231
|
+
confidence_shift_magnitude: delta_metrics[:confidence_delta].abs,
|
|
232
|
+
performance_delta_ms: delta_metrics[:performance_delta_ms],
|
|
233
|
+
performance_delta_percent: delta_metrics[:performance_delta_percent]
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def build_impact_report(results, options)
|
|
238
|
+
report = build_base_report(results)
|
|
239
|
+
report[:confidence_impact] = build_confidence_impact(results)
|
|
240
|
+
report[:rule_execution_frequency] = build_rule_frequency(results)
|
|
241
|
+
report[:performance_impact] = calculate_performance_impact(results)
|
|
242
|
+
add_risk_analysis(report, results, options)
|
|
243
|
+
report
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def build_base_report(results)
|
|
247
|
+
total = results.size
|
|
248
|
+
decision_changes = results.count { |r| r[:decision_changed] }
|
|
249
|
+
baseline_distribution = results.group_by { |r| r[:baseline_decision] }.transform_values(&:count)
|
|
250
|
+
proposed_distribution = results.group_by { |r| r[:proposed_decision] }.transform_values(&:count)
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
total_contexts: total,
|
|
254
|
+
decision_changes: decision_changes,
|
|
255
|
+
change_rate: total.positive? ? (decision_changes.to_f / total) : 0,
|
|
256
|
+
decision_distribution: {
|
|
257
|
+
baseline: baseline_distribution,
|
|
258
|
+
proposed: proposed_distribution
|
|
259
|
+
},
|
|
260
|
+
results: results
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def build_confidence_impact(results)
|
|
265
|
+
confidence_deltas = results.map { |r| r[:confidence_delta] }.compact
|
|
266
|
+
avg_confidence_delta = confidence_deltas.any? ? confidence_deltas.sum / confidence_deltas.size : 0
|
|
267
|
+
max_confidence_shift = confidence_deltas.map(&:abs).max || 0
|
|
268
|
+
|
|
269
|
+
{
|
|
270
|
+
average_delta: avg_confidence_delta,
|
|
271
|
+
max_shift: max_confidence_shift,
|
|
272
|
+
positive_shifts: confidence_deltas.count(&:positive?),
|
|
273
|
+
negative_shifts: confidence_deltas.count(&:negative?)
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def build_rule_frequency(results)
|
|
278
|
+
{
|
|
279
|
+
baseline: calculate_rule_frequency(results, :baseline_decision),
|
|
280
|
+
proposed: calculate_rule_frequency(results, :proposed_decision)
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def add_risk_analysis(report, results, options)
|
|
285
|
+
return unless options[:calculate_risk]
|
|
286
|
+
|
|
287
|
+
report[:risk_score] = calculate_risk_score(results)
|
|
288
|
+
report[:risk_level] = categorize_risk(report[:risk_score])
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def calculate_rule_frequency(results, decision_key)
|
|
292
|
+
# Approximate rule frequency from decision distribution
|
|
293
|
+
# In a real implementation, this would track which rules fired
|
|
294
|
+
results.group_by { |r| r[decision_key] }.transform_values { |v| v.size.to_f / results.size }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def count_rejection_increases(results)
|
|
298
|
+
results.count do |r|
|
|
299
|
+
baseline = r[:baseline_decision].to_s.downcase
|
|
300
|
+
proposed = r[:proposed_decision].to_s.downcase
|
|
301
|
+
(baseline.include?("approve") || baseline.include?("accept")) &&
|
|
302
|
+
(proposed.include?("reject") || proposed.include?("deny"))
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def categorize_risk(risk_score)
|
|
307
|
+
case risk_score
|
|
308
|
+
when 0.0...0.2
|
|
309
|
+
"low"
|
|
310
|
+
when 0.2...0.5
|
|
311
|
+
"medium"
|
|
312
|
+
when 0.5...0.8
|
|
313
|
+
"high"
|
|
314
|
+
else
|
|
315
|
+
"critical"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Calculate performance impact metrics
|
|
320
|
+
# @param results [Array<Hash>] Comparison results with performance data
|
|
321
|
+
# @return [Hash] Performance impact metrics
|
|
322
|
+
def calculate_performance_impact(results)
|
|
323
|
+
return {} if results.empty?
|
|
324
|
+
|
|
325
|
+
metrics = extract_performance_metrics(results)
|
|
326
|
+
latency_stats = calculate_latency_statistics(metrics)
|
|
327
|
+
throughput_stats = calculate_throughput_statistics(latency_stats)
|
|
328
|
+
complexity_stats = calculate_complexity_statistics(metrics)
|
|
329
|
+
performance_deltas = calculate_performance_deltas(metrics, latency_stats, throughput_stats)
|
|
330
|
+
|
|
331
|
+
build_performance_impact_hash(latency_stats, throughput_stats, complexity_stats, performance_deltas)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def extract_performance_metrics(results)
|
|
335
|
+
{
|
|
336
|
+
baseline_durations: results.map { |r| r[:baseline_duration_ms] }.compact,
|
|
337
|
+
proposed_durations: results.map { |r| r[:proposed_duration_ms] }.compact,
|
|
338
|
+
performance_deltas: results.map { |r| r[:performance_delta_ms] }.compact,
|
|
339
|
+
performance_delta_percents: results.map { |r| r[:performance_delta_percent] }.compact,
|
|
340
|
+
baseline_evaluations: results.map { |r| r[:baseline_evaluations_count] }.compact,
|
|
341
|
+
proposed_evaluations: results.map { |r| r[:proposed_evaluations_count] }.compact
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def calculate_latency_statistics(metrics)
|
|
346
|
+
baseline_durations = metrics[:baseline_durations]
|
|
347
|
+
proposed_durations = metrics[:proposed_durations]
|
|
348
|
+
|
|
349
|
+
{
|
|
350
|
+
baseline_avg: calculate_average(baseline_durations),
|
|
351
|
+
baseline_min: baseline_durations.min || 0,
|
|
352
|
+
baseline_max: baseline_durations.max || 0,
|
|
353
|
+
proposed_avg: calculate_average(proposed_durations),
|
|
354
|
+
proposed_min: proposed_durations.min || 0,
|
|
355
|
+
proposed_max: proposed_durations.max || 0
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def calculate_throughput_statistics(latency_stats)
|
|
360
|
+
baseline_throughput = latency_stats[:baseline_avg].positive? ? (1000.0 / latency_stats[:baseline_avg]) : 0
|
|
361
|
+
proposed_throughput = latency_stats[:proposed_avg].positive? ? (1000.0 / latency_stats[:proposed_avg]) : 0
|
|
362
|
+
|
|
363
|
+
{
|
|
364
|
+
baseline: baseline_throughput,
|
|
365
|
+
proposed: proposed_throughput
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def calculate_complexity_statistics(metrics)
|
|
370
|
+
baseline_avg = calculate_average(metrics[:baseline_evaluations], as_float: true)
|
|
371
|
+
proposed_avg = calculate_average(metrics[:proposed_evaluations], as_float: true)
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
baseline_avg: baseline_avg,
|
|
375
|
+
proposed_avg: proposed_avg,
|
|
376
|
+
delta: proposed_avg - baseline_avg
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def calculate_performance_deltas(metrics, _latency_stats, throughput_stats)
|
|
381
|
+
avg_delta_ms = calculate_average(metrics[:performance_deltas])
|
|
382
|
+
avg_delta_percent = calculate_average(metrics[:performance_delta_percents])
|
|
383
|
+
baseline_throughput = throughput_stats[:baseline]
|
|
384
|
+
proposed_throughput = throughput_stats[:proposed]
|
|
385
|
+
throughput_delta_percent = baseline_throughput.positive? ? ((proposed_throughput - baseline_throughput) / baseline_throughput * 100) : 0
|
|
386
|
+
|
|
387
|
+
{
|
|
388
|
+
avg_delta_ms: avg_delta_ms,
|
|
389
|
+
avg_delta_percent: avg_delta_percent,
|
|
390
|
+
throughput_delta_percent: throughput_delta_percent
|
|
391
|
+
}
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def calculate_average(values, as_float: false)
|
|
395
|
+
return 0 if values.empty?
|
|
396
|
+
|
|
397
|
+
sum = as_float ? values.sum.to_f : values.sum
|
|
398
|
+
sum / values.size
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def build_performance_impact_hash(latency_stats, throughput_stats, complexity_stats, performance_deltas)
|
|
402
|
+
{
|
|
403
|
+
latency: build_latency_impact(latency_stats, performance_deltas),
|
|
404
|
+
throughput: build_throughput_impact(throughput_stats, performance_deltas),
|
|
405
|
+
rule_complexity: build_complexity_impact(complexity_stats),
|
|
406
|
+
impact_level: categorize_performance_impact(performance_deltas[:avg_delta_percent]),
|
|
407
|
+
summary: build_performance_summary(
|
|
408
|
+
performance_deltas[:avg_delta_percent],
|
|
409
|
+
performance_deltas[:throughput_delta_percent],
|
|
410
|
+
complexity_stats[:delta]
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def build_latency_impact(latency_stats, performance_deltas)
|
|
416
|
+
{
|
|
417
|
+
baseline: {
|
|
418
|
+
average_ms: latency_stats[:baseline_avg].round(4),
|
|
419
|
+
min_ms: latency_stats[:baseline_min].round(4),
|
|
420
|
+
max_ms: latency_stats[:baseline_max].round(4)
|
|
421
|
+
},
|
|
422
|
+
proposed: {
|
|
423
|
+
average_ms: latency_stats[:proposed_avg].round(4),
|
|
424
|
+
min_ms: latency_stats[:proposed_min].round(4),
|
|
425
|
+
max_ms: latency_stats[:proposed_max].round(4)
|
|
426
|
+
},
|
|
427
|
+
delta_ms: performance_deltas[:avg_delta_ms].round(4),
|
|
428
|
+
delta_percent: performance_deltas[:avg_delta_percent].round(2)
|
|
429
|
+
}
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def build_throughput_impact(throughput_stats, performance_deltas)
|
|
433
|
+
{
|
|
434
|
+
baseline_decisions_per_second: throughput_stats[:baseline].round(2),
|
|
435
|
+
proposed_decisions_per_second: throughput_stats[:proposed].round(2),
|
|
436
|
+
delta_percent: performance_deltas[:throughput_delta_percent].round(2)
|
|
437
|
+
}
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def build_complexity_impact(complexity_stats)
|
|
441
|
+
{
|
|
442
|
+
baseline_avg_evaluations: complexity_stats[:baseline_avg].round(2),
|
|
443
|
+
proposed_avg_evaluations: complexity_stats[:proposed_avg].round(2),
|
|
444
|
+
evaluations_delta: complexity_stats[:delta].round(2)
|
|
445
|
+
}
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Categorize performance impact level
|
|
449
|
+
# @param delta_percent [Float] Performance delta percentage
|
|
450
|
+
# @return [String] Impact level: "improvement", "neutral", "minor_degradation", "moderate_degradation", "significant_degradation"
|
|
451
|
+
def categorize_performance_impact(delta_percent)
|
|
452
|
+
case delta_percent
|
|
453
|
+
when -Float::INFINITY...-5.0
|
|
454
|
+
"improvement"
|
|
455
|
+
when -5.0...5.0
|
|
456
|
+
"neutral"
|
|
457
|
+
when 5.0...15.0
|
|
458
|
+
"minor_degradation"
|
|
459
|
+
when 15.0...30.0
|
|
460
|
+
"moderate_degradation"
|
|
461
|
+
else
|
|
462
|
+
"significant_degradation"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Build human-readable performance summary
|
|
467
|
+
# @param latency_delta_percent [Float] Latency delta percentage
|
|
468
|
+
# @param throughput_delta_percent [Float] Throughput delta percentage
|
|
469
|
+
# @param evaluations_delta [Float] Evaluations delta
|
|
470
|
+
# @return [String] Summary text
|
|
471
|
+
def build_performance_summary(latency_delta_percent, throughput_delta_percent, evaluations_delta)
|
|
472
|
+
parts = []
|
|
473
|
+
|
|
474
|
+
if latency_delta_percent.abs > 5.0
|
|
475
|
+
direction = latency_delta_percent.positive? ? "slower" : "faster"
|
|
476
|
+
parts << "Average latency is #{latency_delta_percent.abs.round(2)}% #{direction}"
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
if throughput_delta_percent.abs > 5.0
|
|
480
|
+
direction = throughput_delta_percent.positive? ? "higher" : "lower"
|
|
481
|
+
parts << "Throughput is #{throughput_delta_percent.abs.round(2)}% #{direction}"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
if evaluations_delta.abs > 0.5
|
|
485
|
+
direction = evaluations_delta.positive? ? "more" : "fewer"
|
|
486
|
+
parts << "Average #{direction} #{evaluations_delta.abs.round(2)} rule evaluations per decision"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if parts.empty?
|
|
490
|
+
"Performance impact is minimal (<5% change)"
|
|
491
|
+
else
|
|
492
|
+
"#{parts.join('. ')}."
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
# rubocop:enable Metrics/ClassLength
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|