decision_agent 0.3.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "decision_agent/dmn/decision_graph"
|
|
5
|
-
|
|
6
|
-
RSpec.describe DecisionAgent::Dmn::DecisionGraph do
|
|
7
|
-
describe "graph construction" do
|
|
8
|
-
let(:graph) do
|
|
9
|
-
described_class.new(id: "graph1", name: "Test Graph")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it "creates an empty graph" do
|
|
13
|
-
expect(graph.decisions).to be_empty
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
it "adds decisions to the graph" do
|
|
17
|
-
decision = DecisionAgent::Dmn::DecisionNode.new(
|
|
18
|
-
id: "decision1",
|
|
19
|
-
name: "First Decision"
|
|
20
|
-
)
|
|
21
|
-
graph.add_decision(decision)
|
|
22
|
-
|
|
23
|
-
expect(graph.decisions["decision1"]).to eq(decision)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it "retrieves decisions by id" do
|
|
27
|
-
decision = DecisionAgent::Dmn::DecisionNode.new(id: "decision1", name: "Test")
|
|
28
|
-
graph.add_decision(decision)
|
|
29
|
-
|
|
30
|
-
retrieved = graph.get_decision("decision1")
|
|
31
|
-
expect(retrieved).to eq(decision)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
describe "decision dependencies" do
|
|
36
|
-
let(:graph) do
|
|
37
|
-
graph = described_class.new(id: "dep_graph", name: "Dependency Graph")
|
|
38
|
-
|
|
39
|
-
# Create decisions
|
|
40
|
-
decision1 = DecisionAgent::Dmn::DecisionNode.new(
|
|
41
|
-
id: "base_rate",
|
|
42
|
-
name: "Base Rate",
|
|
43
|
-
decision_logic: 0.05
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
decision2 = DecisionAgent::Dmn::DecisionNode.new(
|
|
47
|
-
id: "risk_adjustment",
|
|
48
|
-
name: "Risk Adjustment",
|
|
49
|
-
decision_logic: 0.02
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
decision3 = DecisionAgent::Dmn::DecisionNode.new(
|
|
53
|
-
id: "final_rate",
|
|
54
|
-
name: "Final Rate",
|
|
55
|
-
decision_logic: ->(context) { context["base_rate"] + context["risk_adjustment"] }
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# Add dependencies
|
|
59
|
-
decision3.add_dependency("base_rate")
|
|
60
|
-
decision3.add_dependency("risk_adjustment")
|
|
61
|
-
|
|
62
|
-
graph.add_decision(decision1)
|
|
63
|
-
graph.add_decision(decision2)
|
|
64
|
-
graph.add_decision(decision3)
|
|
65
|
-
|
|
66
|
-
graph
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "tracks decision dependencies" do
|
|
70
|
-
decision = graph.get_decision("final_rate")
|
|
71
|
-
expect(decision.information_requirements.length).to eq(2)
|
|
72
|
-
expect(decision.depends_on?("base_rate")).to be true
|
|
73
|
-
expect(decision.depends_on?("risk_adjustment")).to be true
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
it "evaluates decision with dependencies" do
|
|
77
|
-
result = graph.evaluate("final_rate", {})
|
|
78
|
-
expect(result).to eq(0.07) # 0.05 + 0.02
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
describe "topological ordering" do
|
|
83
|
-
let(:graph) do
|
|
84
|
-
graph = described_class.new(id: "topo_graph", name: "Topological Graph")
|
|
85
|
-
|
|
86
|
-
# Create a dependency chain: A -> B -> C
|
|
87
|
-
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A", decision_logic: 1)
|
|
88
|
-
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B", decision_logic: ->(ctx) { ctx["a"] + 1 })
|
|
89
|
-
decision_c = DecisionAgent::Dmn::DecisionNode.new(id: "c", name: "C", decision_logic: ->(ctx) { ctx["b"] + 1 })
|
|
90
|
-
|
|
91
|
-
decision_b.add_dependency("a")
|
|
92
|
-
decision_c.add_dependency("b")
|
|
93
|
-
|
|
94
|
-
graph.add_decision(decision_a)
|
|
95
|
-
graph.add_decision(decision_b)
|
|
96
|
-
graph.add_decision(decision_c)
|
|
97
|
-
|
|
98
|
-
graph
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "returns decisions in topological order" do
|
|
102
|
-
order = graph.topological_order
|
|
103
|
-
expect(order.index("a")).to be < order.index("b")
|
|
104
|
-
expect(order.index("b")).to be < order.index("c")
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it "identifies root decisions" do
|
|
108
|
-
roots = graph.root_decisions
|
|
109
|
-
expect(roots).to eq(["a"])
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
it "identifies leaf decisions" do
|
|
113
|
-
leaves = graph.leaf_decisions
|
|
114
|
-
expect(leaves).to eq(["c"])
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
describe "circular dependency detection" do
|
|
119
|
-
it "detects circular dependencies" do
|
|
120
|
-
graph = described_class.new(id: "circular", name: "Circular Graph")
|
|
121
|
-
|
|
122
|
-
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A")
|
|
123
|
-
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B")
|
|
124
|
-
|
|
125
|
-
decision_a.add_dependency("b")
|
|
126
|
-
decision_b.add_dependency("a")
|
|
127
|
-
|
|
128
|
-
graph.add_decision(decision_a)
|
|
129
|
-
graph.add_decision(decision_b)
|
|
130
|
-
|
|
131
|
-
expect(graph.circular_dependencies?).to be true
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
it "raises error when evaluating circular dependencies" do
|
|
135
|
-
graph = described_class.new(id: "circular", name: "Circular Graph")
|
|
136
|
-
|
|
137
|
-
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A")
|
|
138
|
-
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B")
|
|
139
|
-
|
|
140
|
-
decision_a.add_dependency("b")
|
|
141
|
-
decision_b.add_dependency("a")
|
|
142
|
-
|
|
143
|
-
graph.add_decision(decision_a)
|
|
144
|
-
graph.add_decision(decision_b)
|
|
145
|
-
|
|
146
|
-
expect { graph.topological_order }.to raise_error(DecisionAgent::Dmn::DmnError, /Circular dependency/)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
describe "complex graph evaluation" do
|
|
151
|
-
let(:graph) do
|
|
152
|
-
# Build a loan approval graph
|
|
153
|
-
# Decisions: income_check -> credit_check -> final_decision
|
|
154
|
-
graph = described_class.new(id: "loan_graph", name: "Loan Approval Graph")
|
|
155
|
-
|
|
156
|
-
income_check = DecisionAgent::Dmn::DecisionNode.new(
|
|
157
|
-
id: "income_check",
|
|
158
|
-
name: "Income Check",
|
|
159
|
-
decision_logic: ->(ctx) { ctx["income"] >= 50_000 ? "sufficient" : "insufficient" }
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
credit_check = DecisionAgent::Dmn::DecisionNode.new(
|
|
163
|
-
id: "credit_check",
|
|
164
|
-
name: "Credit Check",
|
|
165
|
-
decision_logic: ->(ctx) { ctx["credit_score"] >= 650 ? "good" : "poor" }
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
final_decision = DecisionAgent::Dmn::DecisionNode.new(
|
|
169
|
-
id: "final_decision",
|
|
170
|
-
name: "Final Decision",
|
|
171
|
-
decision_logic: lambda do |ctx|
|
|
172
|
-
if ctx["income_check"] == "sufficient" && ctx["credit_check"] == "good"
|
|
173
|
-
"Approved"
|
|
174
|
-
else
|
|
175
|
-
"Rejected"
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
final_decision.add_dependency("income_check", "income_check")
|
|
181
|
-
final_decision.add_dependency("credit_check", "credit_check")
|
|
182
|
-
|
|
183
|
-
graph.add_decision(income_check)
|
|
184
|
-
graph.add_decision(credit_check)
|
|
185
|
-
graph.add_decision(final_decision)
|
|
186
|
-
|
|
187
|
-
graph
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
it "evaluates graph with all dependencies for approved case" do
|
|
191
|
-
context = { income: 60_000, credit_score: 700 }
|
|
192
|
-
result = graph.evaluate("final_decision", context)
|
|
193
|
-
expect(result).to eq("Approved")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
it "evaluates graph with all dependencies for rejected case" do
|
|
197
|
-
context = { income: 40_000, credit_score: 600 }
|
|
198
|
-
result = graph.evaluate("final_decision", context)
|
|
199
|
-
expect(result).to eq("Rejected")
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
it "evaluates all decisions in graph" do
|
|
203
|
-
context = { income: 60_000, credit_score: 700 }
|
|
204
|
-
results = graph.evaluate_all(context)
|
|
205
|
-
|
|
206
|
-
expect(results["income_check"]).to eq("sufficient")
|
|
207
|
-
expect(results["credit_check"]).to eq("good")
|
|
208
|
-
expect(results["final_decision"]).to eq("Approved")
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
describe "graph analysis" do
|
|
213
|
-
let(:graph) do
|
|
214
|
-
graph = described_class.new(id: "analysis", name: "Analysis Graph")
|
|
215
|
-
|
|
216
|
-
d1 = DecisionAgent::Dmn::DecisionNode.new(id: "d1", name: "D1", decision_logic: 1)
|
|
217
|
-
d2 = DecisionAgent::Dmn::DecisionNode.new(id: "d2", name: "D2", decision_logic: 2)
|
|
218
|
-
d3 = DecisionAgent::Dmn::DecisionNode.new(id: "d3", name: "D3", decision_logic: 3)
|
|
219
|
-
|
|
220
|
-
d3.add_dependency("d1")
|
|
221
|
-
d3.add_dependency("d2")
|
|
222
|
-
|
|
223
|
-
graph.add_decision(d1)
|
|
224
|
-
graph.add_decision(d2)
|
|
225
|
-
graph.add_decision(d3)
|
|
226
|
-
|
|
227
|
-
graph
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
it "exports dependency graph structure" do
|
|
231
|
-
dep_graph = graph.dependency_graph
|
|
232
|
-
expect(dep_graph["d1"]).to eq([])
|
|
233
|
-
expect(dep_graph["d2"]).to eq([])
|
|
234
|
-
expect(dep_graph["d3"]).to contain_exactly("d1", "d2")
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
it "exports graph to hash representation" do
|
|
238
|
-
hash = graph.to_h
|
|
239
|
-
expect(hash[:id]).to eq("analysis")
|
|
240
|
-
expect(hash[:name]).to eq("Analysis Graph")
|
|
241
|
-
expect(hash[:decisions].keys).to contain_exactly("d1", "d2", "d3")
|
|
242
|
-
expect(hash[:dependency_graph]).to be_a(Hash)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
describe DecisionAgent::Dmn::DecisionNode do
|
|
247
|
-
it "creates decision node with basic attributes" do
|
|
248
|
-
node = described_class.new(id: "test", name: "Test Decision")
|
|
249
|
-
expect(node.id).to eq("test")
|
|
250
|
-
expect(node.name).to eq("Test Decision")
|
|
251
|
-
expect(node.information_requirements).to be_empty
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
it "adds dependencies" do
|
|
255
|
-
node = described_class.new(id: "test", name: "Test")
|
|
256
|
-
node.add_dependency("dep1", "variable1")
|
|
257
|
-
|
|
258
|
-
expect(node.information_requirements.length).to eq(1)
|
|
259
|
-
expect(node.information_requirements.first[:decision_id]).to eq("dep1")
|
|
260
|
-
expect(node.information_requirements.first[:variable_name]).to eq("variable1")
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
it "checks if node depends on another decision" do
|
|
264
|
-
node = described_class.new(id: "test", name: "Test")
|
|
265
|
-
node.add_dependency("dep1")
|
|
266
|
-
|
|
267
|
-
expect(node.depends_on?("dep1")).to be true
|
|
268
|
-
expect(node.depends_on?("dep2")).to be false
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
it "resets evaluation state" do
|
|
272
|
-
node = described_class.new(id: "test", name: "Test")
|
|
273
|
-
node.value = "some value"
|
|
274
|
-
node.evaluated = true
|
|
275
|
-
|
|
276
|
-
node.reset!
|
|
277
|
-
|
|
278
|
-
expect(node.value).to be_nil
|
|
279
|
-
expect(node.evaluated).to be false
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "decision_agent/dmn/decision_tree"
|
|
5
|
-
|
|
6
|
-
RSpec.describe DecisionAgent::Dmn::DecisionTree do
|
|
7
|
-
describe "basic tree structure" do
|
|
8
|
-
let(:tree) do
|
|
9
|
-
described_class.new(id: "tree1", name: "Loan Approval Tree")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it "creates a tree with root node" do
|
|
13
|
-
expect(tree.root).to be_a(DecisionAgent::Dmn::TreeNode)
|
|
14
|
-
expect(tree.root.id).to eq("root")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
it "allows adding children to nodes" do
|
|
18
|
-
child1 = DecisionAgent::Dmn::TreeNode.new(id: "child1", label: "Check Age")
|
|
19
|
-
tree.root.add_child(child1)
|
|
20
|
-
|
|
21
|
-
expect(tree.root.children).to include(child1)
|
|
22
|
-
expect(child1.parent).to eq(tree.root)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe "tree evaluation" do
|
|
27
|
-
let(:tree) do
|
|
28
|
-
# Build a simple decision tree for loan approval
|
|
29
|
-
tree = described_class.new(id: "loan_tree", name: "Loan Approval")
|
|
30
|
-
|
|
31
|
-
# Root node
|
|
32
|
-
root = tree.root
|
|
33
|
-
|
|
34
|
-
# First level - check age
|
|
35
|
-
age_check = DecisionAgent::Dmn::TreeNode.new(
|
|
36
|
-
id: "age_check",
|
|
37
|
-
label: "Age >= 18?",
|
|
38
|
-
condition: "age >= 18"
|
|
39
|
-
)
|
|
40
|
-
root.add_child(age_check)
|
|
41
|
-
|
|
42
|
-
# Second level under age check - check credit score
|
|
43
|
-
good_credit = DecisionAgent::Dmn::TreeNode.new(
|
|
44
|
-
id: "good_credit",
|
|
45
|
-
label: "Credit Score >= 650?",
|
|
46
|
-
condition: "credit_score >= 650"
|
|
47
|
-
)
|
|
48
|
-
age_check.add_child(good_credit)
|
|
49
|
-
|
|
50
|
-
# Leaf nodes - decisions
|
|
51
|
-
approved = DecisionAgent::Dmn::TreeNode.new(
|
|
52
|
-
id: "approved",
|
|
53
|
-
label: "Approved",
|
|
54
|
-
decision: "Approved"
|
|
55
|
-
)
|
|
56
|
-
good_credit.add_child(approved)
|
|
57
|
-
|
|
58
|
-
rejected_credit = DecisionAgent::Dmn::TreeNode.new(
|
|
59
|
-
id: "rejected_credit",
|
|
60
|
-
label: "Rejected - Poor Credit",
|
|
61
|
-
decision: "Rejected - Poor Credit"
|
|
62
|
-
)
|
|
63
|
-
good_credit.add_child(rejected_credit)
|
|
64
|
-
|
|
65
|
-
# Rejected for age
|
|
66
|
-
rejected_age = DecisionAgent::Dmn::TreeNode.new(
|
|
67
|
-
id: "rejected_age",
|
|
68
|
-
label: "Rejected - Too Young",
|
|
69
|
-
decision: "Rejected - Too Young"
|
|
70
|
-
)
|
|
71
|
-
root.add_child(rejected_age)
|
|
72
|
-
|
|
73
|
-
tree
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
it "evaluates tree with context matching approved path" do
|
|
77
|
-
context = { age: 25, credit_score: 700 }
|
|
78
|
-
result = tree.evaluate(context)
|
|
79
|
-
expect(result).to eq("Approved")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "evaluates tree with poor credit" do
|
|
83
|
-
context = { age: 25, credit_score: 600 }
|
|
84
|
-
result = tree.evaluate(context)
|
|
85
|
-
expect(result).to eq("Rejected - Poor Credit")
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
it "evaluates tree with age too young" do
|
|
89
|
-
context = { age: 16, credit_score: 700 }
|
|
90
|
-
result = tree.evaluate(context)
|
|
91
|
-
expect(result).to eq("Rejected - Too Young")
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "returns nil when no path matches" do
|
|
95
|
-
context = {}
|
|
96
|
-
result = tree.evaluate(context)
|
|
97
|
-
expect(result).to be_nil
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
describe "tree serialization" do
|
|
102
|
-
let(:tree_hash) do
|
|
103
|
-
{
|
|
104
|
-
id: "test_tree",
|
|
105
|
-
name: "Test Tree",
|
|
106
|
-
root: {
|
|
107
|
-
id: "root",
|
|
108
|
-
label: "Root",
|
|
109
|
-
condition: nil,
|
|
110
|
-
decision: nil,
|
|
111
|
-
children: [
|
|
112
|
-
{
|
|
113
|
-
id: "node1",
|
|
114
|
-
label: "Node 1",
|
|
115
|
-
condition: "x > 5",
|
|
116
|
-
decision: nil,
|
|
117
|
-
children: [
|
|
118
|
-
{
|
|
119
|
-
id: "leaf1",
|
|
120
|
-
label: "Leaf 1",
|
|
121
|
-
condition: nil,
|
|
122
|
-
decision: "Result A",
|
|
123
|
-
children: []
|
|
124
|
-
}
|
|
125
|
-
]
|
|
126
|
-
}
|
|
127
|
-
]
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
it "converts tree to hash" do
|
|
133
|
-
tree = described_class.new(id: "tree1", name: "Tree 1")
|
|
134
|
-
node1 = DecisionAgent::Dmn::TreeNode.new(id: "node1", condition: "x > 5")
|
|
135
|
-
tree.root.add_child(node1)
|
|
136
|
-
|
|
137
|
-
hash = tree.to_h
|
|
138
|
-
expect(hash[:id]).to eq("tree1")
|
|
139
|
-
expect(hash[:name]).to eq("Tree 1")
|
|
140
|
-
expect(hash[:root][:children].length).to eq(1)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
it "builds tree from hash" do
|
|
144
|
-
tree = described_class.from_hash(tree_hash)
|
|
145
|
-
|
|
146
|
-
expect(tree.id).to eq("test_tree")
|
|
147
|
-
expect(tree.name).to eq("Test Tree")
|
|
148
|
-
expect(tree.root.children.length).to eq(1)
|
|
149
|
-
expect(tree.root.children.first.id).to eq("node1")
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
describe "tree analysis" do
|
|
154
|
-
let(:tree) do
|
|
155
|
-
tree = described_class.new(id: "analysis_tree", name: "Analysis Tree")
|
|
156
|
-
|
|
157
|
-
level1 = DecisionAgent::Dmn::TreeNode.new(id: "level1")
|
|
158
|
-
tree.root.add_child(level1)
|
|
159
|
-
|
|
160
|
-
level2a = DecisionAgent::Dmn::TreeNode.new(id: "level2a")
|
|
161
|
-
level2b = DecisionAgent::Dmn::TreeNode.new(id: "level2b")
|
|
162
|
-
level1.add_child(level2a)
|
|
163
|
-
level1.add_child(level2b)
|
|
164
|
-
|
|
165
|
-
leaf1 = DecisionAgent::Dmn::TreeNode.new(id: "leaf1", decision: "Result 1")
|
|
166
|
-
leaf2 = DecisionAgent::Dmn::TreeNode.new(id: "leaf2", decision: "Result 2")
|
|
167
|
-
level2a.add_child(leaf1)
|
|
168
|
-
level2b.add_child(leaf2)
|
|
169
|
-
|
|
170
|
-
tree
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
it "collects all leaf nodes" do
|
|
174
|
-
leaves = tree.leaf_nodes
|
|
175
|
-
expect(leaves.length).to eq(2)
|
|
176
|
-
expect(leaves.map(&:id)).to include("leaf1", "leaf2")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
it "calculates tree depth" do
|
|
180
|
-
expect(tree.depth).to eq(3) # root -> level1 -> level2 -> leaf (depth 3)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
it "collects all paths from root to leaves" do
|
|
184
|
-
paths = tree.paths
|
|
185
|
-
expect(paths.length).to eq(2)
|
|
186
|
-
expect(paths.first.length).to be >= 3
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
describe DecisionAgent::Dmn::TreeNode do
|
|
191
|
-
it "identifies leaf nodes correctly" do
|
|
192
|
-
node = DecisionAgent::Dmn::TreeNode.new(id: "test", decision: "Decision")
|
|
193
|
-
expect(node.leaf?).to be true
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
it "identifies non-leaf nodes correctly" do
|
|
197
|
-
node = DecisionAgent::Dmn::TreeNode.new(id: "test")
|
|
198
|
-
child = DecisionAgent::Dmn::TreeNode.new(id: "child")
|
|
199
|
-
node.add_child(child)
|
|
200
|
-
expect(node.leaf?).to be false
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent/dmn/errors"
|
|
3
|
-
|
|
4
|
-
RSpec.describe "FEEL Errors" do
|
|
5
|
-
describe DecisionAgent::Dmn::FeelParseError do
|
|
6
|
-
it "creates error with message only" do
|
|
7
|
-
error = DecisionAgent::Dmn::FeelParseError.new("Parse failed")
|
|
8
|
-
expect(error.message).to eq("Parse failed")
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
describe DecisionAgent::Dmn::FeelEvaluationError do
|
|
13
|
-
it "creates error with message only" do
|
|
14
|
-
error = DecisionAgent::Dmn::FeelEvaluationError.new("Evaluation failed")
|
|
15
|
-
expect(error.message).to eq("Evaluation failed")
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|