decision_agent 0.1.2 → 0.1.3

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +212 -35
  3. data/bin/decision_agent +3 -8
  4. data/lib/decision_agent/agent.rb +19 -26
  5. data/lib/decision_agent/audit/null_adapter.rb +1 -2
  6. data/lib/decision_agent/decision.rb +3 -1
  7. data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
  8. data/lib/decision_agent/dsl/rule_parser.rb +4 -6
  9. data/lib/decision_agent/dsl/schema_validator.rb +27 -31
  10. data/lib/decision_agent/errors.rb +11 -8
  11. data/lib/decision_agent/evaluation.rb +3 -1
  12. data/lib/decision_agent/evaluation_validator.rb +78 -0
  13. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
  14. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
  15. data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
  16. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
  17. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
  18. data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
  19. data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
  20. data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
  21. data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
  22. data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
  23. data/lib/decision_agent/replay/replay.rb +12 -22
  24. data/lib/decision_agent/scoring/base.rb +1 -1
  25. data/lib/decision_agent/scoring/consensus.rb +5 -5
  26. data/lib/decision_agent/scoring/weighted_average.rb +1 -1
  27. data/lib/decision_agent/version.rb +1 -1
  28. data/lib/decision_agent/versioning/activerecord_adapter.rb +69 -33
  29. data/lib/decision_agent/versioning/adapter.rb +1 -3
  30. data/lib/decision_agent/versioning/file_storage_adapter.rb +143 -35
  31. data/lib/decision_agent/versioning/version_manager.rb +4 -12
  32. data/lib/decision_agent/web/public/index.html +1 -1
  33. data/lib/decision_agent/web/server.rb +19 -24
  34. data/lib/decision_agent.rb +7 -0
  35. data/lib/generators/decision_agent/install/install_generator.rb +5 -5
  36. data/lib/generators/decision_agent/install/templates/migration.rb +17 -6
  37. data/lib/generators/decision_agent/install/templates/rule.rb +3 -3
  38. data/lib/generators/decision_agent/install/templates/rule_version.rb +13 -7
  39. data/spec/activerecord_thread_safety_spec.rb +553 -0
  40. data/spec/agent_spec.rb +13 -13
  41. data/spec/api_contract_spec.rb +16 -16
  42. data/spec/audit_adapters_spec.rb +3 -3
  43. data/spec/comprehensive_edge_cases_spec.rb +86 -86
  44. data/spec/dsl_validation_spec.rb +83 -83
  45. data/spec/edge_cases_spec.rb +23 -23
  46. data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
  47. data/spec/examples.txt +548 -0
  48. data/spec/issue_verification_spec.rb +685 -0
  49. data/spec/json_rule_evaluator_spec.rb +15 -15
  50. data/spec/monitoring/alert_manager_spec.rb +378 -0
  51. data/spec/monitoring/metrics_collector_spec.rb +281 -0
  52. data/spec/monitoring/monitored_agent_spec.rb +222 -0
  53. data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
  54. data/spec/replay_edge_cases_spec.rb +58 -58
  55. data/spec/replay_spec.rb +11 -11
  56. data/spec/rfc8785_canonicalization_spec.rb +215 -0
  57. data/spec/scoring_spec.rb +1 -1
  58. data/spec/spec_helper.rb +9 -0
  59. data/spec/thread_safety_spec.rb +482 -0
  60. data/spec/thread_safety_spec.rb.broken +878 -0
  61. data/spec/versioning_spec.rb +141 -37
  62. data/spec/web_ui_rack_spec.rb +135 -0
  63. metadata +69 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5711ffe8ff5b3de7d808292d9cda3a0d8818ff77684b2da9a51a4794546a18b5
4
- data.tar.gz: f0aba632f6da2eed6b0a07ca3c1ce73404652fbb4b57c8f812b4b9c679cbc1b4
3
+ metadata.gz: 6c8a831b21d5bb62dbb4948827cc0b4bc9bcf021b52f04a2d9141d1339287274
4
+ data.tar.gz: 97d80cab6513385dc9e9b4bddb7555da1e8629488ce358319aa0842e74cf41e1
5
5
  SHA512:
