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,202 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent"
3
- require "decision_agent/dmn/model"
4
- require "decision_agent/evaluators/dmn_evaluator"
5
-
6
- RSpec.describe "DMN Hit Policies" do
7
- let(:model) { DecisionAgent::Dmn::Model.new(id: "test_model", name: "Test Model") }
8
-
9
- def create_decision_table(hit_policy, rules_data)
10
- table = DecisionAgent::Dmn::DecisionTable.new(id: "test_table", hit_policy: hit_policy)
11
-
12
- # Add inputs
13
- table.add_input(DecisionAgent::Dmn::Input.new(id: "input1", label: "value"))
14
- table.add_output(DecisionAgent::Dmn::Output.new(id: "output1", label: "decision", name: "decision"))
15
-
16
- # Add rules
17
- rules_data.each do |rule_data|
18
- rule = DecisionAgent::Dmn::Rule.new(id: rule_data[:id], description: rule_data[:description])
19
- rule.add_input_entry(rule_data[:input])
20
- rule.add_output_entry(rule_data[:output])
21
- table.add_rule(rule)
22
- end
23
-
24
- decision = DecisionAgent::Dmn::Decision.new(id: "test_decision", name: "Test Decision")
25
- decision.decision_table = table
26
- model.add_decision(decision)
27
-
28
- table
29
- end
30
-
31
- describe "UNIQUE hit policy" do
32
- it "returns result when exactly one rule matches" do
33
- create_decision_table("UNIQUE", [
34
- { id: "rule1", input: ">= 10", output: '"approved"', description: "High value" },
35
- { id: "rule2", input: "< 10", output: '"rejected"', description: "Low value" }
36
- ])
37
-
38
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
39
- model: model,
40
- decision_id: "test_decision"
41
- )
42
-
43
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
44
- expect(evaluation).not_to be_nil
45
- expect(evaluation.decision).to eq("approved")
46
- end
47
-
48
- it "raises error when no rules match" do
49
- create_decision_table("UNIQUE", [
50
- { id: "rule1", input: ">= 10", output: '"approved"', description: "High value" },
51
- { id: "rule2", input: "> 20", output: '"rejected"', description: "Very high value" }
52
- ])
53
-
54
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
55
- model: model,
56
- decision_id: "test_decision"
57
- )
58
-
59
- # Value 5 doesn't match >= 10 or > 20, so no rules match
60
- expect do
61
- evaluator.evaluate(DecisionAgent::Context.new(value: 5))
62
- end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /UNIQUE hit policy requires exactly one matching rule/)
63
- end
64
-
65
- it "raises error when multiple rules match" do
66
- create_decision_table("UNIQUE", [
67
- { id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
68
- { id: "rule2", input: ">= 10", output: '"approved"', description: "Rule 2" }
69
- ])
70
-
71
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
72
- model: model,
73
- decision_id: "test_decision"
74
- )
75
-
76
- expect do
77
- evaluator.evaluate(DecisionAgent::Context.new(value: 15))
78
- end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /UNIQUE hit policy requires exactly one matching rule, but 2 matched/)
79
- end
80
- end
81
-
82
- describe "FIRST hit policy" do
83
- it "returns first matching rule when multiple rules match" do
84
- create_decision_table("FIRST", [
85
- { id: "rule1", input: ">= 5", output: '"first"', description: "First rule" },
86
- { id: "rule2", input: ">= 10", output: '"second"', description: "Second rule" }
87
- ])
88
-
89
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
90
- model: model,
91
- decision_id: "test_decision"
92
- )
93
-
94
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
95
- expect(evaluation).not_to be_nil
96
- expect(evaluation.decision).to eq("first")
97
- expect(evaluation.metadata[:rule_id]).to eq("rule1")
98
- end
99
-
100
- it "returns nil when no rules match" do
101
- create_decision_table("FIRST", [
102
- { id: "rule1", input: ">= 10", output: '"approved"', description: "High value" }
103
- ])
104
-
105
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
106
- model: model,
107
- decision_id: "test_decision"
108
- )
109
-
110
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 5))
111
- expect(evaluation).to be_nil
112
- end
113
- end
114
-
115
- describe "PRIORITY hit policy" do
116
- it "returns first matching rule (rule order determines priority)" do
117
- create_decision_table("PRIORITY", [
118
- { id: "rule1", input: ">= 5", output: '"high_priority"', description: "High priority rule" },
119
- { id: "rule2", input: ">= 10", output: '"low_priority"', description: "Low priority rule" }
120
- ])
121
-
122
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
123
- model: model,
124
- decision_id: "test_decision"
125
- )
126
-
127
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
128
- expect(evaluation).not_to be_nil
129
- expect(evaluation.decision).to eq("high_priority")
130
- expect(evaluation.metadata[:rule_id]).to eq("rule1")
131
- end
132
- end
133
-
134
- describe "ANY hit policy" do
135
- it "returns result when all matching rules have same output" do
136
- create_decision_table("ANY", [
137
- { id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
138
- { id: "rule2", input: ">= 10", output: '"approved"', description: "Rule 2" }
139
- ])
140
-
141
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
142
- model: model,
143
- decision_id: "test_decision"
144
- )
145
-
146
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
147
- expect(evaluation).not_to be_nil
148
- expect(evaluation.decision).to eq("approved")
149
- end
150
-
151
- it "raises error when matching rules have different outputs" do
152
- create_decision_table("ANY", [
153
- { id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
154
- { id: "rule2", input: ">= 10", output: '"rejected"', description: "Rule 2" }
155
- ])
156
-
157
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
158
- model: model,
159
- decision_id: "test_decision"
160
- )
161
-
162
- expect do
163
- evaluator.evaluate(DecisionAgent::Context.new(value: 15))
164
- end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /ANY hit policy requires all matching rules to have the same output/)
165
- end
166
- end
167
-
168
- describe "COLLECT hit policy" do
169
- it "returns first match with metadata about all matches" do
170
- create_decision_table("COLLECT", [
171
- { id: "rule1", input: ">= 5", output: '"match1"', description: "Rule 1" },
172
- { id: "rule2", input: ">= 10", output: '"match2"', description: "Rule 2" }
173
- ])
174
-
175
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
176
- model: model,
177
- decision_id: "test_decision"
178
- )
179
-
180
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
181
- expect(evaluation).not_to be_nil
182
- expect(evaluation.decision).to eq("match1")
183
- expect(evaluation.metadata[:collect_count]).to eq(2)
184
- expect(evaluation.metadata[:collect_decisions]).to eq(%w[match1 match2])
185
- expect(evaluation.metadata[:collect_rule_ids]).to eq(%w[rule1 rule2])
186
- end
187
-
188
- it "returns nil when no rules match" do
189
- create_decision_table("COLLECT", [
190
- { id: "rule1", input: ">= 10", output: '"approved"', description: "High value" }
191
- ])
192
-
193
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
194
- model: model,
195
- decision_id: "test_decision"
196
- )
197
-
198
- evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 5))
199
- expect(evaluation).to be_nil
200
- end
201
- end
202
- end
@@ -1,226 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent"
3
- require "decision_agent/dmn/importer"
4
- require "decision_agent/dmn/exporter"
5
- require "decision_agent/evaluators/dmn_evaluator"
6
- require "tempfile"
7
- require "fileutils"
8
-
9
- RSpec.describe "DMN Integration" do
10
- let(:simple_dmn_path) { File.expand_path("../fixtures/dmn/simple_decision.dmn", __dir__) }
11
- let(:complex_dmn_path) { File.expand_path("../fixtures/dmn/complex_decision.dmn", __dir__) }
12
- let(:invalid_dmn_path) { File.expand_path("../fixtures/dmn/invalid_structure.dmn", __dir__) }
13
-
14
- # Create temporary directory for file storage adapter
15
- let(:temp_dir) { Dir.mktmpdir }
16
- let(:version_manager) do
17
- DecisionAgent::Versioning::VersionManager.new(
18
- adapter: DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: temp_dir)
19
- )
20
- end
21
-
22
- after do
23
- FileUtils.rm_rf(temp_dir)
24
- end
25
-
26
- describe "import and execute simple decision" do
27
- it "imports DMN file and makes decisions" do
28
- # Import
29
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
30
- result = importer.import(simple_dmn_path, created_by: "test")
31
-
32
- expect(result[:decisions_imported]).to eq(1)
33
- expect(result[:model]).to be_a(DecisionAgent::Dmn::Model)
34
- expect(result[:model].decisions.size).to eq(1)
35
-
36
- # Create evaluator
37
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
38
- model: result[:model],
39
- decision_id: "age_check"
40
- )
41
-
42
- # Test approval case (age >= 18)
43
- context_approve = DecisionAgent::Context.new({ age: 25 })
44
- evaluation_approve = evaluator.evaluate(context_approve)
45
-
46
- expect(evaluation_approve).not_to be_nil
47
- expect(evaluation_approve.decision).to eq("approve")
48
- expect(evaluation_approve.evaluator_name).to include("DmnEvaluator")
49
-
50
- # Test rejection case (age < 18)
51
- context_reject = DecisionAgent::Context.new({ age: 15 })
52
- evaluation_reject = evaluator.evaluate(context_reject)
53
-
54
- expect(evaluation_reject).not_to be_nil
55
- expect(evaluation_reject.decision).to eq("reject")
56
- end
57
- end
58
-
59
- describe "import and execute complex decision" do
60
- it "handles multi-input decision tables" do
61
- # Import
62
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
63
- result = importer.import(complex_dmn_path, created_by: "test")
64
-
65
- expect(result[:decisions_imported]).to eq(1)
66
-
67
- # Create evaluator
68
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
69
- model: result[:model],
70
- decision_id: "loan_approval"
71
- )
72
-
73
- # Test excellent case
74
- context_excellent = DecisionAgent::Context.new({
75
- credit_score: 800,
76
- income: 75_000,
77
- loan_amount: 150_000
78
- })
79
- evaluation_excellent = evaluator.evaluate(context_excellent)
80
-
81
- expect(evaluation_excellent).not_to be_nil
82
- expect(evaluation_excellent.decision).to eq("approve")
83
-
84
- # Test good case
85
- context_good = DecisionAgent::Context.new({
86
- credit_score: 700,
87
- income: 45_000,
88
- loan_amount: 100_000
89
- })
90
- evaluation_good = evaluator.evaluate(context_good)
91
-
92
- expect(evaluation_good).not_to be_nil
93
- expect(evaluation_good.decision).to eq("conditional_approve")
94
-
95
- # Test rejection case
96
- context_reject = DecisionAgent::Context.new({
97
- credit_score: 500,
98
- income: 25_000,
99
- loan_amount: 100_000
100
- })
101
- evaluation_reject = evaluator.evaluate(context_reject)
102
-
103
- expect(evaluation_reject).not_to be_nil
104
- expect(evaluation_reject.decision).to eq("reject")
105
- end
106
- end
107
-
108
- describe "invalid DMN handling" do
109
- it "validates and rejects invalid DMN structure" do
110
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
111
-
112
- expect do
113
- importer.import(invalid_dmn_path, created_by: "test")
114
- end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /Expected 1 input entries, got 2/)
115
- end
116
- end
117
-
118
- describe "round-trip conversion" do
119
- it "preserves structure through import-export-import cycle" do
120
- # Import original
121
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
122
- original = importer.import(simple_dmn_path, ruleset_name: "age_check_v1", created_by: "test")
123
-
124
- expect(original[:decisions_imported]).to eq(1)
125
-
126
- # Export
127
- exporter = DecisionAgent::Dmn::Exporter.new(version_manager: version_manager)
128
- exported_xml = exporter.export("age_check_v1")
129
-
130
- expect(exported_xml).to include('xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"')
131
- expect(exported_xml).to include("<decision")
132
- expect(exported_xml).to include("<decisionTable")
133
-
134
- # Re-import
135
- reimported = importer.import_from_xml(exported_xml, ruleset_name: "age_check_v2", created_by: "test")
136
-
137
- # Compare structures
138
- expect(reimported[:model].decisions.size).to eq(original[:model].decisions.size)
139
- expect(reimported[:decisions_imported]).to eq(original[:decisions_imported])
140
-
141
- # Verify it still works
142
- evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
143
- model: reimported[:model],
144
- decision_id: reimported[:model].decisions.first.id
145
- )
146
-
147
- context = DecisionAgent::Context.new({ age: 25 })
148
- evaluation = evaluator.evaluate(context)
149
-
150
- expect(evaluation).not_to be_nil
151
- expect(evaluation.decision).to eq("approve")
152
- end
153
- end
154
-
155
- describe "combining with JSON evaluators" do
156
- it "works alongside JsonRuleEvaluator in same agent" do
157
- # Load DMN
158
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
159
- dmn_result = importer.import(simple_dmn_path, created_by: "test")
160
-
161
- dmn_evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
162
- model: dmn_result[:model],
163
- decision_id: "age_check"
164
- )
165
-
166
- # Create JSON evaluator
167
- json_rules = {
168
- version: "1.0",
169
- ruleset: "json_rules",
170
- rules: [
171
- {
172
- id: "priority_rule",
173
- if: { field: "priority", op: "eq", value: "high" },
174
- then: { decision: "escalate", weight: 0.9, reason: "High priority escalation" }
175
- }
176
- ]
177
- }
178
-
179
- json_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(
180
- rules_json: json_rules
181
- )
182
-
183
- # Use both in agent
184
- agent = DecisionAgent::Agent.new(
185
- evaluators: [dmn_evaluator, json_evaluator]
186
- )
187
-
188
- # Test with context matching both evaluators
189
- decision = agent.decide(
190
- context: { age: 25, priority: "high" }
191
- )
192
-
193
- expect(%w[approve escalate]).to include(decision.decision)
194
- expect(decision.evaluations.size).to eq(2)
195
- expect(decision.evaluations.map(&:evaluator_name)).to include(
196
- match(/DmnEvaluator/),
197
- match(/JsonRuleEvaluator/)
198
- )
199
- end
200
- end
201
-
202
- describe "versioning integration" do
203
- it "stores and retrieves DMN models from versioning system" do
204
- importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
205
-
206
- # Import first version
207
- v1 = importer.import(simple_dmn_path, ruleset_name: "age_check", created_by: "test_user")
208
-
209
- expect(v1[:versions].size).to eq(1)
210
- expect(v1[:versions].first[:rule_id]).to eq("age_check")
211
-
212
- # Get active version
213
- active = version_manager.get_active_version(rule_id: "age_check")
214
- expect(active).not_to be_nil
215
- expect(active[:content]).to be_a(Hash)
216
- # Content may have string or symbol keys depending on storage adapter
217
- rules = active[:content]["rules"] || active[:content][:rules]
218
- expect(rules).to be_an(Array)
219
-
220
- # Get version history
221
- versions = version_manager.get_versions(rule_id: "age_check")
222
- expect(versions.size).to eq(1)
223
- expect(versions.first[:created_by]).to eq("test_user")
224
- end
225
- end
226
- end