decision_agent 0.2.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -1,165 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe DecisionAgent::EvaluationValidator do
4
- let(:valid_evaluation) do
5
- DecisionAgent::Evaluation.new(
6
- decision: "approve",
7
- weight: 0.8,
8
- reason: "Valid reason",
9
- evaluator_name: "TestEvaluator"
10
- )
11
- end
12
-
13
- describe ".validate!" do
14
- it "validates a valid evaluation" do
15
- expect do
16
- described_class.validate!(valid_evaluation)
17
- end.not_to raise_error
18
- end
19
-
20
- it "raises error for nil evaluation" do
21
- expect do
22
- described_class.validate!(nil)
23
- end.to raise_error(described_class::ValidationError, /cannot be nil/)
24
- end
25
-
26
- it "raises error for non-Evaluation object" do
27
- expect do
28
- described_class.validate!("not an evaluation")
29
- end.to raise_error(described_class::ValidationError, /must be an Evaluation instance/)
30
- end
31
-
32
- it "validates multiple valid evaluations" do
33
- eval1 = DecisionAgent::Evaluation.new(
34
- decision: "approve",
35
- weight: 0.8,
36
- reason: "Reason 1",
37
- evaluator_name: "Eval1"
38
- )
39
-
40
- eval2 = DecisionAgent::Evaluation.new(
41
- decision: "reject",
42
- weight: 0.9,
43
- reason: "Reason 2",
44
- evaluator_name: "Eval2"
45
- )
46
-
47
- expect do
48
- described_class.validate!(eval1)
49
- described_class.validate!(eval2)
50
- end.not_to raise_error
51
- end
52
- end
53
-
54
- describe ".validate_all!" do
55
- it "validates an array of valid evaluations" do
56
- evaluations = [
57
- valid_evaluation,
58
- DecisionAgent::Evaluation.new(
59
- decision: "reject",
60
- weight: 0.9,
61
- reason: "Another reason",
62
- evaluator_name: "OtherEvaluator"
63
- )
64
- ]
65
-
66
- expect do
67
- described_class.validate_all!(evaluations)
68
- end.not_to raise_error
69
- end
70
-
71
- it "raises error for non-array input" do
72
- expect do
73
- described_class.validate_all!("not an array")
74
- end.to raise_error(described_class::ValidationError, /must be an Array/)
75
- end
76
-
77
- it "raises error for empty array" do
78
- expect do
79
- described_class.validate_all!([])
80
- end.to raise_error(described_class::ValidationError, /cannot be empty/)
81
- end
82
-
83
- it "validates all evaluations in array" do
84
- eval1 = DecisionAgent::Evaluation.new(
85
- decision: "approve",
86
- weight: 0.8,
87
- reason: "Reason 1",
88
- evaluator_name: "Eval1"
89
- )
90
-
91
- eval2 = DecisionAgent::Evaluation.new(
92
- decision: "reject",
93
- weight: 0.9,
94
- reason: "Reason 2",
95
- evaluator_name: "Eval2"
96
- )
97
-
98
- expect do
99
- described_class.validate_all!([eval1, eval2])
100
- end.not_to raise_error
101
- end
102
-
103
- it "includes index in error message for invalid evaluation" do
104
- evaluations = [
105
- valid_evaluation,
106
- nil # Invalid evaluation
107
- ]
108
-
109
- expect do
110
- described_class.validate_all!(evaluations)
111
- end.to raise_error(described_class::ValidationError, /index 1/)
112
- end
113
- end
114
-
115
- describe "optimized frozen validation" do
116
- it "uses fast path for frozen evaluations" do
117
- # Evaluations are always frozen in their initializer
118
- evaluation = DecisionAgent::Evaluation.new(
119
- decision: "approve",
120
- weight: 0.8,
121
- reason: "Test reason",
122
- evaluator_name: "TestEvaluator"
123
- )
124
-
125
- expect(evaluation).to be_frozen
126
- expect do
127
- described_class.validate!(evaluation)
128
- end.not_to raise_error
129
- end
130
-
131
- it "skips nested frozen checks when evaluation is frozen" do
132
- # Since evaluations are always frozen in initializer,
133
- # the optimized validator should skip checking nested structures
134
- evaluation = DecisionAgent::Evaluation.new(
135
- decision: "approve",
136
- weight: 0.8,
137
- reason: "Test reason",
138
- evaluator_name: "TestEvaluator",
139
- metadata: { nested: { data: "value" } }
140
- )
141
-
142
- expect(evaluation).to be_frozen
143
- expect(evaluation.metadata).to be_frozen
144
- expect do
145
- described_class.validate!(evaluation)
146
- end.not_to raise_error
147
- end
148
-
149
- it "still validates unfrozen evaluations" do
150
- # Create a mock object that isn't frozen (simulating an edge case)
151
- # In practice, evaluations are always frozen in their initializer
152
- unfrozen_evaluation = double("UnfrozenEvaluation")
153
- allow(unfrozen_evaluation).to receive(:frozen?).and_return(false)
154
- allow(unfrozen_evaluation).to receive(:is_a?).with(DecisionAgent::Evaluation).and_return(true)
155
- allow(unfrozen_evaluation).to receive(:decision).and_return("approve")
156
- allow(unfrozen_evaluation).to receive(:weight).and_return(0.8)
157
- allow(unfrozen_evaluation).to receive(:reason).and_return("Test reason")
158
- allow(unfrozen_evaluation).to receive(:evaluator_name).and_return("TestEvaluator")
159
-
160
- expect do
161
- described_class.validate!(unfrozen_evaluation)
162
- end.to raise_error(described_class::ValidationError, /must be frozen/)
163
- end
164
- end
165
- end
@@ -1,460 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require_relative "../../examples/feedback_aware_evaluator"
5
-
6
- RSpec.describe Examples::FeedbackAwareEvaluator do
7
- describe "initialization" do
8
- it "creates an evaluator with base decision and weight" do
9
- evaluator = described_class.new(base_decision: "approve", base_weight: 0.8)
10
- expect(evaluator).to be_a(DecisionAgent::Evaluators::Base)
11
- end
12
-
13
- it "uses default weight of 0.5 when not specified" do
14
- evaluator = described_class.new(base_decision: "approve")
15
- context = DecisionAgent::Context.new({})
16
- result = evaluator.evaluate(context, feedback: {})
17
- expect(result.weight).to eq(0.5)
18
- end
19
- end
20
-
21
- describe "#evaluate with no feedback" do
22
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
23
- let(:context) { DecisionAgent::Context.new({ user: "alice" }) }
24
-
25
- it "returns base decision with base weight" do
26
- result = evaluator.evaluate(context, feedback: {})
27
-
28
- expect(result.decision).to eq("approve")
29
- expect(result.weight).to eq(0.8)
30
- expect(result.reason).to include("Base decision")
31
- end
32
-
33
- it "includes metadata about feedback presence" do
34
- result = evaluator.evaluate(context, feedback: {})
35
-
36
- expect(result.metadata[:base_weight]).to eq(0.8)
37
- expect(result.metadata[:feedback_present]).to be false
38
- expect(result.metadata[:feedback_keys]).to eq([])
39
- end
40
-
41
- it "tracks feedback keys when feedback is provided" do
42
- result = evaluator.evaluate(context, feedback: { custom_key: "value" })
43
-
44
- expect(result.metadata[:feedback_present]).to be true
45
- expect(result.metadata[:feedback_keys]).to include(:custom_key)
46
- end
47
- end
48
-
49
- describe "#evaluate with override feedback" do
50
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
51
- let(:context) { DecisionAgent::Context.new({ user: "bob" }) }
52
-
53
- it "overrides the base decision" do
54
- result = evaluator.evaluate(
55
- context,
56
- feedback: { override: "reject" }
57
- )
58
-
59
- expect(result.decision).to eq("reject")
60
- expect(result.decision).not_to eq("approve")
61
- end
62
-
63
- it "uses high confidence weight for manual override" do
64
- result = evaluator.evaluate(
65
- context,
66
- feedback: { override: "reject" }
67
- )
68
-
69
- expect(result.weight).to eq(0.9)
70
- end
71
-
72
- it "uses provided reason from feedback" do
73
- result = evaluator.evaluate(
74
- context,
75
- feedback: { override: "reject", reason: "Fraud detected" }
76
- )
77
-
78
- expect(result.reason).to eq("Fraud detected")
79
- end
80
-
81
- it "uses default reason when none provided" do
82
- result = evaluator.evaluate(
83
- context,
84
- feedback: { override: "reject" }
85
- )
86
-
87
- expect(result.reason).to eq("Manual override from feedback")
88
- end
89
-
90
- it "includes override metadata" do
91
- result = evaluator.evaluate(
92
- context,
93
- feedback: { override: "manual_review", reason: "Complex case" }
94
- )
95
-
96
- expect(result.metadata[:feedback_override]).to be true
97
- expect(result.metadata[:original_decision]).to eq("approve")
98
- expect(result.metadata[:override_timestamp]).to be_a(String)
99
- end
100
-
101
- it "takes precedence over other feedback types" do
102
- result = evaluator.evaluate(
103
- context,
104
- feedback: {
105
- override: "escalate",
106
- past_accuracy: 0.95,
107
- source: "expert_review"
108
- }
109
- )
110
-
111
- # Override should take precedence
112
- expect(result.decision).to eq("escalate")
113
- expect(result.weight).to eq(0.9)
114
- end
115
- end
116
-
117
- describe "#evaluate with past_accuracy feedback" do
118
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
119
- let(:context) { DecisionAgent::Context.new({ user: "charlie" }) }
120
-
121
- it "adjusts weight based on past accuracy" do
122
- result = evaluator.evaluate(
123
- context,
124
- feedback: { past_accuracy: 0.5 }
125
- )
126
-
127
- # 0.8 * 0.5 = 0.4
128
- expect(result.weight).to eq(0.4)
129
- expect(result.decision).to eq("approve")
130
- end
131
-
132
- it "boosts weight with high accuracy" do
133
- result = evaluator.evaluate(
134
- context,
135
- feedback: { past_accuracy: 1.0 }
136
- )
137
-
138
- # 0.8 * 1.0 = 0.8
139
- expect(result.weight).to eq(0.8)
140
- end
141
-
142
- it "reduces weight with low accuracy" do
143
- result = evaluator.evaluate(
144
- context,
145
- feedback: { past_accuracy: 0.25 }
146
- )
147
-
148
- # 0.8 * 0.25 = 0.2
149
- expect(result.weight).to eq(0.2)
150
- end
151
-
152
- it "clamps weight to minimum 0.0" do
153
- result = evaluator.evaluate(
154
- context,
155
- feedback: { past_accuracy: -1.0 } # Invalid, but should be handled
156
- )
157
-
158
- expect(result.weight).to be >= 0.0
159
- end
160
-
161
- it "clamps weight to maximum 1.0" do
162
- result = evaluator.evaluate(
163
- context,
164
- feedback: { past_accuracy: 2.0 } # Would produce 1.6, should clamp to 1.0
165
- )
166
-
167
- expect(result.weight).to be <= 1.0
168
- end
169
-
170
- it "includes adjustment metadata" do
171
- result = evaluator.evaluate(
172
- context,
173
- feedback: { past_accuracy: 0.95 }
174
- )
175
-
176
- expect(result.metadata[:base_weight]).to eq(0.8)
177
- expect(result.metadata[:adjusted_weight]).to eq(0.76)
178
- expect(result.metadata[:past_accuracy]).to eq(0.95)
179
- expect(result.metadata[:adjustment_factor]).to eq(0.95)
180
- end
181
-
182
- it "provides descriptive reason with accuracy percentage" do
183
- result = evaluator.evaluate(
184
- context,
185
- feedback: { past_accuracy: 0.95 }
186
- )
187
-
188
- expect(result.reason).to include("95%")
189
- expect(result.reason).to include("past accuracy")
190
- end
191
- end
192
-
193
- describe "#evaluate with source feedback" do
194
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
195
- let(:context) { DecisionAgent::Context.new({ user: "dave" }) }
196
-
197
- it "reduces weight for user_override source" do
198
- result = evaluator.evaluate(
199
- context,
200
- feedback: { source: "user_override" }
201
- )
202
-
203
- # 0.8 * 0.5 = 0.4
204
- expect(result.weight).to eq(0.4)
205
- expect(result.metadata[:multiplier]).to eq(0.5)
206
- end
207
-
208
- it "maintains weight for automated_test source" do
209
- result = evaluator.evaluate(
210
- context,
211
- feedback: { source: "automated_test" }
212
- )
213
-
214
- # 0.8 * 1.0 = 0.8
215
- expect(result.weight).to eq(0.8)
216
- expect(result.metadata[:multiplier]).to eq(1.0)
217
- end
218
-
219
- it "boosts weight for peer_review source" do
220
- result = evaluator.evaluate(
221
- context,
222
- feedback: { source: "peer_review" }
223
- )
224
-
225
- # 0.8 * 1.1 = 0.88
226
- expect(result.weight).to be_within(0.0001).of(0.88)
227
- expect(result.metadata[:multiplier]).to eq(1.1)
228
- end
229
-
230
- it "boosts weight significantly for expert_review source" do
231
- result = evaluator.evaluate(
232
- context,
233
- feedback: { source: "expert_review" }
234
- )
235
-
236
- # 0.8 * 1.2 = 0.96
237
- expect(result.weight).to eq(0.96)
238
- expect(result.metadata[:multiplier]).to eq(1.2)
239
- end
240
-
241
- it "uses default multiplier for unknown source" do
242
- result = evaluator.evaluate(
243
- context,
244
- feedback: { source: "unknown_source" }
245
- )
246
-
247
- # 0.8 * 1.0 = 0.8 (default multiplier)
248
- expect(result.weight).to eq(0.8)
249
- expect(result.metadata[:multiplier]).to eq(1.0)
250
- end
251
-
252
- it "clamps boosted weight to 1.0" do
253
- high_weight_evaluator = described_class.new(base_decision: "approve", base_weight: 0.9)
254
-
255
- result = high_weight_evaluator.evaluate(
256
- context,
257
- feedback: { source: "expert_review" } # 0.9 * 1.2 = 1.08, should clamp to 1.0
258
- )
259
-
260
- expect(result.weight).to eq(1.0)
261
- end
262
-
263
- it "includes source metadata" do
264
- result = evaluator.evaluate(
265
- context,
266
- feedback: { source: "peer_review" }
267
- )
268
-
269
- expect(result.metadata[:feedback_source]).to eq("peer_review")
270
- expect(result.metadata[:base_weight]).to eq(0.8)
271
- expect(result.metadata[:adjusted_weight]).to be_within(0.0001).of(0.88)
272
- end
273
-
274
- it "provides descriptive reason with source" do
275
- result = evaluator.evaluate(
276
- context,
277
- feedback: { source: "expert_review" }
278
- )
279
-
280
- expect(result.reason).to include("expert_review")
281
- expect(result.reason).to include("feedback source")
282
- end
283
- end
284
-
285
- describe "feedback priority" do
286
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
287
- let(:context) { DecisionAgent::Context.new({}) }
288
-
289
- it "override takes precedence over past_accuracy" do
290
- result = evaluator.evaluate(
291
- context,
292
- feedback: { override: "reject", past_accuracy: 0.95 }
293
- )
294
-
295
- expect(result.decision).to eq("reject")
296
- expect(result.weight).to eq(0.9) # Override weight, not adjusted weight
297
- end
298
-
299
- it "override takes precedence over source" do
300
- result = evaluator.evaluate(
301
- context,
302
- feedback: { override: "escalate", source: "expert_review" }
303
- )
304
-
305
- expect(result.decision).to eq("escalate")
306
- expect(result.weight).to eq(0.9) # Override weight, not source-adjusted weight
307
- end
308
-
309
- it "past_accuracy takes precedence over source" do
310
- result = evaluator.evaluate(
311
- context,
312
- feedback: { past_accuracy: 0.5, source: "expert_review" }
313
- )
314
-
315
- # Should use past_accuracy adjustment (0.8 * 0.5 = 0.4)
316
- # Not source adjustment (0.8 * 1.2 = 0.96)
317
- expect(result.weight).to eq(0.4)
318
- expect(result.metadata[:past_accuracy]).to eq(0.5)
319
- end
320
- end
321
-
322
- describe "integration with DecisionAgent::Agent" do
323
- it "works as an evaluator in a full decision agent" do
324
- evaluator = described_class.new(base_decision: "approve", base_weight: 0.8)
325
- agent = DecisionAgent::Agent.new(evaluators: [evaluator])
326
-
327
- result = agent.decide(
328
- context: { user_id: 123 },
329
- feedback: {}
330
- )
331
-
332
- expect(result.decision).to eq("approve")
333
- expect(result.confidence).to eq(1.0) # Single evaluator normalized to 1.0
334
- end
335
-
336
- it "respects feedback in agent context" do
337
- evaluator = described_class.new(base_decision: "approve", base_weight: 0.8)
338
- agent = DecisionAgent::Agent.new(evaluators: [evaluator])
339
-
340
- result = agent.decide(
341
- context: { user_id: 456 },
342
- feedback: { override: "manual_review", reason: "Sensitive action" }
343
- )
344
-
345
- expect(result.decision).to eq("manual_review")
346
- expect(result.evaluations.first.reason).to eq("Sensitive action")
347
- end
348
-
349
- it "combines with other evaluators" do
350
- feedback_eval = described_class.new(base_decision: "approve", base_weight: 0.8)
351
- static_eval = DecisionAgent::Evaluators::StaticEvaluator.new(
352
- decision: "approve",
353
- weight: 0.6
354
- )
355
-
356
- agent = DecisionAgent::Agent.new(evaluators: [feedback_eval, static_eval])
357
-
358
- result = agent.decide(
359
- context: {},
360
- feedback: { past_accuracy: 0.5 } # Reduces feedback_eval weight to 0.4
361
- )
362
-
363
- expect(result.decision).to eq("approve")
364
- expect(result.evaluations.size).to eq(2)
365
-
366
- # Confidence = (0.4 + 0.6) / (0.4 + 0.6) = 1.0 (both agree)
367
- expect(result.confidence).to eq(1.0)
368
- end
369
-
370
- it "feedback affects only feedback-aware evaluators" do
371
- feedback_eval = described_class.new(base_decision: "approve", base_weight: 0.8)
372
- static_eval = DecisionAgent::Evaluators::StaticEvaluator.new(
373
- decision: "reject",
374
- weight: 0.9
375
- )
376
-
377
- agent = DecisionAgent::Agent.new(
378
- evaluators: [feedback_eval, static_eval],
379
- scoring_strategy: DecisionAgent::Scoring::MaxWeight.new
380
- )
381
-
382
- result_no_feedback = agent.decide(context: {}, feedback: {})
383
- # MaxWeight picks static_eval (0.9 > 0.8)
384
- expect(result_no_feedback.decision).to eq("reject")
385
-
386
- result_with_feedback = agent.decide(
387
- context: {},
388
- feedback: { past_accuracy: 0.5 } # Reduces feedback_eval to 0.4
389
- )
390
- # MaxWeight still picks static_eval (0.9 > 0.4)
391
- expect(result_with_feedback.decision).to eq("reject")
392
- end
393
- end
394
-
395
- describe "edge cases" do
396
- let(:evaluator) { described_class.new(base_decision: "approve", base_weight: 0.8) }
397
- let(:context) { DecisionAgent::Context.new({}) }
398
-
399
- it "handles feedback with string keys" do
400
- result = evaluator.evaluate(
401
- context,
402
- feedback: { "override" => "reject" } # String key instead of symbol
403
- )
404
-
405
- # Should not match because code expects symbols
406
- # Falls back to base decision
407
- expect(result.decision).to eq("approve")
408
- expect(result.weight).to eq(0.8)
409
- end
410
-
411
- it "handles nil feedback gracefully" do
412
- # Feedback defaults to {} in the signature, but test explicit nil handling
413
- result = evaluator.evaluate(context, feedback: {})
414
- expect(result.decision).to eq("approve")
415
- end
416
-
417
- it "handles empty override value" do
418
- result = evaluator.evaluate(
419
- context,
420
- feedback: { override: "" }
421
- )
422
-
423
- # Empty string is truthy in Ruby, so override applies
424
- expect(result.decision).to eq("")
425
- expect(result.metadata[:feedback_override]).to be true
426
- end
427
-
428
- it "handles zero past_accuracy" do
429
- result = evaluator.evaluate(
430
- context,
431
- feedback: { past_accuracy: 0.0 }
432
- )
433
-
434
- # 0.8 * 0.0 = 0.0
435
- expect(result.weight).to eq(0.0)
436
- end
437
-
438
- it "handles very high past_accuracy" do
439
- result = evaluator.evaluate(
440
- context,
441
- feedback: { past_accuracy: 10.0 }
442
- )
443
-
444
- # 0.8 * 10.0 = 8.0, clamped to 1.0
445
- expect(result.weight).to eq(1.0)
446
- end
447
- end
448
-
449
- describe "evaluator_name" do
450
- it "returns the correct evaluator name" do
451
- evaluator = described_class.new(base_decision: "approve", base_weight: 0.8)
452
- context = DecisionAgent::Context.new({})
453
-
454
- result = evaluator.evaluate(context, feedback: {})
455
-
456
- # Base class extracts just the class name without module
457
- expect(result.evaluator_name).to eq("FeedbackAwareEvaluator")
458
- end
459
- end
460
- end