6
- metadata.gz: 99b980e8fec99a261db15b0042891675903b83cf238f14604a7222c4efa30be3153c9ce5eda2857fcd1167cd30b4b1fd33090e4ff87d770959a5ba8b5fd7013b
7
- data.tar.gz: be51208704d78c7b76b4dc956384d246e30d64ed8edcfe4d162e5ebd1221365a1e26057628634a56d62f0d04ac45c5482efb7556d614c6f28e6792c515276470
6
+ metadata.gz: dcfa8f3cc0a5931a1a163b97caf7586c17eddfee38850b52b832fa1a94d720342868c6823158597d0dafa14bf7df82b9d9b394fdffac3c74d06a556eca2b5c18
7
+ data.tar.gz: f183fc1b8c021e589571d0d9a97c3b2679fec3b529d2e73a76a4b7752a9792dcba77f4914de876c5448711416179dcbd0ba35b515fe1c4f2a74bcab27c1a873e
data/README.md CHANGED
@@ -59,7 +59,11 @@ puts result.explanations # => ["High value transaction"]
59
59
 
60
60
  ## Web UI - Visual Rule Builder
61
61
 
62
- Launch the visual rule builder for non-technical users:
62
+ The DecisionAgent Web UI provides a visual interface for building and testing rules.
63
+
64
+ ### Standalone Usage
65
+
66
+ Launch the visual rule builder:
63
67
 
