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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. 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