decision_agent 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +1060 -0
  4. data/bin/decision_agent +104 -0
  5. data/lib/decision_agent/agent.rb +147 -0
  6. data/lib/decision_agent/audit/adapter.rb +9 -0
  7. data/lib/decision_agent/audit/logger_adapter.rb +27 -0
  8. data/lib/decision_agent/audit/null_adapter.rb +8 -0
  9. data/lib/decision_agent/context.rb +42 -0
  10. data/lib/decision_agent/decision.rb +51 -0
  11. data/lib/decision_agent/dsl/condition_evaluator.rb +133 -0
  12. data/lib/decision_agent/dsl/rule_parser.rb +36 -0
  13. data/lib/decision_agent/dsl/schema_validator.rb +275 -0
  14. data/lib/decision_agent/errors.rb +62 -0
  15. data/lib/decision_agent/evaluation.rb +52 -0
  16. data/lib/decision_agent/evaluators/base.rb +15 -0
  17. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +51 -0
  18. data/lib/decision_agent/evaluators/static_evaluator.rb +31 -0
  19. data/lib/decision_agent/replay/replay.rb +147 -0
  20. data/lib/decision_agent/scoring/base.rb +19 -0
  21. data/lib/decision_agent/scoring/consensus.rb +40 -0
  22. data/lib/decision_agent/scoring/max_weight.rb +16 -0
  23. data/lib/decision_agent/scoring/threshold.rb +40 -0
  24. data/lib/decision_agent/scoring/weighted_average.rb +26 -0
  25. data/lib/decision_agent/version.rb +3 -0
  26. data/lib/decision_agent/web/public/app.js +580 -0
  27. data/lib/decision_agent/web/public/index.html +190 -0
  28. data/lib/decision_agent/web/public/styles.css +558 -0
  29. data/lib/decision_agent/web/server.rb +255 -0
  30. data/lib/decision_agent.rb +29 -0
  31. data/spec/agent_spec.rb +249 -0
  32. data/spec/api_contract_spec.rb +430 -0
  33. data/spec/audit_adapters_spec.rb +74 -0
  34. data/spec/comprehensive_edge_cases_spec.rb +1777 -0
  35. data/spec/context_spec.rb +84 -0
  36. data/spec/dsl_validation_spec.rb +648 -0
  37. data/spec/edge_cases_spec.rb +353 -0
  38. data/spec/examples/feedback_aware_evaluator_spec.rb +460 -0
  39. data/spec/json_rule_evaluator_spec.rb +587 -0
  40. data/spec/replay_edge_cases_spec.rb +699 -0
  41. data/spec/replay_spec.rb +210 -0
  42. data/spec/scoring_spec.rb +225 -0
  43. data/spec/spec_helper.rb +28 -0
  44. metadata +133 -0