64
68
  ```bash
65
69
  decision_agent web
@@ -67,45 +71,119 @@ decision_agent web
67
71
 
68
72
  Open [http://localhost:4567](http://localhost:4567) in your browser.
69
73
 
70
- <img width="1602" alt="DecisionAgent Web UI" src="https://github.com/user-attachments/assets/6ee6859c-f9f2-4f93-8bff-923986ccb1bc" />
74
+ ### Mount in Rails
71
75
 
72
- ## Documentation
76
+ Add to your `config/routes.rb`:
77
+
78
+ ```ruby
79
+ require 'decision_agent/web/server'
80
+
81
+ Rails.application.routes.draw do
82
+ # Mount DecisionAgent Web UI
83
+ mount DecisionAgent::Web::Server, at: '/decision_agent'
84
+ end
85
+ ```
86
+
87
+ Then visit `http://localhost:3000/decision_agent` in your browser.
88
+
89
+ **With Authentication:**
90
+
91
+ ```ruby
92
+ authenticate :user, ->(user) { user.admin? } do
93
+ mount DecisionAgent::Web::Server, at: '/decision_agent'
94
+ end
95
+ ```
96
+
97
+ ### Mount in Rack/Sinatra Apps
98
+
99
+ ```ruby
100
+ # config.ru
101
+ require 'decision_agent/web/server'
102
+
103
+ map '/decision_agent' do
104
+ run DecisionAgent::Web::Server
105
+ end
106
+ ```
107
+
108
+ <img width="1622" height="820" alt="Screenshot" src="https://github.com/user-attachments/assets/687e9ff6-669a-40f9-be27-085c614392d4" />
109
+
110
+ See [Web UI Rails Integration Guide](wiki/WEB_UI_RAILS_INTEGRATION.md) for detailed setup instructions.
111
+
112
+ ## Monitoring & Analytics
113
+
114
+ Real-time monitoring, metrics, and alerting for production environments.
115
+
116
+ ### Quick Start
117
+
118
+ ```ruby
119
+ require 'decision_agent/monitoring/metrics_collector'
120
+ require 'decision_agent/monitoring/dashboard_server'
121
+
122
+ # Initialize metrics collection
123
+ collector = DecisionAgent::Monitoring::MetricsCollector.new(window_size: 3600)
124
+
125
+ # Start real-time dashboard
126
+ DecisionAgent::Monitoring::DashboardServer.start!(
127
+ port: 4568,
128
+ metrics_collector: collector
129
+ )
130
+
131
+ # Record decisions
132
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
133
+ result = agent.decide(context: { amount: 1500 })
134
+ collector.record_decision(result, context, duration_ms: 25.5)
135
+ ```
136
+
137
+ Open [http://localhost:4568](http://localhost:4568) for the monitoring dashboard.
138
+
139
+ ### Features
140
+
141
+ - **Real-time Dashboard** - Live metrics with WebSocket updates
142
+ - **Prometheus Export** - Industry-standard metrics format
143
+ - **Intelligent Alerting** - Anomaly detection with customizable rules
144
+ - **Grafana Integration** - Pre-built dashboards and alert rules
145
+ - **Custom KPIs** - Track business-specific metrics
146
+ - **Thread-Safe** - Production-ready performance
147
+
148
+ ### Prometheus & Grafana
73
149
 
150
+ ```yaml
151
+ # prometheus.yml
152
+ scrape_configs:
153
+ - job_name: 'decision_agent'
154
+ static_configs:
155
+ - targets: ['localhost:4568']
156
+ metrics_path: '/metrics'
74
157
  ```
75
- 📚 DecisionAgent Documentation
76
-
77
- ├── 🚀 Getting Started
78
- │ ├── Installation (above)
79
- │ ├── Quick Start (above)
80
- │ └── Examples → examples/README.md
81
-
82
- ├── 📖 Core Documentation
83
- │ ├── Core Concepts → wiki/CORE_CONCEPTS.md
84
- │ ├── JSON Rule DSL → wiki/JSON_RULE_DSL.md
85
- │ ├── API Reference → wiki/API_CONTRACT.md
86
- │ └── Error Handling → wiki/ERROR_HANDLING.md
87
-
88
- ├── 🎯 Advanced Features
89
- │ ├── Versioning System → wiki/VERSIONING.md
90
- │ ├── Decision Replay → wiki/REPLAY.md
91
- │ ├── Advanced Usage → wiki/ADVANCED_USAGE.md
92
- │ └── Custom Components → wiki/ADVANCED_USAGE.md#custom-components
93
-
94
- ├── 🔌 Integration Guides
95
- │ ├── Rails Integration → wiki/INTEGRATION.md#rails
96
- │ ├── Redmine Plugin → wiki/INTEGRATION.md#redmine
97
- │ ├── Standalone Service → wiki/INTEGRATION.md#standalone
98
- │ └── Testing Guide → wiki/TESTING.md
99
-
100
- ├── 🎨 Web UI
101
- │ ├── User Guide → wiki/WEB_UI.md
102
- │ └── Setup Guide → wiki/WEB_UI_SETUP.md
103
-
104
- └── 📝 Reference
105
- ├── Changelog → wiki/CHANGELOG.md
106
- └── Full Wiki Index → wiki/README.md
158
+
159
+ Import the pre-built Grafana dashboard from [grafana/decision_agent_dashboard.json](grafana/decision_agent_dashboard.json).
160
+
161
+ ### Alert Management
162
+
163
+ ```ruby
164
+ alert_manager = DecisionAgent::Monitoring::AlertManager.new(
165
+ metrics_collector: collector
166
+ )
167
+
168
+ # Add alert rules
169
+ alert_manager.add_rule(
170
+ name: 'High Error Rate',
171
+ condition: AlertManager.high_error_rate(threshold: 0.1),
172
+ severity: :critical
173
+ )
174
+
175
+ # Register alert handlers
176
+ alert_manager.add_handler do |alert|
177
+ SlackNotifier.notify("🚨 #{alert[:message]}")
178
+ end
179
+
180
+ # Start monitoring
181
+ alert_manager.start_monitoring(interval: 60)
107
182
  ```
108
183
 
184
+ See [Monitoring & Analytics Guide](wiki/MONITORING_AND_ANALYTICS.md) for complete documentation.
185
+
186
+
109
187
  ## Key Features
110
188
 
111
189
  ### Decision Making
@@ -124,11 +202,21 @@ Open [http://localhost:4567](http://localhost:4567) in your browser.
124
202
  - **JSON Rule DSL** - Non-technical users can write rules
125
203
  - **Visual Rule Builder** - Web UI for rule management
126
204
 
205
+ ### Monitoring & Observability
206
+ - **Real-time Metrics** - Live dashboard with WebSocket updates (<1 second latency)
207
+ - **Prometheus Export** - Industry-standard metrics format at `/metrics` endpoint
208
+ - **Intelligent Alerting** - Anomaly detection with customizable rules and severity levels
209
+ - **Grafana Integration** - Pre-built dashboards and alert configurations in `grafana/` directory
210
+ - **Custom KPIs** - Track business-specific metrics with thread-safe operations
211
+ - **MonitoredAgent** - Drop-in replacement that auto-records all metrics
212
+ - **AlertManager** - Built-in anomaly detection (error rates, latency spikes, low confidence)
213
+
127
214
  ### Production Ready
128
215
  - **Comprehensive Testing** - 90%+ code coverage
129
216
  - **Error Handling** - Clear, actionable error messages
130
217
  - **Versioning** - Full rule version control and rollback
131
218
  - **Performance** - Fast, zero external dependencies
219
+ - **Thread-Safe** - Safe for multi-threaded servers and background jobs
132
220
 
133
221
  ## Examples
134
222
 
@@ -162,6 +250,68 @@ rules = {
162
250
 
163
251
  See [examples/](examples/) for complete working examples.
164
252
 
253
+ ## Thread-Safety Guarantees
254
+
255
+ DecisionAgent is designed to be **thread-safe and FAST** for use in multi-threaded environments:
256
+
257
+ ### Performance
258
+ - **10,000+ decisions/second** throughput
259
+ - **~0.1ms average latency** per decision
260
+ - **Zero performance overhead** from thread-safety
261
+ - **Linear scalability** with thread count
262
+
263
+ ### Safe Concurrent Usage
264
+ - **Agent instances** can be shared across threads safely
265
+ - **Evaluators** are immutable after initialization
266
+ - **Decisions and Evaluations** are deeply frozen
267
+ - **File storage** uses mutex-protected operations
268
+
269
+ ### Best Practices
270
+ ```ruby
271
+ # Safe: Reuse agent instance across threads
272
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
273
+
274
+ Thread.new { agent.decide(context: { user_id: 1 }) }
275
+ Thread.new { agent.decide(context: { user_id: 2 }) }
276
+
277
+ # Safe: Share evaluators across agent instances
278
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
279
+ agent1 = DecisionAgent::Agent.new(evaluators: [evaluator])
280
+ agent2 = DecisionAgent::Agent.new(evaluators: [evaluator])
281
+ ```
282
+
283
+ ### What's Frozen
284
+ All data structures are deeply frozen to prevent mutation:
285
+ - Decision objects (decision, confidence, explanations, evaluations)
286
+ - Evaluation objects (decision, weight, reason, metadata)
287
+ - Context data
288
+ - Rule definitions in evaluators
289
+
290
+ This ensures safe concurrent access without race conditions.
291
+
292
+ ### RFC 8785 Canonical JSON
293
+ DecisionAgent uses **RFC 8785 (JSON Canonicalization Scheme)** for deterministic audit hashing:
294
+
295
+ - **Industry Standard** - Official IETF specification for canonical JSON
296
+ - **Cryptographically Sound** - Ensures deterministic hashing of decision payloads
297
+ - **Reproducible** - Same decision always produces same audit hash
298
+ - **Interoperable** - Compatible with other systems using RFC 8785
299
+
300
+ Every decision includes a deterministic SHA-256 hash in the audit payload, enabling:
301
+ - Tamper detection in audit logs
302
+ - Exact replay verification
303
+ - Regulatory compliance documentation
304
+
305
+ Learn more: [RFC 8785 Specification](https://datatracker.ietf.org/doc/html/rfc8785)
306
+
307
+ ### Performance Benchmark
308
+ Run the included benchmark to verify zero overhead:
309
+ ```bash
310
+ ruby examples/thread_safe_performance.rb
311
+ ```
312
+
313
+ See [THREAD_SAFETY.md](wiki/THREAD_SAFETY.md) for detailed implementation guide and [PERFORMANCE_AND_THREAD_SAFETY.md](wiki/PERFORMANCE_AND_THREAD_SAFETY.md) for detailed performance analysis.
314
+
165
315
  ## When to Use DecisionAgent
166
316
 
167
317
  ✅ **Perfect for:**
@@ -176,6 +326,33 @@ See [examples/](examples/) for complete working examples.
176
326
  - Pure AI/ML with no rules
177
327
  - Single-step validations
178
328
 
329
+ ## Documentation
330
+
331
+ **Getting Started**
332
+ - [Installation](#installation)
333
+ - [Quick Start](#quick-start)
334
+ - [Examples](examples/README.md)
335
+
336
+ **Core Features**
337
+ - [Versioning System](wiki/VERSIONING.md) - Version control for rules
338
+ - [Web UI](wiki/WEB_UI.md) - Visual rule builder
339
+ - [Web UI Setup](wiki/WEB_UI_SETUP.md) - Setup guide
340
+ - [Web UI Rails Integration](wiki/WEB_UI_RAILS_INTEGRATION.md) - Mount in Rails/Rack apps
341
+ - [Monitoring & Analytics](wiki/MONITORING_AND_ANALYTICS.md) - Real-time monitoring, metrics, and alerting
342
+ - [Monitoring Architecture](wiki/MONITORING_ARCHITECTURE.md) - System architecture and design
343
+
344
+ **Performance & Thread-Safety**
345
+ - [Performance & Thread-Safety Summary](wiki/PERFORMANCE_AND_THREAD_SAFETY.md) - Benchmarks and production readiness
346
+ - [Thread-Safety Implementation](wiki/THREAD_SAFETY.md) - Technical implementation guide
347
+
348
+ **Reference**
349
+ - [API Contract](wiki/API_CONTRACT.md) - Full API reference
350
+ - [Changelog](wiki/CHANGELOG.md) - Version history
351
+
352
+ **More Resources**
353
+ - [Wiki Home](wiki/README.md) - Documentation index
354
+ - [GitHub Issues](https://github.com/samaswin87/decision_agent/issues) - Report bugs or request features
355
+
179
356
  ## Contributing
180
357
 
181
358
  1. Fork the repository
data/bin/decision_agent CHANGED
@@ -24,15 +24,13 @@ def print_help
24
24
  decision_agent version
25
25
 
26
26
  For more information, visit:
27
- https://github.com/yourusername/decision_agent
27
+ https://github.com/samaswin87/decision_agent
28
28
  HELP
29
29
  end
30
30
 
31
31
  def start_web_ui(port = 4567)
32
32
  # Ruby 4.0 compatibility: Puma expects Bundler::ORIGINAL_ENV which was removed
33
- if defined?(Bundler) && !Bundler.const_defined?(:ORIGINAL_ENV)
34
- Bundler.const_set(:ORIGINAL_ENV, ENV.to_h.dup)
35
- end
33
+ Bundler.const_set(:ORIGINAL_ENV, ENV.to_h.dup) if defined?(Bundler) && !Bundler.const_defined?(:ORIGINAL_ENV)
36
34
 
37
35
  puts "🎯 Starting DecisionAgent Rule Builder..."
38
36
  puts "📍 Server: http://localhost:#{port}"
@@ -60,19 +58,16 @@ def validate_file(filepath)
60
58
  puts " Version: #{data['version'] || data[:version]}"
61
59
  puts " Ruleset: #{data['ruleset'] || data[:ruleset]}"
62
60
  puts " Rules: #{data['rules']&.size || 0}"
63
-
64
61
  rescue JSON::ParserError => e
65
62
  puts "❌ JSON Parsing Error:"
66
63
  puts " #{e.message}"
67
64
  exit 1
68
-
69
65
  rescue DecisionAgent::InvalidRuleDslError => e
70
66
  puts "❌ Validation Failed:"
71
67
  puts ""
72
68
  puts e.message
73
69
  exit 1
74
-
75
- rescue => e
70
+ rescue StandardError => e
76
71
  puts "❌ Unexpected Error:"
77
72
  puts " #{e.message}"
78
73
  exit 1
@@ -1,5 +1,6 @@
1
1
  require "digest"
2
2
  require "json"
3
+ require "json/canonicalization"
3
4
 
4
5
  module DecisionAgent
5
6
  class Agent
@@ -11,6 +12,9 @@ module DecisionAgent
11
12
  @audit_adapter = audit_adapter || Audit::NullAdapter.new
12
13
 
13
14
  validate_configuration!
15
+
16
+ # Freeze instance variables for thread-safety
17
+ @evaluators.freeze
14
18
  end
15
19
 
16
20
  def decide(context:, feedback: {})
@@ -20,6 +24,9 @@ module DecisionAgent
20
24
 
21
25
  raise NoEvaluationsError if evaluations.empty?
22
26
 
27
+ # Validate all evaluations for correctness and thread-safety
28
+ EvaluationValidator.validate_all!(evaluations)
29
+
23
30
  scored_result = @scoring_strategy.score(evaluations)
24
31
 
25
32
  decision_value = scored_result[:decision]
@@ -51,32 +58,24 @@ module DecisionAgent
51
58
  private
52
59
 
53
60
  def validate_configuration!
54
- if @evaluators.empty?
55
- raise InvalidConfigurationError, "At least one evaluator is required"
56
- end
61
+ raise InvalidConfigurationError, "At least one evaluator is required" if @evaluators.empty?
57
62
 
58
63
  @evaluators.each do |evaluator|
59
- unless evaluator.respond_to?(:evaluate)
60
- raise InvalidEvaluatorError
61
- end
64
+ raise InvalidEvaluatorError unless evaluator.respond_to?(:evaluate)
62
65
  end
63
66
 
64
- unless @scoring_strategy.respond_to?(:score)
65
- raise InvalidScoringStrategyError
66
- end
67
+ raise InvalidScoringStrategyError unless @scoring_strategy.respond_to?(:score)
67
68
 
68
- unless @audit_adapter.respond_to?(:record)
69
- raise InvalidAuditAdapterError
70
- end
69
+ return if @audit_adapter.respond_to?(:record)
70
+
71
+ raise InvalidAuditAdapterError
71
72
  end
72
73
 
73
74
  def collect_evaluations(context, feedback)
74
75
  @evaluators.map do |evaluator|
75
- begin
76
- evaluator.evaluate(context, feedback: feedback)
77
- rescue => e
78
- nil
79
- end
76
+ evaluator.evaluate(context, feedback: feedback)
77
+ rescue StandardError
78
+ nil
80
79
  end.compact
81
80
  end
82
81
 
@@ -132,16 +131,10 @@ module DecisionAgent
132
131
  Digest::SHA256.hexdigest(canonical)
133
132
  end
134
133
 
134
+ # Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
135
+ # This is the industry standard for cryptographic hashing of JSON data
135
136
  def canonical_json(obj)
136
- case obj
137
- when Hash
138
- sorted = obj.keys.sort.map { |k| [k.to_s, canonical_json(obj[k])] }.to_h
139
- JSON.generate(sorted, quirks_mode: false)
140
- when Array
141
- JSON.generate(obj.map { |v| canonical_json(v) }, quirks_mode: false)
142
- else
143
- obj.to_s
144
- end
137
+ obj.to_json_c14n
145
138
  end
146
139
  end
147
140
  end
@@ -1,8 +1,7 @@
1
1
  module DecisionAgent
2
2
  module Audit
3
3
  class NullAdapter < Adapter
4
- def record(decision, context)
5
- end
4
+ def record(decision, context); end
6
5
  end
7
6
  end
8
7
  end
@@ -10,6 +10,8 @@ module DecisionAgent
10
10
  @explanations = Array(explanations).map(&:freeze).freeze
11
11
  @evaluations = Array(evaluations).freeze
12
12
  @audit_payload = deep_freeze(audit_payload)
13
+
14
+ freeze
13
15
  end
14
16
 
15
17
  def to_h
@@ -34,7 +36,7 @@ module DecisionAgent
34
36
 
35
37
  def validate_confidence!(confidence)
36
38
  c = confidence.to_f
37
- raise InvalidConfidenceError.new(confidence) unless c >= 0.0 && c <= 1.0
39
+ raise InvalidConfidenceError, confidence unless c.between?(0.0, 1.0)
38
40
  end
39
41
 
40
42
  def deep_freeze(obj)
@@ -21,13 +21,12 @@ module DecisionAgent
21
21
  end
22
22
  end
23
23
 
24
- private
25
-
26
24
  # Evaluates 'all' condition - returns true only if ALL sub-conditions are true
27
25
  # Empty array returns true (vacuous truth)
28
26
  def self.evaluate_all(conditions, context)
29
27
  return true if conditions.is_a?(Array) && conditions.empty?
30
28
  return false unless conditions.is_a?(Array)
29
+
31
30
  conditions.all? { |cond| evaluate(cond, context) }
32
31
  end
33
32
 
@@ -35,6 +34,7 @@ module DecisionAgent
35
34
  # Empty array returns false (no options to match)
36
35
  def self.evaluate_any(conditions, context)
37
36
  return false unless conditions.is_a?(Array)
37
+
38
38
  conditions.any? { |cond| evaluate(cond, context) }
39
39
  end
40
40
 
@@ -117,6 +117,7 @@ module DecisionAgent
117
117
  keys = key_path.to_s.split(".")
118
118
  keys.reduce(hash) do |memo, key|
119
119
  return nil unless memo.is_a?(Hash)
120
+
120
121
  memo[key] || memo[key.to_sym]
121
122
  end
122
123
  end
@@ -126,7 +127,7 @@ module DecisionAgent
126
127
  def self.comparable?(val1, val2)
127
128
  (val1.is_a?(Numeric) || val1.is_a?(String)) &&
128
129
  (val2.is_a?(Numeric) || val2.is_a?(String)) &&
129
- val1.class == val2.class
130
+ val1.instance_of?(val2.class)
130
131
  end
131
132
  end
132
133
  end
@@ -13,14 +13,12 @@ module DecisionAgent
13
13
  rescue JSON::ParserError => e
14
14
  raise InvalidRuleDslError, "Invalid JSON syntax: #{e.message}\n\n" \
15
15
  "Please ensure your JSON is properly formatted. " \
16
- "Common issues:\n" \
17
- " - Missing or extra commas\n" \
18
- " - Unquoted keys or values\n" \
19
- " - Unmatched brackets or braces"
16
+ "Common issues:\n " \
17
+ "- Missing or extra commas\n " \
18
+ "- Unquoted keys or values\n " \
19
+ "- Unmatched brackets or braces"
20
20
  end
21
21
 
22
- private
23
-
24
22
  def self.parse_json(input)
25
23
  if input.is_a?(String)
26
24
  JSON.parse(input)
@@ -31,18 +31,18 @@ module DecisionAgent
31
31
  private
32
32
 
33
33
  def validate_root_structure
34
- unless @data.is_a?(Hash)
35
- @errors << "Root element must be a hash/object, got #{@data.class}"
36
- return
37
- end
34
+ return if @data.is_a?(Hash)
35
+
36
+ @errors << "Root element must be a hash/object, got #{@data.class}"
37
+ nil
38
38
  end
39
39
 
40
40
  def validate_version
41
41
  return if @errors.any? # Skip if root structure is invalid
42
42
 
43
- unless @data.key?("version") || @data.key?(:version)
44
- @errors << "Missing required field 'version'. Example: { \"version\": \"1.0\", ... }"
45
- end
43
+ return if @data.key?("version") || @data.key?(:version)
44
+
45
+ @errors << "Missing required field 'version'. Example: { \"version\": \"1.0\", ... }"
46
46
  end
47
47
 
48
48
  def validate_rules_array
@@ -55,9 +55,9 @@ module DecisionAgent
55
55
  return
56
56
  end
57
57
 
58
- unless rules.is_a?(Array)
59
- @errors << "Field 'rules' must be an array, got #{rules.class}. Example: \"rules\": [...]"
60
- end
58
+ return if rules.is_a?(Array)
59
+
60
+ @errors << "Field 'rules' must be an array, got #{rules.class}. Example: \"rules\": [...]"
61
61
  end
62
62
 
63
63
  def validate_each_rule
@@ -93,9 +93,9 @@ module DecisionAgent
93
93
  return
94
94
  end
95
95
 
96
- unless rule_id.is_a?(String) || rule_id.is_a?(Symbol)
97
- @errors << "#{rule_path}: Field 'id' must be a string, got #{rule_id.class}"
98
- end
96
+ return if rule_id.is_a?(String) || rule_id.is_a?(Symbol)
97
+
98
+ @errors << "#{rule_path}: Field 'id' must be a string, got #{rule_id.class}"
99
99
  end
100
100
 
101
101
  def validate_if_clause(rule, rule_path)
@@ -150,9 +150,7 @@ module DecisionAgent
150
150
  value = condition["value"] || condition[:value]
151
151
 
152
152
  # Validate field
153
- unless field
154
- @errors << "#{path}: Field condition missing 'field' key"
155
- end
153
+ @errors << "#{path}: Field condition missing 'field' key" unless field
156
154
 
157
155
  # Validate operator
158
156
  unless operator
@@ -174,10 +172,10 @@ module DecisionAgent
174
172
  def validate_operator(operator, path)
175
173
  operator_str = operator.to_s
176
174
 
177
- unless SUPPORTED_OPERATORS.include?(operator_str)
178
- @errors << "#{path}: Unsupported operator '#{operator}'. " \
179
- "Supported operators: #{SUPPORTED_OPERATORS.join(', ')}"
180
- end
175
+ return if SUPPORTED_OPERATORS.include?(operator_str)
176
+
177
+ @errors << "#{path}: Unsupported operator '#{operator}'. " \
178
+ "Supported operators: #{SUPPORTED_OPERATORS.join(', ')}"
181
179
  end
182
180
 
183
181
  def validate_field_path(field, path)
@@ -191,11 +189,11 @@ module DecisionAgent
191
189
  # Validate dot-notation
192
190
  parts = field.split(".")
193
191
 
194
- if parts.any?(&:empty?)
195
- @errors << "#{path}: Invalid field path '#{field}'. " \
196
- "Dot-notation paths cannot have empty segments. " \
197
- "Example: 'user.profile.role'"
198
- end
192
+ return unless parts.any?(&:empty?)
193
+
194
+ @errors << "#{path}: Invalid field path '#{field}'. " \
195
+ "Dot-notation paths cannot have empty segments. " \
196
+ "Example: 'user.profile.role'"
199
197
  end
200
198
 
201
199
  def validate_all_condition(condition, path)
@@ -241,9 +239,7 @@ module DecisionAgent
241
239
  # Validate decision
242
240
  decision = then_clause["decision"] || then_clause[:decision]
243
241
 
244
- unless decision
245
- @errors << "#{rule_path}.then: Missing required field 'decision'"
246
- end
242
+ @errors << "#{rule_path}.then: Missing required field 'decision'" unless decision
247
243
 
248
244
  # Validate optional weight
249
245
  weight = then_clause["weight"] || then_clause[:weight]
@@ -257,9 +253,9 @@ module DecisionAgent
257
253
  # Validate optional reason
258
254
  reason = then_clause["reason"] || then_clause[:reason]
259
255
 
260
- if reason && !reason.is_a?(String)
261
- @errors << "#{rule_path}.then.reason: Must be a string, got #{reason.class}"
262
- end
256
+ return unless reason && !reason.is_a?(String)
257
+
258
+ @errors << "#{rule_path}.then.reason: Must be a string, got #{reason.class}"
263
259
  end
264
260
 
265
261
  def format_errors