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,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Helpers
6
+ # Parameter parsing helpers for ConditionEvaluator
7
+ module ParameterParsingHelpers
8
+ def self.parse_range(value, param_cache:, param_cache_mutex:)
9
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[min max])
10
+ cache_key = Operators::Base.normalize_param_cache_key(normalized, "range")
11
+
12
+ cached = param_cache[cache_key]
13
+ return cached if cached
14
+
15
+ param_cache_mutex.synchronize do
16
+ param_cache[cache_key] ||= parse_range_impl(normalized)
17
+ end
18
+ end
19
+
20
+ def self.parse_range_impl(value)
21
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[min max])
22
+ return nil unless normalized.is_a?(Hash)
23
+
24
+ min = normalized[:min] || normalized["min"]
25
+ max = normalized[:max] || normalized["max"]
26
+ return nil unless min && max
27
+
28
+ { min: min, max: max }
29
+ end
30
+
31
+ def self.parse_modulo_params(value, param_cache:, param_cache_mutex:)
32
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[divisor remainder])
33
+ cache_key = Operators::Base.normalize_param_cache_key(normalized, "modulo")
34
+
35
+ cached = param_cache[cache_key]
36
+ return cached if cached
37
+
38
+ param_cache_mutex.synchronize do
39
+ param_cache[cache_key] ||= parse_modulo_params_impl(normalized)
40
+ end
41
+ end
42
+
43
+ def self.parse_modulo_params_impl(value)
44
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[divisor remainder])
45
+ return nil unless normalized.is_a?(Hash)
46
+
47
+ divisor = normalized[:divisor] || normalized["divisor"]
48
+ remainder = normalized[:remainder] || normalized["remainder"]
49
+ return nil unless divisor && !remainder.nil?
50
+
51
+ { divisor: divisor, remainder: remainder }
52
+ end
53
+
54
+ def self.parse_power_params(value)
55
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[exponent result])
56
+ return nil unless normalized.is_a?(Hash)
57
+
58
+ exponent = normalized[:exponent] || normalized["exponent"]
59
+ result = normalized[:result] || normalized["result"]
60
+ return nil unless exponent && !result.nil?
61
+
62
+ { exponent: exponent, result: result }
63
+ end
64
+
65
+ def self.parse_atan2_params(value)
66
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[y result])
67
+ return nil unless normalized.is_a?(Hash)
68
+
69
+ y = normalized[:y] || normalized["y"]
70
+ result = normalized[:result] || normalized["result"]
71
+ return nil unless y && !result.nil?
72
+
73
+ { y: y, result: result }
74
+ end
75
+
76
+ def self.parse_gcd_lcm_params(value)
77
+ normalized = Operators::Base.normalize_params_to_hash(value, %i[other result])
78
+ return nil unless normalized.is_a?(Hash)
79
+
80
+ other = normalized[:other] || normalized["other"]
81
+ result = normalized[:result] || normalized["result"]
82
+ return nil unless other && !result.nil?
83
+
84
+ { other: other, result: result }
85
+ end
86
+
87
+ def self.parse_percentile_params(value)
88
+ return nil unless value.is_a?(Hash)
89
+
90
+ percentile = value["percentile"] || value[:percentile]
91
+ return nil unless percentile.is_a?(Numeric) && percentile >= 0 && percentile <= 100
92
+
93
+ {
94
+ percentile: percentile.to_f,
95
+ threshold: value["threshold"] || value[:threshold],
96
+ gt: value["gt"] || value[:gt],
97
+ lt: value["lt"] || value[:lt],
98
+ gte: value["gte"] || value[:gte],
99
+ lte: value["lte"] || value[:lte],
100
+ eq: value["eq"] || value[:eq]
101
+ }
102
+ end
103
+
104
+ def self.parse_duration_params(value)
105
+ return nil unless value.is_a?(Hash)
106
+
107
+ end_field = value["end"] || value[:end]
108
+ return nil unless end_field
109
+
110
+ {
111
+ end: end_field.to_s,
112
+ min: value["min"] || value[:min],
113
+ max: value["max"] || value[:max],
114
+ gt: value["gt"] || value[:gt],
115
+ lt: value["lt"] || value[:lt],
116
+ gte: value["gte"] || value[:gte],
117
+ lte: value["lte"] || value[:lte]
118
+ }
119
+ end
120
+
121
+ def self.parse_date_arithmetic_params(value, unit = :days)
122
+ return nil unless value.is_a?(Hash)
123
+
124
+ unit_value = value[unit.to_s] || value[unit]
125
+ return nil unless unit_value.is_a?(Numeric)
126
+
127
+ {
128
+ unit => unit_value.to_f,
129
+ target: value["target"] || value[:target] || "now",
130
+ compare: value["compare"] || value[:compare],
131
+ eq: value["eq"] || value[:eq],
132
+ gt: value["gt"] || value[:gt],
133
+ lt: value["lt"] || value[:lt],
134
+ gte: value["gte"] || value[:gte],
135
+ lte: value["lte"] || value[:lte]
136
+ }
137
+ end
138
+
139
+ def self.parse_moving_window_params(value)
140
+ return nil unless value.is_a?(Hash)
141
+
142
+ window = value["window"] || value[:window]
143
+ return nil unless window.is_a?(Numeric) && window.positive?
144
+
145
+ {
146
+ window: window.to_i,
147
+ threshold: value["threshold"] || value[:threshold],
148
+ gt: value["gt"] || value[:gt],
149
+ lt: value["lt"] || value[:lt],
150
+ gte: value["gte"] || value[:gte],
151
+ lte: value["lte"] || value[:lte]
152
+ }
153
+ end
154
+
155
+ def self.parse_compound_interest_params(value)
156
+ return nil unless value.is_a?(Hash)
157
+
158
+ rate = value["rate"] || value[:rate]
159
+ periods = value["periods"] || value[:periods]
160
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
161
+
162
+ {
163
+ rate: rate.to_f,
164
+ periods: periods.to_f,
165
+ result: value["result"] || value[:result],
166
+ compare: value["compare"] || value[:compare],
167
+ threshold: value["threshold"] || value[:threshold]
168
+ }
169
+ end
170
+
171
+ def self.parse_present_value_params(value)
172
+ return nil unless value.is_a?(Hash)
173
+
174
+ rate = value["rate"] || value[:rate]
175
+ periods = value["periods"] || value[:periods]
176
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
177
+
178
+ {
179
+ rate: rate.to_f,
180
+ periods: periods.to_f,
181
+ result: value["result"] || value[:result]
182
+ }
183
+ end
184
+
185
+ def self.parse_future_value_params(value)
186
+ parse_present_value_params(value)
187
+ end
188
+
189
+ def self.parse_payment_params(value)
190
+ parse_compound_interest_params(value)
191
+ end
192
+
193
+ def self.parse_join_params(value)
194
+ return nil unless value.is_a?(Hash)
195
+
196
+ separator = value["separator"] || value[:separator] || ","
197
+ {
198
+ separator: separator.to_s,
199
+ result: value["result"] || value[:result],
200
+ contains: value["contains"] || value[:contains]
201
+ }
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Helpers
6
+ # Template expansion and mapping helpers for ConditionEvaluator
7
+ module TemplateHelpers
8
+ def self.expand_template_params(params, context_hash, get_nested_value:)
9
+ return {} unless params.is_a?(Hash)
10
+
11
+ params.transform_values do |value|
12
+ expand_template_value(value, context_hash, get_nested_value: get_nested_value)
13
+ end
14
+ end
15
+
16
+ def self.expand_template_value(value, context_hash, get_nested_value:)
17
+ return value unless value.is_a?(String)
18
+ return value unless value.match?(/\{\{.*\}\}/)
19
+
20
+ # Extract path from {{path}} syntax
21
+ value.gsub(/\{\{([^}]+)\}\}/) do |_match|
22
+ path = Regexp.last_match(1).strip
23
+ get_nested_value.call(context_hash, path) || value
24
+ end
25
+ end
26
+
27
+ def self.apply_mapping(response_data, mapping, get_nested_value:)
28
+ return {} unless response_data.is_a?(Hash)
29
+ return {} unless mapping.is_a?(Hash)
30
+
31
+ mapping.each_with_object({}) do |(source_key, target_key), result|
32
+ source_value = get_nested_value.call(response_data, source_key.to_s)
33
+ result[target_key.to_s] = source_value unless source_value.nil?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Helpers
6
+ # Utility helpers for ConditionEvaluator
7
+ module UtilityHelpers
8
+ def self.get_nested_value(hash, key_path, get_cached_path:)
9
+ keys = get_cached_path.call(key_path)
10
+ keys.reduce(hash) do |memo, key|
11
+ return nil unless memo.is_a?(Hash)
12
+
13
+ # OPTIMIZE: try symbol first (most common), then string
14
+ # Check key existence first to avoid double lookup
15
+ key_sym = key.to_sym
16
+ if memo.key?(key_sym)
17
+ memo[key_sym]
18
+ elsif memo.key?(key)
19
+ memo[key]
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.comparable?(val1, val2)
25
+ # Both are numeric - allow comparison between different numeric types
26
+ # (e.g., Integer and Float are comparable in Ruby)
27
+ return true if val1.is_a?(Numeric) && val2.is_a?(Numeric)
28
+
29
+ # Both are strings - require exact same type
30
+ return val1.instance_of?(val2.class) if val1.is_a?(String) && val2.is_a?(String)
31
+
32
+ false
33
+ end
34
+
35
+ def self.epsilon_equal?(value1, value2, epsilon = 1e-10)
36
+ (value1 - value2).abs < epsilon
37
+ end
38
+
39
+ def self.string_operator?(actual_value, expected_value)
40
+ actual_value.is_a?(String) && expected_value.is_a?(String)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Base utilities shared across all operator modules
7
+ module Base
8
+ # Normalize params to hash - converts arrays to hashes for better performance
9
+ # If value is an array and keys are provided, convert to hash
10
+ # If value is already a hash, normalize keys to symbols
11
+ def self.normalize_params_to_hash(value, keys)
12
+ if value.is_a?(Array) && value.size == keys.size
13
+ # Convert array to hash for better performance with large params
14
+ keys.each_with_index.with_object({}) do |(key, idx), hash|
15
+ hash[key] = value[idx]
16
+ end
17
+ elsif value.is_a?(Hash)
18
+ # Normalize hash keys to symbols for consistency
19
+ value.each_with_object({}) do |(k, v), h|
20
+ key = k.is_a?(String) ? k.to_sym : k
21
+ h[key] = v
22
+ end
23
+ else
24
+ value
25
+ end
26
+ end
27
+
28
+ # Normalize parameter value for cache key generation
29
+ def self.normalize_param_cache_key(value, prefix)
30
+ case value
31
+ when Array
32
+ "#{prefix}:#{value.inspect}"
33
+ when Hash
34
+ # Normalize keys to symbols and sort for consistent cache keys
35
+ normalized = value.each_with_object({}) do |(k, v), h|
36
+ key = k.is_a?(String) ? k.to_sym : k
37
+ h[key] = v
38
+ end
39
+ sorted_keys = normalized.keys.sort
40
+ "#{prefix}:#{sorted_keys.map { |k| "#{k}:#{normalized[k]}" }.join(',')}"
41
+ else
42
+ "#{prefix}:#{value.inspect}"
43
+ end
44
+ end
45
+
46
+ # Compare aggregation result with expected value (supports hash with comparison operators)
47
+ def self.compare_aggregation_result(actual, expected)
48
+ if expected.is_a?(Hash)
49
+ result = true
50
+ result &&= (actual >= expected[:min]) if expected[:min]
51
+ result &&= (actual <= expected[:max]) if expected[:max]
52
+ result &&= (actual > expected[:gt]) if expected[:gt]
53
+ result &&= (actual < expected[:lt]) if expected[:lt]
54
+ result &&= (actual >= expected[:gte]) if expected[:gte]
55
+ result &&= (actual <= expected[:lte]) if expected[:lte]
56
+ result &&= (actual == expected[:eq]) if expected[:eq]
57
+ result
58
+ else
59
+ actual == expected
60
+ end
61
+ end
62
+
63
+ # Epsilon comparison for floating point numbers
64
+ def self.epsilon_equal?(a, b, epsilon = 1e-10) # rubocop:disable Naming/MethodParameterName
65
+ (a - b).abs < epsilon
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles basic comparison operators: eq, neq, gt, gte, lt, lte, in, present, blank
7
+ module BasicComparisonOperators
8
+ def self.handle(op, actual_value, expected_value)
9
+ case op
10
+ when "eq"
11
+ # Equality - uses Ruby's == for comparison
12
+ actual_value == expected_value
13
+
14
+ when "neq"
15
+ # Not equal - inverse of ==
16
+ actual_value != expected_value
17
+
18
+ when "gt"
19
+ # Greater than - only for comparable types (numbers, strings)
20
+ comparable?(actual_value, expected_value) && actual_value > expected_value
21
+
22
+ when "gte"
23
+ # Greater than or equal - only for comparable types
24
+ comparable?(actual_value, expected_value) && actual_value >= expected_value
25
+
26
+ when "lt"
27
+ # Less than - only for comparable types
28
+ comparable?(actual_value, expected_value) && actual_value < expected_value
29
+
30
+ when "lte"
31
+ # Less than or equal - only for comparable types
32
+ comparable?(actual_value, expected_value) && actual_value <= expected_value
33
+
34
+ when "in"
35
+ # Array membership - checks if actual_value is in the expected array
36
+ Array(expected_value).include?(actual_value)
37
+
38
+ when "present"
39
+ # PRESENT SEMANTICS:
40
+ # Returns true if value exists AND is not empty
41
+ # - nil: false
42
+ # - Empty string "": false
43
+ # - Empty array []: false
44
+ # - Empty hash {}: false
45
+ # - Zero 0: true (zero is a valid value)
46
+ # - False boolean: true (false is a valid value)
47
+ # - Non-empty values: true
48
+ !actual_value.nil? && (actual_value.respond_to?(:empty?) ? !actual_value.empty? : true)
49
+
50
+ when "blank"
51
+ # BLANK SEMANTICS:
52
+ # Returns true if value is nil OR empty
53
+ # - nil: true
54
+ # - Empty string "": true
55
+ # - Empty array []: true
56
+ # - Empty hash {}: true
57
+ # - Zero 0: false (zero is a valid value)
58
+ # - False boolean: false (false is a valid value)
59
+ # - Non-empty values: false
60
+ actual_value.nil? || (actual_value.respond_to?(:empty?) ? actual_value.empty? : false)
61
+ end
62
+ # Returns nil if not handled by this module
63
+ end
64
+
65
+ # Checks if two values can be compared with <, >, <=, >=
66
+ # Allows comparison between numeric types (Float, Integer, etc.) or same string types
67
+ def self.comparable?(val1, val2)
68
+ # Both are numeric - allow comparison between different numeric types
69
+ # (e.g., Integer and Float are comparable in Ruby)
70
+ return true if val1.is_a?(Numeric) && val2.is_a?(Numeric)
71
+
72
+ # Both are strings - require exact same type
73
+ return val1.instance_of?(val2.class) if val1.is_a?(String) && val2.is_a?(String)
74
+
75
+ false
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles collection operators: contains_all, contains_any, intersects, subset_of
7
+ module CollectionOperators
8
+ def self.handle(op, actual_value, expected_value)
9
+ case op
10
+ when "contains_all"
11
+ # Checks if array contains all specified elements
12
+ return false unless actual_value.is_a?(Array)
13
+ return false unless expected_value.is_a?(Array)
14
+ return true if expected_value.empty?
15
+
16
+ # OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
17
+ actual_set = actual_value.to_set
18
+ expected_value.all? { |item| actual_set.include?(item) }
19
+
20
+ when "contains_any"
21
+ # Checks if array contains any of the specified elements
22
+ return false unless actual_value.is_a?(Array)
23
+ return false unless expected_value.is_a?(Array)
24
+ return false if expected_value.empty?
25
+
26
+ # OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
27
+ actual_set = actual_value.to_set
28
+ expected_value.any? { |item| actual_set.include?(item) }
29
+
30
+ when "intersects"
31
+ # Checks if two arrays have any common elements
32
+ return false unless actual_value.is_a?(Array)
33
+ return false unless expected_value.is_a?(Array)
34
+ return false if actual_value.empty? || expected_value.empty?
35
+
36
+ # OPTIMIZE: Use Set intersection for O(n) instead of array & which creates intermediate array
37
+ if actual_value.size <= expected_value.size
38
+ expected_set = expected_value.to_set
39
+ actual_value.any? { |item| expected_set.include?(item) }
40
+ else
41
+ actual_set = actual_value.to_set
42
+ expected_value.any? { |item| actual_set.include?(item) }
43
+ end
44
+
45
+ when "subset_of"
46
+ # Checks if array is a subset of another array
47
+ return false unless actual_value.is_a?(Array)
48
+ return false unless expected_value.is_a?(Array)
49
+ return true if actual_value.empty?
50
+
51
+ # OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
52
+ expected_set = expected_value.to_set
53
+ actual_value.all? { |item| expected_set.include?(item) }
54
+ end
55
+ # Returns nil if not handled by this module
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end