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
@@ -1,587 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent::Evaluators::JsonRuleEvaluator do
4
- describe "basic rule matching" do
5
- it "matches simple equality rule" do
6
- rules = {
7
- version: "1.0",
8
- ruleset: "test",
9
- rules: [
10
- {
11
- id: "rule_1",
12
- if: { field: "status", op: "eq", value: "active" },
13
- then: { decision: "approve", weight: 0.8, reason: "Status is active" }
14
- }
15
- ]
16
- }
17
-
18
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
19
- context = DecisionAgent::Context.new({ status: "active" })
20
-
21
- evaluation = evaluator.evaluate(context)
22
-
23
- expect(evaluation).not_to be_nil
24
- expect(evaluation.decision).to eq("approve")
25
- expect(evaluation.weight).to eq(0.8)
26
- expect(evaluation.reason).to eq("Status is active")
27
- expect(evaluation.metadata[:rule_id]).to eq("rule_1")
28
- end
29
-
30
- it "returns nil when no rules match" do
31
- rules = {
32
- version: "1.0",
33
- ruleset: "test",
34
- rules: [
35
- {
36
- id: "rule_1",
37
- if: { field: "status", op: "eq", value: "active" },
38
- then: { decision: "approve" }
39
- }
40
- ]
41
- }
42
-
43
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
44
- context = DecisionAgent::Context.new({ status: "inactive" })
45
-
46
- evaluation = evaluator.evaluate(context)
47
-
48
- expect(evaluation).to be_nil
49
- end
50
-
51
- it "matches first rule when multiple rules match" do
52
- rules = {
53
- version: "1.0",
54
- ruleset: "test",
55
- rules: [
56
- {
57
- id: "rule_1",
58
- if: { field: "priority", op: "eq", value: "high" },
59
- then: { decision: "escalate" }
60
- },
61
- {
62
- id: "rule_2",
63
- if: { field: "priority", op: "eq", value: "high" },
64
- then: { decision: "notify" }
65
- }
66
- ]
67
- }
68
-
69
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
70
- context = DecisionAgent::Context.new({ priority: "high" })
71
-
72
- evaluation = evaluator.evaluate(context)
73
-
74
- expect(evaluation.decision).to eq("escalate")
75
- expect(evaluation.metadata[:rule_id]).to eq("rule_1")
76
- end
77
- end
78
-
79
- describe "all/any conditions" do
80
- it "matches 'all' condition when all sub-conditions are true" do
81
- rules = {
82
- version: "1.0",
83
- ruleset: "test",
84
- rules: [
85
- {
86
- id: "rule_1",
87
- if: {
88
- all: [
89
- { field: "priority", op: "eq", value: "high" },
90
- { field: "hours", op: "gte", value: 4 }
91
- ]
92
- },
93
- then: { decision: "notify" }
94
- }
95
- ]
96
- }
97
-
98
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
99
- context = DecisionAgent::Context.new({ priority: "high", hours: 5 })
100
-
101
- evaluation = evaluator.evaluate(context)
102
-
103
- expect(evaluation).not_to be_nil
104
- expect(evaluation.decision).to eq("notify")
105
- end
106
-
107
- it "does not match 'all' when one sub-condition fails" do
108
- rules = {
109
- version: "1.0",
110
- ruleset: "test",
111
- rules: [
112
- {
113
- id: "rule_1",
114
- if: {
115
- all: [
116
- { field: "priority", op: "eq", value: "high" },
117
- { field: "hours", op: "gte", value: 4 }
118
- ]
119
- },
120
- then: { decision: "notify" }
121
- }
122
- ]
123
- }
124
-
125
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
126
- context = DecisionAgent::Context.new({ priority: "high", hours: 2 })
127
-
128
- evaluation = evaluator.evaluate(context)
129
-
130
- expect(evaluation).to be_nil
131
- end
132
-
133
- it "matches 'any' condition when at least one sub-condition is true" do
134
- rules = {
135
- version: "1.0",
136
- ruleset: "test",
137
- rules: [
138
- {
139
- id: "rule_1",
140
- if: {
141
- any: [
142
- { field: "priority", op: "eq", value: "critical" },
143
- { field: "hours", op: "gte", value: 24 }
144
- ]
145
- },
146
- then: { decision: "escalate" }
147
- }
148
- ]
149
- }
150
-
151
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
152
- context = DecisionAgent::Context.new({ priority: "low", hours: 30 })
153
-
154
- evaluation = evaluator.evaluate(context)
155
-
156
- expect(evaluation).not_to be_nil
157
- expect(evaluation.decision).to eq("escalate")
158
- end
159
-
160
- it "does not match 'any' when all sub-conditions fail" do
161
- rules = {
162
- version: "1.0",
163
- ruleset: "test",
164
- rules: [
165
- {
166
- id: "rule_1",
167
- if: {
168
- any: [
169
- { field: "priority", op: "eq", value: "critical" },
170
- { field: "hours", op: "gte", value: 24 }
171
- ]
172
- },
173
- then: { decision: "escalate" }
174
- }
175
- ]
176
- }
177
-
178
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
179
- context = DecisionAgent::Context.new({ priority: "low", hours: 5 })
180
-
181
- evaluation = evaluator.evaluate(context)
182
-
183
- expect(evaluation).to be_nil
184
- end
185
- end
186
-
187
- describe "comparison operators" do
188
- it "supports gt (greater than)" do
189
- rules = {
190
- version: "1.0",
191
- ruleset: "test",
192
- rules: [
193
- {
194
- id: "rule_1",
195
- if: { field: "score", op: "gt", value: 80 },
196
- then: { decision: "pass" }
197
- }
198
- ]
199
- }
200
-
201
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
202
-
203
- context1 = DecisionAgent::Context.new({ score: 85 })
204
- context2 = DecisionAgent::Context.new({ score: 80 })
205
-
206
- expect(evaluator.evaluate(context1)).not_to be_nil
207
- expect(evaluator.evaluate(context2)).to be_nil
208
- end
209
-
210
- it "supports gte (greater than or equal)" do
211
- rules = {
212
- version: "1.0",
213
- ruleset: "test",
214
- rules: [
215
- {
216
- id: "rule_1",
217
- if: { field: "score", op: "gte", value: 80 },
218
- then: { decision: "pass" }
219
- }
220
- ]
221
- }
222
-
223
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
224
-
225
- context1 = DecisionAgent::Context.new({ score: 80 })
226
- context2 = DecisionAgent::Context.new({ score: 79 })
227
-
228
- expect(evaluator.evaluate(context1)).not_to be_nil
229
- expect(evaluator.evaluate(context2)).to be_nil
230
- end
231
-
232
- it "supports lt (less than)" do
233
- rules = {
234
- version: "1.0",
235
- ruleset: "test",
236
- rules: [
237
- {
238
- id: "rule_1",
239
- if: { field: "temperature", op: "lt", value: 32 },
240
- then: { decision: "freeze" }
241
- }
242
- ]
243
- }
244
-
245
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
246
-
247
- context1 = DecisionAgent::Context.new({ temperature: 30 })
248
- context2 = DecisionAgent::Context.new({ temperature: 32 })
249
-
250
- expect(evaluator.evaluate(context1)).not_to be_nil
251
- expect(evaluator.evaluate(context2)).to be_nil
252
- end
253
-
254
- it "supports lte (less than or equal)" do
255
- rules = {
256
- version: "1.0",
257
- ruleset: "test",
258
- rules: [
259
- {
260
- id: "rule_1",
261
- if: { field: "temperature", op: "lte", value: 32 },
262
- then: { decision: "freeze" }
263
- }
264
- ]
265
- }
266
-
267
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
268
-
269
- context1 = DecisionAgent::Context.new({ temperature: 32 })
270
- context2 = DecisionAgent::Context.new({ temperature: 33 })
271
-
272
- expect(evaluator.evaluate(context1)).not_to be_nil
273
- expect(evaluator.evaluate(context2)).to be_nil
274
- end
275
-
276
- it "supports neq (not equal)" do
277
- rules = {
278
- version: "1.0",
279
- ruleset: "test",
280
- rules: [
281
- {
282
- id: "rule_1",
283
- if: { field: "status", op: "neq", value: "closed" },
284
- then: { decision: "process" }
285
- }
286
- ]
287
- }
288
-
289
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
290
-
291
- context1 = DecisionAgent::Context.new({ status: "open" })
292
- context2 = DecisionAgent::Context.new({ status: "closed" })
293
-
294
- expect(evaluator.evaluate(context1)).not_to be_nil
295
- expect(evaluator.evaluate(context2)).to be_nil
296
- end
297
-
298
- it "supports in (array membership)" do
299
- rules = {
300
- version: "1.0",
301
- ruleset: "test",
302
- rules: [
303
- {
304
- id: "rule_1",
305
- if: { field: "status", op: "in", value: %w[open pending review] },
306
- then: { decision: "active" }
307
- }
308
- ]
309
- }
310
-
311
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
312
-
313
- context1 = DecisionAgent::Context.new({ status: "pending" })
314
- context2 = DecisionAgent::Context.new({ status: "closed" })
315
-
316
- expect(evaluator.evaluate(context1)).not_to be_nil
317
- expect(evaluator.evaluate(context2)).to be_nil
318
- end
319
-
320
- it "supports present (field exists and not empty)" do
321
- rules = {
322
- version: "1.0",
323
- ruleset: "test",
324
- rules: [
325
- {
326
- id: "rule_1",
327
- if: { field: "assignee", op: "present" },
328
- then: { decision: "assigned" }
329
- }
330
- ]
331
- }
332
-
333
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
334
-
335
- context1 = DecisionAgent::Context.new({ assignee: "alice" })
336
- context2 = DecisionAgent::Context.new({ assignee: nil })
337
- context3 = DecisionAgent::Context.new({ assignee: "" })
338
- context4 = DecisionAgent::Context.new({})
339
-
340
- expect(evaluator.evaluate(context1)).not_to be_nil
341
- expect(evaluator.evaluate(context2)).to be_nil
342
- expect(evaluator.evaluate(context3)).to be_nil
343
- expect(evaluator.evaluate(context4)).to be_nil
344
- end
345
-
346
- it "supports blank (field missing, nil, or empty)" do
347
- rules = {
348
- version: "1.0",
349
- ruleset: "test",
350
- rules: [
351
- {
352
- id: "rule_1",
353
- if: { field: "description", op: "blank" },
354
- then: { decision: "needs_description" }
355
- }
356
- ]
357
- }
358
-
359
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
360
-
361
- context1 = DecisionAgent::Context.new({ description: "" })
362
- context2 = DecisionAgent::Context.new({ description: nil })
363
- context3 = DecisionAgent::Context.new({})
364
- context4 = DecisionAgent::Context.new({ description: "valid" })
365
-
366
- expect(evaluator.evaluate(context1)).not_to be_nil
367
- expect(evaluator.evaluate(context2)).not_to be_nil
368
- expect(evaluator.evaluate(context3)).not_to be_nil
369
- expect(evaluator.evaluate(context4)).to be_nil
370
- end
371
- end
372
-
373
- describe "nested field access" do
374
- it "supports dot notation for nested fields" do
375
- rules = {
376
- version: "1.0",
377
- ruleset: "test",
378
- rules: [
379
- {
380
- id: "rule_1",
381
- if: { field: "user.role", op: "eq", value: "admin" },
382
- then: { decision: "allow" }
383
- }
384
- ]
385
- }
386
-
387
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
388
-
389
- context1 = DecisionAgent::Context.new({ user: { role: "admin" } })
390
- context2 = DecisionAgent::Context.new({ user: { role: "user" } })
391
-
392
- expect(evaluator.evaluate(context1)).not_to be_nil
393
- expect(evaluator.evaluate(context2)).to be_nil
394
- end
395
-
396
- it "handles missing nested fields gracefully" do
397
- rules = {
398
- version: "1.0",
399
- ruleset: "test",
400
- rules: [
401
- {
402
- id: "rule_1",
403
- if: { field: "user.role", op: "eq", value: "admin" },
404
- then: { decision: "allow" }
405
- }
406
- ]
407
- }
408
-
409
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
410
-
411
- context1 = DecisionAgent::Context.new({})
412
- context2 = DecisionAgent::Context.new({ user: nil })
413
-
414
- expect(evaluator.evaluate(context1)).to be_nil
415
- expect(evaluator.evaluate(context2)).to be_nil
416
- end
417
- end
418
-
419
- describe "invalid DSL handling" do
420
- it "raises InvalidRuleDslError for malformed JSON" do
421
- expect do
422
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: "{ invalid json")
423
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Invalid JSON/)
424
- end
425
-
426
- it "raises InvalidRuleDslError when version is missing" do
427
- rules = { rules: [] }
428
-
429
- expect do
430
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
431
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /version/)
432
- end
433
-
434
- it "raises InvalidRuleDslError when rules array is missing" do
435
- rules = { version: "1.0" }
436
-
437
- expect do
438
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
439
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /rules/)
440
- end
441
-
442
- it "raises InvalidRuleDslError when rule is missing id" do
443
- rules = {
444
- version: "1.0",
445
- rules: [
446
- {
447
- if: { field: "status", op: "eq", value: "active" },
448
- then: { decision: "approve" }
449
- }
450
- ]
451
- }
452
-
453
- expect do
454
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
455
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'id'/)
456
- end
457
-
458
- it "raises InvalidRuleDslError when rule is missing if clause" do
459
- rules = {
460
- version: "1.0",
461
- rules: [
462
- {
463
- id: "rule_1",
464
- then: { decision: "approve" }
465
- }
466
- ]
467
- }
468
-
469
- expect do
470
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
471
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'if'/)
472
- end
473
-
474
- it "raises InvalidRuleDslError when rule is missing then clause" do
475
- rules = {
476
- version: "1.0",
477
- rules: [
478
- {
479
- id: "rule_1",
480
- if: { field: "status", op: "eq", value: "active" }
481
- }
482
- ]
483
- }
484
-
485
- expect do
486
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
487
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'then'/)
488
- end
489
-
490
- it "raises InvalidRuleDslError when then clause is missing decision" do
491
- rules = {
492
- version: "1.0",
493
- rules: [
494
- {
495
- id: "rule_1",
496
- if: { field: "status", op: "eq", value: "active" },
497
- then: { weight: 0.8 }
498
- }
499
- ]
500
- }
501
-
502
- expect do
503
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
504
- end.to raise_error(DecisionAgent::InvalidRuleDslError, /Missing required field 'decision'/)
505
- end
506
- end
507
-
508
- describe "default values" do
509
- it "uses default weight of 1.0 when not specified" do
510
- rules = {
511
- version: "1.0",
512
- ruleset: "test",
513
- rules: [
514
- {
515
- id: "rule_1",
516
- if: { field: "status", op: "eq", value: "active" },
517
- then: { decision: "approve" }
518
- }
519
- ]
520
- }
521
-
522
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
523
- context = DecisionAgent::Context.new({ status: "active" })
524
-
525
- evaluation = evaluator.evaluate(context)
526
-
527
- expect(evaluation.weight).to eq(1.0)
528
- end
529
-
530
- it "uses default reason when not specified" do
531
- rules = {
532
- version: "1.0",
533
- ruleset: "test",
534
- rules: [
535
- {
536
- id: "rule_1",
537
- if: { field: "status", op: "eq", value: "active" },
538
- then: { decision: "approve" }
539
- }
540
- ]
541
- }
542
-
543
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
544
- context = DecisionAgent::Context.new({ status: "active" })
545
-
546
- evaluation = evaluator.evaluate(context)
547
-
548
- expect(evaluation.reason).to eq("Rule matched")
549
- end
550
- end
551
-
552
- describe "complex nested conditions" do
553
- it "handles nested all/any combinations" do
554
- rules = {
555
- version: "1.0",
556
- ruleset: "test",
557
- rules: [
558
- {
559
- id: "rule_1",
560
- if: {
561
- all: [
562
- { field: "priority", op: "eq", value: "high" },
563
- {
564
- any: [
565
- { field: "hours", op: "gte", value: 24 },
566
- { field: "escalated", op: "eq", value: true }
567
- ]
568
- }
569
- ]
570
- },
571
- then: { decision: "urgent" }
572
- }
573
- ]
574
- }
575
-
576
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
577
-
578
- context1 = DecisionAgent::Context.new({ priority: "high", hours: 30, escalated: false })
579
- context2 = DecisionAgent::Context.new({ priority: "high", hours: 5, escalated: true })
580
- context3 = DecisionAgent::Context.new({ priority: "low", hours: 30, escalated: true })
581
-
582
- expect(evaluator.evaluate(context1)).not_to be_nil
583
- expect(evaluator.evaluate(context2)).not_to be_nil
584
- expect(evaluator.evaluate(context3)).to be_nil
585
- end
586
- end
587
- end