@@ -0,0 +1,353 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "Edge Cases" do
4
+ describe "missing fields in context" do
5
+ it "handles missing fields gracefully in rule evaluation" do
6
+ rules = {
7
+ version: "1.0",
8
+ ruleset: "test",
9
+ rules: [
10
+ {
11
+ id: "rule_1",
12
+ if: { field: "missing_field", op: "eq", value: "value" },
13
+ then: { decision: "approve" }
14
+ }
15
+ ]
16
+ }
17
+
18
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
19
+ context = DecisionAgent::Context.new({})
20
+
21
+ evaluation = evaluator.evaluate(context)
22
+
23
+ expect(evaluation).to be_nil
24
+ end
25
+
26
+ it "handles nil values in comparisons" do
27
+ rules = {
28
+ version: "1.0",
29
+ ruleset: "test",
30
+ rules: [
31
+ {
32
+ id: "rule_1",
33
+ if: { field: "value", op: "gt", value: 10 },
34
+ then: { decision: "approve" }
35
+ }
36
+ ]
37
+ }
38
+
39
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
40
+ context = DecisionAgent::Context.new({ value: nil })
41
+
42
+ evaluation = evaluator.evaluate(context)
43
+
44
+ expect(evaluation).to be_nil
45
+ end
46
+ end
47
+
48
+ describe "confidence edge cases" do
49
+ it "raises error when confidence exceeds 1.0" do
50
+ expect {
51
+ DecisionAgent::Decision.new(
52
+ decision: "test",
53
+ confidence: 1.5,
54
+ explanations: [],
55
+ evaluations: [],
56
+ audit_payload: {}
57
+ )
58
+ }.to raise_error(DecisionAgent::InvalidConfidenceError)
59
+ end
60
+
61
+ it "raises error when confidence is negative" do
62
+ expect {
63
+ DecisionAgent::Decision.new(
64
+ decision: "test",
65
+ confidence: -0.1,
66
+ explanations: [],
67
+ evaluations: [],
68
+ audit_payload: {}
69
+ )
70
+ }.to raise_error(DecisionAgent::InvalidConfidenceError)
71
+ end
72
+
73
+ it "accepts confidence at boundary values" do
74
+ decision0 = DecisionAgent::Decision.new(
75
+ decision: "test",
76
+ confidence: 0.0,
77
+ explanations: [],
78
+ evaluations: [],
79
+ audit_payload: {}
80
+ )
81
+
82
+ decision1 = DecisionAgent::Decision.new(
83
+ decision: "test",
84
+ confidence: 1.0,
85
+ explanations: [],
86
+ evaluations: [],
87
+ audit_payload: {}
88
+ )
89
+
90
+ expect(decision0.confidence).to eq(0.0)
91
+ expect(decision1.confidence).to eq(1.0)
92
+ end
93
+ end
94
+
95
+ describe "weight edge cases" do
96
+ it "raises error when weight exceeds 1.0" do
97
+ expect {
98
+ DecisionAgent::Evaluation.new(
99
+ decision: "test",
100
+ weight: 1.5,
101
+ reason: "test",
102
+ evaluator_name: "test"
103
+ )
104
+ }.to raise_error(DecisionAgent::InvalidWeightError)
105
+ end
106
+
107
+ it "raises error when weight is negative" do
108
+ expect {
109
+ DecisionAgent::Evaluation.new(
110
+ decision: "test",
111
+ weight: -0.1,
112
+ reason: "test",
113
+ evaluator_name: "test"
114
+ )
115
+ }.to raise_error(DecisionAgent::InvalidWeightError)
116
+ end
117
+
118
+ it "accepts weight at boundary values" do
119
+ eval0 = DecisionAgent::Evaluation.new(
120
+ decision: "test",
121
+ weight: 0.0,
122
+ reason: "test",
123
+ evaluator_name: "test"
124
+ )
125
+
126
+ eval1 = DecisionAgent::Evaluation.new(
127
+ decision: "test",
128
+ weight: 1.0,
129
+ reason: "test",
130
+ evaluator_name: "test"
131
+ )
132
+
133
+ expect(eval0.weight).to eq(0.0)
134
+ expect(eval1.weight).to eq(1.0)
135
+ end
136
+ end
137
+
138
+ describe "empty arrays and collections" do
139
+ it "handles rules with empty 'all' conditions" do
140
+ rules = {
141
+ version: "1.0",
142
+ ruleset: "test",
143
+ rules: [
144
+ {
145
+ id: "rule_1",
146
+ if: { all: [] },
147
+ then: { decision: "approve" }
148
+ }
149
+ ]
150
+ }
151
+
152
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
153
+ context = DecisionAgent::Context.new({})
154
+
155
+ evaluation = evaluator.evaluate(context)
156
+
157
+ expect(evaluation).not_to be_nil
158
+ end
159
+
160
+ it "handles rules with empty 'any' conditions" do
161
+ rules = {
162
+ version: "1.0",
163
+ ruleset: "test",
164
+ rules: [
165
+ {
166
+ id: "rule_1",
167
+ if: { any: [] },
168
+ then: { decision: "approve" }
169
+ }
170
+ ]
171
+ }
172
+
173
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
174
+ context = DecisionAgent::Context.new({})
175
+
176
+ evaluation = evaluator.evaluate(context)
177
+
178
+ expect(evaluation).to be_nil
179
+ end
180
+ end
181
+
182
+ describe "type mismatches in comparisons" do
183
+ it "handles type mismatches in numeric comparisons" do
184
+ rules = {
185
+ version: "1.0",
186
+ ruleset: "test",
187
+ rules: [
188
+ {
189
+ id: "rule_1",
190
+ if: { field: "value", op: "gt", value: 10 },
191
+ then: { decision: "approve" }
192
+ }
193
+ ]
194
+ }
195
+
196
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
197
+ context = DecisionAgent::Context.new({ value: "not_a_number" })
198
+
199
+ evaluation = evaluator.evaluate(context)
200
+
201
+ expect(evaluation).to be_nil
202
+ end
203
+ end
204
+
205
+ describe "immutability" do
206
+ it "freezes context data to prevent modification" do
207
+ context = DecisionAgent::Context.new({ user: "alice" })
208
+
209
+ expect {
210
+ context.to_h[:user] = "bob"
211
+ }.to raise_error(FrozenError)
212
+ end
213
+
214
+ it "freezes evaluation fields" do
215
+ evaluation = DecisionAgent::Evaluation.new(
216
+ decision: "approve",
217
+ weight: 0.8,
218
+ reason: "test",
219
+ evaluator_name: "test"
220
+ )
221
+
222
+ expect(evaluation.decision).to be_frozen
223
+ expect(evaluation.reason).to be_frozen
224
+ expect(evaluation.evaluator_name).to be_frozen
225
+ end
226
+
227
+ it "freezes decision fields" do
228
+ decision = DecisionAgent::Decision.new(
229
+ decision: "approve",
230
+ confidence: 0.8,
231
+ explanations: ["test"],
232
+ evaluations: [],
233
+ audit_payload: {}
234
+ )
235
+
236
+ expect(decision.decision).to be_frozen
237
+ expect(decision.explanations).to be_frozen
238
+ end
239
+ end
240
+
241
+ describe "special characters and unicode" do
242
+ it "handles unicode in context values" do
243
+ context = DecisionAgent::Context.new({ user: "用户", message: "Hello 世界" })
244
+
245
+ evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
246
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
247
+
248
+ result = agent.decide(context: context)
249
+
250
+ expect(result.audit_payload[:context][:user]).to eq("用户")
251
+ end
252
+
253
+ it "handles special characters in rule values" do
254
+ rules = {
255
+ version: "1.0",
256
+ ruleset: "test",
257
+ rules: [
258
+ {
259
+ id: "rule_1",
260
+ if: { field: "symbol", op: "eq", value: "@#$%^&*()" },
261
+ then: { decision: "special" }
262
+ }
263
+ ]
264
+ }
265
+
266
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
267
+ context = DecisionAgent::Context.new({ symbol: "@#$%^&*()" })
268
+
269
+ evaluation = evaluator.evaluate(context)
270
+
271
+ expect(evaluation).not_to be_nil
272
+ expect(evaluation.decision).to eq("special")
273
+ end
274
+ end
275
+
276
+ describe "very large numbers and values" do
277
+ it "handles large numeric values in comparisons" do
278
+ rules = {
279
+ version: "1.0",
280
+ ruleset: "test",
281
+ rules: [
282
+ {
283
+ id: "rule_1",
284
+ if: { field: "amount", op: "gte", value: 1_000_000_000 },
285
+ then: { decision: "large_amount" }
286
+ }
287
+ ]
288
+ }
289
+
290
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
291
+ context = DecisionAgent::Context.new({ amount: 5_000_000_000 })
292
+
293
+ evaluation = evaluator.evaluate(context)
294
+
295
+ expect(evaluation).not_to be_nil
296
+ expect(evaluation.decision).to eq("large_amount")
297
+ end
298
+ end
299
+
300
+ describe "deeply nested context" do
301
+ it "handles deeply nested field access" do
302
+ rules = {
303
+ version: "1.0",
304
+ ruleset: "test",
305
+ rules: [
306
+ {
307
+ id: "rule_1",
308
+ if: { field: "a.b.c.d.e", op: "eq", value: "deep" },
309
+ then: { decision: "found_deep" }
310
+ }
311
+ ]
312
+ }
313
+
314
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
315
+ context = DecisionAgent::Context.new({
316
+ a: {
317
+ b: {
318
+ c: {
319
+ d: {
320
+ e: "deep"
321
+ }
322
+ }
323
+ }
324
+ }
325
+ })
326
+
327
+ evaluation = evaluator.evaluate(context)
328
+
329
+ expect(evaluation).not_to be_nil
330
+ expect(evaluation.decision).to eq("found_deep")
331
+ end
332
+ end
333
+
334
+ describe "audit adapter errors" do
335
+ it "propagates errors from audit adapter" do
336
+ failing_adapter = Class.new(DecisionAgent::Audit::Adapter) do
337
+ def record(decision, context)
338
+ raise StandardError, "Audit failed"
339
+ end
340
+ end
341
+
342
+ evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
343
+ agent = DecisionAgent::Agent.new(
344
+ evaluators: [evaluator],
345
+ audit_adapter: failing_adapter.new
346
+ )
347
+
348
+ expect {
349
+ agent.decide(context: {})
350
+ }.to raise_error(StandardError, "Audit failed")
351
+ end
352
+ end
353
+ end