decision_agent 0.3.0 → 1.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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module DecisionAgent
6
+ module Simulation
7
+ # Engine for shadow testing - comparing new rules against production without affecting outcomes
8
+ class ShadowTestEngine
9
+ attr_reader :production_agent, :version_manager
10
+
11
+ def initialize(production_agent:, version_manager: nil)
12
+ @production_agent = production_agent
13
+ @version_manager = version_manager || Versioning::VersionManager.new
14
+ end
15
+
16
+ # Execute shadow test - compare shadow version against production
17
+ # @param context [Hash, Context] Context for decision
18
+ # @param shadow_version [String, Integer, Hash] Shadow rule version to test
19
+ # @param options [Hash] Test options
20
+ # - :track_differences [Boolean] Track and return differences (default: true)
21
+ # - :record_results [Boolean] Record results for later analysis (default: false)
22
+ # @return [Hash] Shadow test result
23
+ def test(context:, shadow_version:, options: {})
24
+ options = {
25
+ track_differences: true,
26
+ record_results: false
27
+ }.merge(options)
28
+
29
+ ctx = normalize_context(context)
30
+ production_decision = execute_production_decision(ctx)
31
+ shadow_decision = execute_shadow_decision(ctx, shadow_version)
32
+
33
+ result = build_comparison_result(ctx, production_decision, shadow_decision)
34
+ add_differences(result, production_decision, shadow_decision) if options[:track_differences] && !result[:matches]
35
+ record_result(result, shadow_version) if options[:record_results]
36
+
37
+ result
38
+ end
39
+
40
+ def normalize_context(context)
41
+ context.is_a?(Context) ? context : Context.new(context)
42
+ end
43
+
44
+ def execute_production_decision(context)
45
+ @production_agent.decide(context: context)
46
+ rescue NoEvaluationsError
47
+ nil
48
+ end
49
+
50
+ def execute_shadow_decision(context, shadow_version)
51
+ shadow_agent = build_shadow_agent(shadow_version)
52
+ shadow_agent.decide(context: context)
53
+ rescue NoEvaluationsError
54
+ nil
55
+ end
56
+
57
+ def build_comparison_result(context, production_decision, shadow_decision)
58
+ {
59
+ context: context.to_h,
60
+ production_decision: production_decision&.decision,
61
+ production_confidence: production_decision&.confidence || 0.0,
62
+ shadow_decision: shadow_decision&.decision,
63
+ shadow_confidence: shadow_decision&.confidence || 0.0,
64
+ matches: production_decision&.decision == shadow_decision&.decision,
65
+ confidence_delta: (shadow_decision&.confidence || 0.0) - (production_decision&.confidence || 0.0),
66
+ timestamp: Time.now.utc.iso8601
67
+ }
68
+ end
69
+
70
+ def add_differences(result, production_decision, shadow_decision)
71
+ result[:differences] = {
72
+ decision_mismatch: true,
73
+ production_explanations: production_decision&.explanations || [],
74
+ shadow_explanations: shadow_decision&.explanations || []
75
+ }
76
+ end
77
+
78
+ # Batch shadow test multiple contexts
79
+ # @param contexts [Array<Hash>] Array of contexts to test
80
+ # @param shadow_version [String, Integer, Hash] Shadow rule version
81
+ # @param options [Hash] Test options
82
+ # - :parallel [Boolean] Use parallel execution (default: true)
83
+ # - :thread_count [Integer] Number of threads (default: 4)
84
+ # - :progress_callback [Proc] Progress callback
85
+ # @return [Hash] Batch shadow test results
86
+ def batch_test(contexts:, shadow_version:, options: {})
87
+ options = {
88
+ parallel: true,
89
+ thread_count: 4,
90
+ progress_callback: nil,
91
+ track_differences: true,
92
+ record_results: false
93
+ }.merge(options)
94
+
95
+ shadow_agent = build_shadow_agent(shadow_version)
96
+ results = execute_contexts_with_progress(contexts, shadow_version, shadow_agent, options)
97
+
98
+ build_batch_report(results)
99
+ end
100
+
101
+ def execute_contexts_with_progress(contexts, shadow_version, shadow_agent, options)
102
+ results = []
103
+ mutex = Mutex.new
104
+ progress_tracker = ProgressTracker.new(contexts.size, options[:progress_callback])
105
+
106
+ if options[:parallel] && contexts.size > 1
107
+ execute_parallel(contexts, shadow_agent, options, mutex) do |result|
108
+ mutex.synchronize do
109
+ results << result
110
+ progress_tracker.increment
111
+ end
112
+ end
113
+ else
114
+ execute_sequential_contexts(contexts, shadow_version, options, results, progress_tracker)
115
+ end
116
+
117
+ results
118
+ end
119
+
120
+ def execute_sequential_contexts(contexts, shadow_version, options, results, progress_tracker)
121
+ contexts.each_with_index do |context, _index|
122
+ result = test(context: context, shadow_version: shadow_version, options: options)
123
+ results << result
124
+ progress_tracker.increment
125
+ end
126
+ end
127
+
128
+ # Helper class for tracking progress
129
+ class ProgressTracker
130
+ def initialize(total, callback)
131
+ @total = total
132
+ @callback = callback
133
+ @completed = 0
134
+ end
135
+
136
+ def increment
137
+ @completed += 1
138
+ return unless @callback
139
+
140
+ @callback.call(
141
+ completed: @completed,
142
+ total: @total,
143
+ percentage: (@completed.to_f / @total * 100).round(2)
144
+ )
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def build_shadow_agent(shadow_version)
151
+ version_hash = resolve_version(shadow_version)
152
+ evaluators = build_evaluators_from_version(version_hash)
153
+ Agent.new(
154
+ evaluators: evaluators,
155
+ scoring_strategy: @production_agent.scoring_strategy,
156
+ audit_adapter: Audit::NullAdapter.new
157
+ )
158
+ end
159
+
160
+ def resolve_version(version)
161
+ case version
162
+ when String, Integer
163
+ version_data = @version_manager.get_version(version_id: version)
164
+ raise InvalidShadowTestError, "Shadow version not found: #{version}" unless version_data
165
+
166
+ version_data
167
+ when Hash
168
+ version
169
+ else
170
+ raise InvalidShadowTestError, "Invalid shadow version format: #{version.class}"
171
+ end
172
+ end
173
+
174
+ def build_evaluators_from_version(version)
175
+ content = version[:content] || version["content"]
176
+ raise InvalidShadowTestError, "Shadow version has no content" unless content
177
+
178
+ if content.is_a?(Hash) && content[:evaluators]
179
+ build_evaluators_from_config(content[:evaluators])
180
+ elsif content.is_a?(Hash) && (content[:rules] || content["rules"])
181
+ [Evaluators::JsonRuleEvaluator.new(rules_json: content)]
182
+ else
183
+ raise InvalidShadowTestError, "Cannot build evaluators from shadow version"
184
+ end
185
+ end
186
+
187
+ def build_evaluators_from_config(configs)
188
+ Array(configs).map do |config|
189
+ case config[:type] || config["type"]
190
+ when "json_rule"
191
+ Evaluators::JsonRuleEvaluator.new(rules_json: config[:rules] || config["rules"])
192
+ when "dmn"
193
+ model = config[:model] || config["model"]
194
+ decision_id = config[:decision_id] || config["decision_id"]
195
+ Evaluators::DmnEvaluator.new(model: model, decision_id: decision_id)
196
+ else
197
+ raise InvalidShadowTestError, "Unknown evaluator type: #{config[:type]}"
198
+ end
199
+ end
200
+ end
201
+
202
+ def execute_parallel(contexts, shadow_agent, options, _mutex, &block)
203
+ thread_count = [options[:thread_count], contexts.size].min
204
+ queue = Queue.new
205
+ contexts.each { |c| queue << c }
206
+
207
+ threads = Array.new(thread_count) do
208
+ Thread.new do
209
+ process_contexts_from_queue(queue, shadow_agent, options, &block)
210
+ end
211
+ end
212
+
213
+ threads.each(&:join)
214
+ end
215
+
216
+ def process_contexts_from_queue(queue, shadow_agent, options)
217
+ loop do
218
+ context = dequeue_context(queue)
219
+ break unless context
220
+
221
+ ctx = normalize_context(context)
222
+ production_decision = execute_production_decision(ctx)
223
+ shadow_decision = execute_shadow_decision_in_parallel(ctx, shadow_agent)
224
+
225
+ result = build_comparison_result(ctx, production_decision, shadow_decision)
226
+ add_differences(result, production_decision, shadow_decision) if options[:track_differences] && !result[:matches]
227
+
228
+ yield result
229
+ end
230
+ end
231
+
232
+ def dequeue_context(queue)
233
+ queue.pop(true)
234
+ rescue ThreadError
235
+ nil
236
+ end
237
+
238
+ def execute_shadow_decision_in_parallel(context, shadow_agent)
239
+ shadow_agent.decide(context: context)
240
+ rescue NoEvaluationsError
241
+ nil
242
+ end
243
+
244
+ def record_result(_result, shadow_version)
245
+ # In a real implementation, this would store results in a database or file
246
+ # For now, this is a placeholder
247
+ shadow_version.is_a?(Hash) ? shadow_version[:id] || shadow_version["id"] : shadow_version
248
+ # Store result for later analysis
249
+ end
250
+
251
+ def build_batch_report(results)
252
+ total = results.size
253
+ matches = results.count { |r| r[:matches] }
254
+ mismatches = total - matches
255
+ confidence_deltas = results.map { |r| r[:confidence_delta] }.compact
256
+
257
+ {
258
+ total_tests: total,
259
+ matches: matches,
260
+ mismatches: mismatches,
261
+ match_rate: total.positive? ? (matches.to_f / total) : 0,
262
+ average_confidence_delta: confidence_deltas.any? ? confidence_deltas.sum / confidence_deltas.size : 0,
263
+ max_confidence_delta: confidence_deltas.map(&:abs).max || 0,
264
+ decision_distribution: {
265
+ production: results.group_by { |r| r[:production_decision] }.transform_values(&:count),
266
+ shadow: results.group_by { |r| r[:shadow_decision] }.transform_values(&:count)
267
+ },
268
+ mismatched_results: results.reject { |r| r[:matches] },
269
+ results: results
270
+ }
271
+ end
272
+ end
273
+ end
274
+ end