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.
- checksums.yaml +4 -4
- data/README.md +313 -8
- data/bin/decision_agent +104 -0
- 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/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
- data/lib/decision_agent/dsl/schema_validator.rb +53 -14
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
- 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/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +52 -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 +86 -0
- data/lib/decision_agent/web/server.rb +1059 -23
- data/lib/decision_agent.rb +60 -2
- metadata +105 -61
- 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 -481
- 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 -550
- 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/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 -1633
- 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 -499
- data/spec/monitoring/monitored_agent_spec.rb +0 -222
- 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 -486
- 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 -482
- 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 -1840
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a016bb964d8daeb5676d84ba597f07af9e7b4816a8f075d3ca9feb6f1ddd2f44
|
|
4
|
+
data.tar.gz: ccc960d23a5c863b8e9be429b08188f32abe630d83e2008a3e70534401779d92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f84301ecb298b3fdc3d39b2ca02e850345d2df13eb0c8834555f1b3f08241f243dee54fd044dfd0a37bd2fa32e690dca7583c69049af68d0e951572ca222ad0
|
|
7
|
+
data.tar.gz: a52598d8c6358ea03ed16a3fd23d68fbea1ef9619f1c4dec613359427a11e9290cd6f4b9f8816fb6673d436b59bdd23b986f816a23ecac185eae1e27b86e6dee
|
data/README.md
CHANGED
|
@@ -14,8 +14,8 @@ A production-grade, deterministic, explainable, and auditable decision engine fo
|
|
|
14
14
|
DecisionAgent is designed for applications that require **deterministic, explainable, and auditable** decision-making:
|
|
15
15
|
|
|
16
16
|
- ✅ **Deterministic** - Same input always produces same output
|
|
17
|
-
- ✅ **Explainable** - Every decision includes human-readable reasoning
|
|
18
|
-
- ✅ **Auditable** - Reproduce any historical decision exactly
|
|
17
|
+
- ✅ **Explainable** - Every decision includes human-readable reasoning and machine-readable condition traces
|
|
18
|
+
- ✅ **Auditable** - Reproduce any historical decision exactly with complete explainability
|
|
19
19
|
- ✅ **Framework-agnostic** - Pure Ruby, works anywhere
|
|
20
20
|
- ✅ **Production-ready** - Comprehensive testing ([Coverage Report](coverage.md)), error handling, and versioning
|
|
21
21
|
|
|
@@ -55,9 +55,12 @@ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
|
55
55
|
# Make decision
|
|
56
56
|
result = agent.decide(context: { amount: 1500 })
|
|
57
57
|
|
|
58
|
-
puts result.decision
|
|
59
|
-
puts result.confidence
|
|
60
|
-
puts result.explanations
|
|
58
|
+
puts result.decision # => "approve"
|
|
59
|
+
puts result.confidence # => 0.9
|
|
60
|
+
puts result.explanations # => ["High value transaction"]
|
|
61
|
+
puts result.because # => ["amount > 1000"]
|
|
62
|
+
puts result.failed_conditions # => []
|
|
63
|
+
puts result.explainability # => { decision: "approve", because: [...], failed_conditions: [] }
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
|
|
@@ -69,25 +72,52 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
|
|
|
69
72
|
- **Conflict Resolution** - Weighted average, consensus, threshold, max weight
|
|
70
73
|
- **Rich Context** - Nested data, dot notation, flexible operators
|
|
71
74
|
- **Advanced Operators** - String, numeric, date/time, collection, and geospatial operators
|
|
75
|
+
- **REST API Data Enrichment** - Fetch external data during decision-making with caching and circuit breaker
|
|
72
76
|
|
|
73
77
|
### Auditability & Compliance
|
|
74
78
|
- **Complete Audit Trails** - Every decision fully logged
|
|
79
|
+
- **Explainability Layer** - Machine-readable condition traces for every decision
|
|
80
|
+
- `result.because` - Conditions that led to the decision
|
|
81
|
+
- `result.failed_conditions` - Conditions that failed
|
|
82
|
+
- `result.explainability` - Complete machine-readable explainability data
|
|
75
83
|
- **Deterministic Replay** - Reproduce historical decisions exactly
|
|
76
84
|
- **RFC 8785 Canonical JSON** - Industry-standard deterministic hashing
|
|
77
85
|
- **Compliance Ready** - HIPAA, SOX, regulatory compliance support
|
|
78
86
|
|
|
87
|
+
### Testing & Simulation
|
|
88
|
+
- **Simulation & What-If Analysis** - Test rule changes before deployment
|
|
89
|
+
- **Historical Replay / Backtesting** - Replay past decisions with new rules (CSV, JSON, database import)
|
|
90
|
+
- **What-If Analysis** - Simulate scenarios and sensitivity analysis with decision boundary visualization
|
|
91
|
+
- **Impact Analysis** - Quantify rule change effects (decision distribution, confidence shifts, performance impact)
|
|
92
|
+
- **Shadow Testing** - Compare new rules against production without affecting outcomes
|
|
93
|
+
- **Monte Carlo Simulation** - Model probabilistic inputs and understand decision outcome probabilities
|
|
94
|
+
- **Batch Testing** - Test rules against large datasets with CSV/Excel import, coverage analysis, and resume capability
|
|
95
|
+
- **A/B Testing** - Champion/Challenger testing with statistical significance analysis
|
|
96
|
+
|
|
97
|
+
### Security & Access Control
|
|
98
|
+
- **Role-Based Access Control (RBAC)** - Enterprise-grade authentication and authorization
|
|
99
|
+
- Built-in user/role system with bcrypt password hashing
|
|
100
|
+
- Configurable adapters for Devise, CanCanCan, Pundit, or custom auth systems
|
|
101
|
+
- 5 default roles (Admin, Editor, Viewer, Auditor, Approver) with 7 permissions
|
|
102
|
+
- Password reset functionality with secure token management
|
|
103
|
+
- Comprehensive access audit logging for compliance
|
|
104
|
+
- Web UI integration with login and user management pages
|
|
105
|
+
|
|
79
106
|
### Developer Experience
|
|
80
107
|
- **Pluggable Architecture** - Custom evaluators, scoring, audit adapters
|
|
81
108
|
- **Framework Agnostic** - Works with Rails, Sinatra, or standalone
|
|
82
109
|
- **JSON Rule DSL** - Non-technical users can write rules
|
|
83
|
-
- **
|
|
110
|
+
- **DMN 1.3 Support** - Industry-standard Decision Model and Notation with full FEEL expression language
|
|
111
|
+
- **Visual Rule Builder** - Web UI for rule management and DMN modeler
|
|
112
|
+
- **CLI Tools** - Command-line interface for DMN import/export and web server
|
|
84
113
|
|
|
85
114
|
### Production Features
|
|
86
115
|
- **Real-time Monitoring** - Live dashboard with WebSocket updates
|
|
116
|
+
- **Persistent Monitoring** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
87
117
|
- **Prometheus Export** - Industry-standard metrics format
|
|
88
118
|
- **Intelligent Alerting** - Anomaly detection with customizable rules
|
|
89
119
|
- **Grafana Integration** - Pre-built dashboards and alert rules
|
|
90
|
-
- **Version Control** - Full rule version control and
|
|
120
|
+
- **Version Control** - Full rule version control, rollback, and history ([Versioning Guide](docs/VERSIONING.md))
|
|
91
121
|
- **Thread-Safe** - Safe for multi-threaded servers and background jobs
|
|
92
122
|
- **High Performance** - 10,000+ decisions/second, ~0.1ms latency
|
|
93
123
|
|
|
@@ -121,6 +151,91 @@ end
|
|
|
121
151
|
|
|
122
152
|
See [Web UI Integration Guide](docs/WEB_UI_RAILS_INTEGRATION.md) for detailed setup.
|
|
123
153
|
|
|
154
|
+
## DMN (Decision Model and Notation) Support
|
|
155
|
+
|
|
156
|
+
DecisionAgent includes full support for **DMN 1.3**, the industry standard for decision modeling:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
require 'decision_agent'
|
|
160
|
+
require 'decision_agent/dmn/importer'
|
|
161
|
+
require 'decision_agent/evaluators/dmn_evaluator'
|
|
162
|
+
|
|
163
|
+
# Import DMN XML file
|
|
164
|
+
importer = DecisionAgent::Dmn::Importer.new
|
|
165
|
+
result = importer.import('path/to/model.dmn', created_by: 'user@example.com')
|
|
166
|
+
|
|
167
|
+
# Create DMN evaluator
|
|
168
|
+
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
169
|
+
model: result[:model],
|
|
170
|
+
decision_id: 'loan_approval'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Use with Agent
|
|
174
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
175
|
+
result = agent.decide(context: { amount: 50000, credit_score: 750 })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Features:**
|
|
179
|
+
- **DMN 1.3 Standard** - Full OMG DMN 1.3 compliance
|
|
180
|
+
- **FEEL Expressions** - Complete FEEL 1.3 language support (arithmetic, logical, functions)
|
|
181
|
+
- **All Hit Policies** - UNIQUE, FIRST, PRIORITY, ANY, COLLECT
|
|
182
|
+
- **Import/Export** - Round-trip conversion with other DMN tools (Camunda, Drools, IBM ODM)
|
|
183
|
+
- **Visual Modeler** - Web-based DMN editor at `/dmn/editor`
|
|
184
|
+
- **CLI Commands** - `decision_agent dmn import` and `decision_agent dmn export`
|
|
185
|
+
|
|
186
|
+
See [DMN Guide](docs/DMN_GUIDE.md) for complete documentation and [DMN Examples](examples/dmn/README.md) for working examples.
|
|
187
|
+
|
|
188
|
+
## REST API Data Enrichment
|
|
189
|
+
|
|
190
|
+
DecisionAgent supports fetching external data during decision-making without manual context assembly:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
require 'decision_agent'
|
|
194
|
+
|
|
195
|
+
# Configure data enrichment endpoints
|
|
196
|
+
DecisionAgent.configure_data_enrichment do |config|
|
|
197
|
+
config.add_endpoint(:credit_bureau,
|
|
198
|
+
url: "https://api.creditbureau.com/v1/score",
|
|
199
|
+
method: :post,
|
|
200
|
+
auth: { type: :api_key, header: "X-API-Key" },
|
|
201
|
+
cache: { ttl: 3600, adapter: :memory }
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Use in rules with fetch_from_api operator
|
|
206
|
+
rules = {
|
|
207
|
+
version: "1.0",
|
|
208
|
+
ruleset: "loan_approval",
|
|
209
|
+
rules: [{
|
|
210
|
+
id: "check_credit",
|
|
211
|
+
if: {
|
|
212
|
+
field: "credit_score",
|
|
213
|
+
op: "fetch_from_api",
|
|
214
|
+
value: {
|
|
215
|
+
endpoint: "credit_bureau",
|
|
216
|
+
params: { ssn: "{{customer.ssn}}" },
|
|
217
|
+
mapping: { score: "credit_score" }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
then: { decision: "approve", weight: 0.8 }
|
|
221
|
+
}]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
225
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
226
|
+
result = agent.decide(context: { customer: { ssn: "123-45-6789" } })
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Features:**
|
|
230
|
+
- **HTTP Client** - Support for GET, POST, PUT, DELETE methods
|
|
231
|
+
- **Response Caching** - Configurable TTL per endpoint with memory adapter
|
|
232
|
+
- **Circuit Breaker** - Fail-fast after N failures to prevent cascading failures
|
|
233
|
+
- **Authentication** - API key, Basic Auth, and Bearer token support
|
|
234
|
+
- **Template Parameters** - Use `{{path}}` syntax to reference context values
|
|
235
|
+
- **Error Handling** - Graceful degradation with cached data fallback
|
|
236
|
+
|
|
237
|
+
See [Data Enrichment Guide](docs/DATA_ENRICHMENT.md) for complete documentation and [Data Enrichment Example](examples/data_enrichment_example.rb) for working examples.
|
|
238
|
+
|
|
124
239
|
## Monitoring & Analytics
|
|
125
240
|
|
|
126
241
|
Real-time monitoring, metrics, and alerting for production environments.
|
|
@@ -140,11 +255,155 @@ Open [http://localhost:4568](http://localhost:4568) for the monitoring dashboard
|
|
|
140
255
|
|
|
141
256
|
**Features:**
|
|
142
257
|
- Real-time dashboard with WebSocket updates
|
|
258
|
+
- **Persistent Storage** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
143
259
|
- Prometheus metrics export
|
|
144
260
|
- Intelligent alerting with anomaly detection
|
|
145
261
|
- Grafana integration with pre-built dashboards
|
|
146
262
|
|
|
147
|
-
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complete documentation.
|
|
263
|
+
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) and [Persistent Monitoring Guide](docs/PERSISTENT_MONITORING.md) for complete documentation.
|
|
264
|
+
|
|
265
|
+
## Simulation & What-If Analysis
|
|
266
|
+
|
|
267
|
+
DecisionAgent provides comprehensive simulation capabilities to test rule changes before deployment:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
require 'decision_agent/simulation/replay_engine'
|
|
271
|
+
|
|
272
|
+
# Replay historical decisions with new rules
|
|
273
|
+
replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
|
|
274
|
+
agent: agent,
|
|
275
|
+
version_manager: version_manager
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
results = replay_engine.replay(historical_data: "decisions.csv")
|
|
279
|
+
|
|
280
|
+
# What-if analysis
|
|
281
|
+
whatif = DecisionAgent::Simulation::WhatIfAnalyzer.new(agent: agent)
|
|
282
|
+
analysis = whatif.analyze(
|
|
283
|
+
base_context: { credit_score: 700, amount: 50000 },
|
|
284
|
+
scenarios: [
|
|
285
|
+
{ credit_score: 750 },
|
|
286
|
+
{ credit_score: 650 }
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Impact analysis
|
|
291
|
+
impact = DecisionAgent::Simulation::ImpactAnalyzer.new
|
|
292
|
+
comparison = impact.compare(
|
|
293
|
+
baseline: baseline_evaluator,
|
|
294
|
+
proposed: proposed_evaluator,
|
|
295
|
+
contexts: test_contexts
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Features:**
|
|
300
|
+
- **Historical Replay** - Replay past decisions with CSV/JSON/database import
|
|
301
|
+
- **What-If Analysis** - Scenario simulation with decision boundary visualization
|
|
302
|
+
- **Impact Analysis** - Quantify rule change effects (decisions, confidence, performance)
|
|
303
|
+
- **Shadow Testing** - Test new rules in production without affecting outcomes
|
|
304
|
+
- **Monte Carlo Simulation** - Probabilistic decision modeling
|
|
305
|
+
- **Web UI** - Complete simulation dashboard at `/simulation`
|
|
306
|
+
|
|
307
|
+
See [Simulation Guide](docs/SIMULATION.md) for complete documentation and [Simulation Example](examples/simulation_example.rb) for working examples.
|
|
308
|
+
|
|
309
|
+
## Role-Based Access Control (RBAC)
|
|
310
|
+
|
|
311
|
+
Enterprise-grade authentication and authorization system:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
require 'decision_agent'
|
|
315
|
+
|
|
316
|
+
# Configure RBAC (works with any auth system)
|
|
317
|
+
DecisionAgent.configure_rbac(:devise_cancan, ability_class: Ability)
|
|
318
|
+
|
|
319
|
+
# Or use built-in RBAC
|
|
320
|
+
authenticator = DecisionAgent::Auth::Authenticator.new
|
|
321
|
+
admin = authenticator.create_user(
|
|
322
|
+
email: "admin@example.com",
|
|
323
|
+
password: "secure_password",
|
|
324
|
+
roles: [:admin]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
session = authenticator.login("admin@example.com", "secure_password")
|
|
328
|
+
|
|
329
|
+
# Permission checks
|
|
330
|
+
checker = DecisionAgent.permission_checker
|
|
331
|
+
checker.can?(admin, :write) # => true
|
|
332
|
+
checker.can?(admin, :approve) # => true
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Features:**
|
|
336
|
+
- **Built-in User System** - User management with bcrypt password hashing
|
|
337
|
+
- **5 Default Roles** - Admin, Editor, Viewer, Auditor, Approver
|
|
338
|
+
- **Configurable Adapters** - Devise, CanCanCan, Pundit, or custom
|
|
339
|
+
- **Password Reset** - Secure token-based password reset
|
|
340
|
+
- **Access Audit Logging** - Comprehensive audit trail for compliance
|
|
341
|
+
- **Web UI Integration** - Login page and user management interface
|
|
342
|
+
|
|
343
|
+
See [RBAC Configuration Guide](docs/RBAC_CONFIGURATION.md) for complete documentation and [RBAC Examples](examples/rbac_configuration_examples.rb) for integration examples.
|
|
344
|
+
|
|
345
|
+
## Batch Testing
|
|
346
|
+
|
|
347
|
+
Test rules against large datasets with comprehensive analysis:
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
require 'decision_agent/testing/batch_test_runner'
|
|
351
|
+
|
|
352
|
+
runner = DecisionAgent::Testing::BatchTestRunner.new(agent: agent)
|
|
353
|
+
|
|
354
|
+
# Import from CSV or Excel
|
|
355
|
+
importer = DecisionAgent::Testing::BatchTestImporter.new
|
|
356
|
+
scenarios = importer.import_csv("test_data.csv", {
|
|
357
|
+
context_fields: ["credit_score", "amount"],
|
|
358
|
+
expected_fields: ["expected_decision"]
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
# Run batch test
|
|
362
|
+
results = runner.run(scenarios: scenarios)
|
|
363
|
+
|
|
364
|
+
puts "Total: #{results[:total]}"
|
|
365
|
+
puts "Passed: #{results[:passed]}"
|
|
366
|
+
puts "Failed: #{results[:failed]}"
|
|
367
|
+
puts "Coverage: #{results[:coverage]}"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Features:**
|
|
371
|
+
- **CSV/Excel Import** - Import test scenarios from files
|
|
372
|
+
- **Database Import** - Load test data from databases
|
|
373
|
+
- **Coverage Analysis** - Identify untested rule combinations
|
|
374
|
+
- **Resume Capability** - Continue interrupted tests from checkpoint
|
|
375
|
+
- **Progress Tracking** - Real-time progress updates for large imports
|
|
376
|
+
- **Web UI** - Complete batch testing interface with file upload
|
|
377
|
+
|
|
378
|
+
See [Batch Testing Guide](docs/BATCH_TESTING.md) for complete documentation.
|
|
379
|
+
|
|
380
|
+
## A/B Testing
|
|
381
|
+
|
|
382
|
+
Compare rule versions with statistical analysis:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
require 'decision_agent/testing/ab_test_manager'
|
|
386
|
+
|
|
387
|
+
ab_manager = DecisionAgent::Testing::AbTestManager.new(version_manager: version_manager)
|
|
388
|
+
|
|
389
|
+
test = ab_manager.create_test(
|
|
390
|
+
name: "loan_approval_v2",
|
|
391
|
+
champion_version: champion_version_id,
|
|
392
|
+
challenger_version: challenger_version_id,
|
|
393
|
+
traffic_split: 0.5
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
results = ab_manager.run_test(test_id: test.id, contexts: test_contexts)
|
|
397
|
+
ab_manager.analyze_results(test_id: test.id)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Features:**
|
|
401
|
+
- **Champion/Challenger Testing** - Compare baseline vs proposed rules
|
|
402
|
+
- **Statistical Significance** - P-value calculation and confidence intervals
|
|
403
|
+
- **Traffic Splitting** - Configurable split ratios
|
|
404
|
+
- **Decision Distribution Comparison** - Visualize differences in outcomes
|
|
405
|
+
|
|
406
|
+
See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
|
|
148
407
|
|
|
149
408
|
## When to Use DecisionAgent
|
|
150
409
|
|
|
@@ -169,18 +428,31 @@ See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complet
|
|
|
169
428
|
- [Examples Directory](examples/README.md) - Working examples with explanations
|
|
170
429
|
|
|
171
430
|
### Core Features
|
|
431
|
+
- [Explainability Layer](docs/EXPLAINABILITY.md) - Machine-readable decision explanations with condition-level tracing
|
|
172
432
|
- [Advanced Operators](docs/ADVANCED_OPERATORS.md) - String, numeric, date/time, collection, and geospatial operators
|
|
433
|
+
- [Data Enrichment](docs/DATA_ENRICHMENT.md) - REST API data enrichment with caching and circuit breaker
|
|
434
|
+
- [DMN Guide](docs/DMN_GUIDE.md) - Complete DMN 1.3 support guide
|
|
435
|
+
- [DMN API Reference](docs/DMN_API.md) - DMN API documentation
|
|
436
|
+
- [FEEL Reference](docs/FEEL_REFERENCE.md) - FEEL expression language reference
|
|
437
|
+
- [DMN Migration Guide](docs/DMN_MIGRATION_GUIDE.md) - Migrating from JSON to DMN
|
|
438
|
+
- [DMN Best Practices](docs/DMN_BEST_PRACTICES.md) - DMN modeling best practices
|
|
173
439
|
- [Versioning System](docs/VERSIONING.md) - Version control for rules
|
|
440
|
+
- [Simulation & What-If Analysis](docs/SIMULATION.md) - Historical replay, what-if analysis, impact analysis, and shadow testing
|
|
174
441
|
- [A/B Testing](docs/AB_TESTING.md) - Compare rule versions with statistical analysis
|
|
442
|
+
- [Batch Testing](docs/BATCH_TESTING.md) - Test rules against large datasets with CSV/Excel import
|
|
443
|
+
- [RBAC Configuration](docs/RBAC_CONFIGURATION.md) - Role-based access control setup and integration
|
|
444
|
+
- [RBAC Quick Reference](docs/RBAC_QUICK_REFERENCE.md) - Quick reference for RBAC configuration
|
|
175
445
|
- [Web UI](docs/WEB_UI.md) - Visual rule builder
|
|
176
446
|
- [Web UI Setup](docs/WEB_UI_SETUP.md) - Setup guide
|
|
177
447
|
- [Web UI Rails Integration](docs/WEB_UI_RAILS_INTEGRATION.md) - Mount in Rails/Rack apps
|
|
178
448
|
- [Monitoring & Analytics](docs/MONITORING_AND_ANALYTICS.md) - Real-time monitoring, metrics, and alerting
|
|
179
449
|
- [Monitoring Architecture](docs/MONITORING_ARCHITECTURE.md) - System architecture and design
|
|
450
|
+
- [Persistent Monitoring](docs/PERSISTENT_MONITORING.md) - Database storage for long-term analytics
|
|
180
451
|
|
|
181
452
|
### Performance & Thread-Safety
|
|
182
453
|
- [Performance & Thread-Safety Summary](docs/PERFORMANCE_AND_THREAD_SAFETY.md) - Benchmarks and production readiness
|
|
183
454
|
- [Thread-Safety Implementation](docs/THREAD_SAFETY.md) - Technical implementation guide
|
|
455
|
+
- [Benchmarks](benchmarks/README.md) - Comprehensive benchmark suite and performance testing
|
|
184
456
|
|
|
185
457
|
### Reference
|
|
186
458
|
- [API Contract](docs/API_CONTRACT.md) - Full API reference
|
|
@@ -204,6 +476,39 @@ All data structures are deeply frozen to prevent mutation, ensuring safe concurr
|
|
|
204
476
|
|
|
205
477
|
See [Thread-Safety Guide](docs/THREAD_SAFETY.md) and [Performance Analysis](docs/PERFORMANCE_AND_THREAD_SAFETY.md) for details.
|
|
206
478
|
|
|
479
|
+
**Run Benchmarks:**
|
|
480
|
+
```bash
|
|
481
|
+
# Run all benchmarks
|
|
482
|
+
rake benchmark:all
|
|
483
|
+
|
|
484
|
+
# Run specific benchmarks
|
|
485
|
+
rake benchmark:basic # Basic decision performance
|
|
486
|
+
rake benchmark:threads # Thread-safety and scalability
|
|
487
|
+
rake benchmark:regression # Compare against baseline
|
|
488
|
+
|
|
489
|
+
# See [Benchmarks Guide](benchmarks/README.md) for complete documentation
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Latest Benchmark Results
|
|
493
|
+
|
|
494
|
+
**Last Updated:** 2026-01-06T04:03:29Z
|
|
495
|
+
|
|
496
|
+
#### Performance Comparison
|
|
497
|
+
|
|
498
|
+
| Metric | Latest (2026-01-06) | Previous (2026-01-06) | Change |
|
|
499
|
+
|--------|--------------------------------------------------|------------------------------------------------------|--------|
|
|
500
|
+
| Basic Throughput | 8966.04 decisions/sec | 9751.42 decisions/sec | ↓ 8.05% (degraded) |
|
|
501
|
+
| Basic Latency | 0.1115 ms | 0.1025 ms | ↑ 8.78% (degraded) |
|
|
502
|
+
| Multi-threaded (50 threads) Throughput | 8560.69 decisions/sec | 8849.86 decisions/sec | ↓ 3.27% (degraded) |
|
|
503
|
+
| Multi-threaded (50 threads) Latency | 0.1168 ms | 0.113 ms | ↑ 3.36% (degraded) |
|
|
504
|
+
|
|
505
|
+
**Environment:**
|
|
506
|
+
- Ruby Version: 3.3.5
|
|
507
|
+
- Hardware: x86_64
|
|
508
|
+
- OS: Darwin
|
|
509
|
+
- Git Commit: `aba46af5`
|
|
510
|
+
|
|
511
|
+
> 💡 **Note:** Run `rake benchmark:regression` to generate new benchmark results. This section is automatically updated with the last 2 benchmark runs.
|
|
207
512
|
## Contributing
|
|
208
513
|
|
|
209
514
|
1. Fork the repository
|
data/bin/decision_agent
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require_relative "../lib/decision_agent"
|
|
5
5
|
require_relative "../lib/decision_agent/web/server"
|
|
6
|
+
require_relative "../lib/decision_agent/dmn/importer"
|
|
7
|
+
require_relative "../lib/decision_agent/dmn/exporter"
|
|
6
8
|
|
|
7
9
|
def print_help
|
|
8
10
|
puts <<~HELP
|
|
@@ -14,6 +16,8 @@ def print_help
|
|
|
14
16
|
Commands:
|
|
15
17
|
web [PORT] Start the web UI rule builder (default port: 4567)
|
|
16
18
|
validate FILE Validate a rules JSON file
|
|
19
|
+
dmn import FILE Import a DMN XML file
|
|
20
|
+
dmn export RULESET OUTPUT Export a ruleset to DMN XML
|
|
17
21
|
version Show version
|
|
18
22
|
help Show this help message
|
|
19
23
|
|
|
@@ -21,6 +25,8 @@ def print_help
|
|
|
21
25
|
decision_agent web # Start web UI on port 4567
|
|
22
26
|
decision_agent web 8080 # Start web UI on port 8080
|
|
23
27
|
decision_agent validate rules.json
|
|
28
|
+
decision_agent dmn import loan_decision.dmn
|
|
29
|
+
decision_agent dmn export loan_rules loan_export.dmn
|
|
24
30
|
decision_agent version
|
|
25
31
|
|
|
26
32
|
For more information, visit:
|
|
@@ -74,6 +80,75 @@ def validate_file(filepath)
|
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
|
|
83
|
+
def dmn_import(filepath, ruleset_name: nil)
|
|
84
|
+
unless File.exist?(filepath)
|
|
85
|
+
puts "❌ Error: File not found: #{filepath}"
|
|
86
|
+
exit 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
puts "📥 Importing DMN file: #{filepath}..."
|
|
91
|
+
|
|
92
|
+
importer = DecisionAgent::Dmn::Importer.new
|
|
93
|
+
result = importer.import(
|
|
94
|
+
filepath,
|
|
95
|
+
ruleset_name: ruleset_name,
|
|
96
|
+
created_by: ENV["USER"] || "cli_user"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
puts "✅ Import successful!"
|
|
100
|
+
puts " Model: #{result[:model].name}"
|
|
101
|
+
puts " Decisions imported: #{result[:decisions_imported]}"
|
|
102
|
+
puts " Namespace: #{result[:model].namespace}"
|
|
103
|
+
|
|
104
|
+
result[:model].decisions.each do |decision|
|
|
105
|
+
puts " - Decision: #{decision.name} (#{decision.id})"
|
|
106
|
+
if decision.decision_table
|
|
107
|
+
puts " Rules: #{decision.decision_table.rules.size}"
|
|
108
|
+
puts " Hit Policy: #{decision.decision_table.hit_policy}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if result[:versions].any?
|
|
113
|
+
puts ""
|
|
114
|
+
puts " Versions created:"
|
|
115
|
+
result[:versions].each do |version|
|
|
116
|
+
puts " - #{version[:rule_id]}: version #{version[:version]}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
rescue DecisionAgent::Dmn::InvalidDmnModelError, DecisionAgent::Dmn::DmnParseError => e
|
|
120
|
+
puts "❌ DMN Import Error:"
|
|
121
|
+
puts " #{e.message}"
|
|
122
|
+
exit 1
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
puts "❌ Unexpected Error:"
|
|
125
|
+
puts " #{e.message}"
|
|
126
|
+
puts " #{e.backtrace.first}" if ENV["DEBUG"]
|
|
127
|
+
exit 1
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def dmn_export(ruleset_id, output_path)
|
|
132
|
+
puts "📤 Exporting ruleset: #{ruleset_id}..."
|
|
133
|
+
|
|
134
|
+
exporter = DecisionAgent::Dmn::Exporter.new
|
|
135
|
+
dmn_xml = exporter.export(ruleset_id, output_path: output_path)
|
|
136
|
+
|
|
137
|
+
puts "✅ Export successful!"
|
|
138
|
+
puts " Ruleset: #{ruleset_id}"
|
|
139
|
+
puts " Output: #{output_path}"
|
|
140
|
+
puts " Size: #{dmn_xml.bytesize} bytes"
|
|
141
|
+
rescue DecisionAgent::Dmn::InvalidDmnModelError => e
|
|
142
|
+
puts "❌ Export Error:"
|
|
143
|
+
puts " #{e.message}"
|
|
144
|
+
exit 1
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
puts "❌ Unexpected Error:"
|
|
147
|
+
puts " #{e.message}"
|
|
148
|
+
puts " #{e.backtrace.first}" if ENV["DEBUG"]
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
|
|
77
152
|
# Main CLI handler
|
|
78
153
|
command = ARGV[0] || "help"
|
|
79
154
|
|
|
@@ -90,6 +165,35 @@ when "validate"
|
|
|
90
165
|
end
|
|
91
166
|
validate_file(ARGV[1])
|
|
92
167
|
|
|
168
|
+
when "dmn"
|
|
169
|
+
subcommand = ARGV[1]
|
|
170
|
+
case subcommand
|
|
171
|
+
when "import"
|
|
172
|
+
if ARGV[2].nil?
|
|
173
|
+
puts "❌ Error: Please provide a DMN file path"
|
|
174
|
+
puts "Usage: decision_agent dmn import <file.xml>"
|
|
175
|
+
exit 1
|
|
176
|
+
end
|
|
177
|
+
ruleset_name = ARGV[3] # Optional ruleset name
|
|
178
|
+
dmn_import(ARGV[2], ruleset_name: ruleset_name)
|
|
179
|
+
|
|
180
|
+
when "export"
|
|
181
|
+
if ARGV[2].nil? || ARGV[3].nil?
|
|
182
|
+
puts "❌ Error: Please provide ruleset ID and output file path"
|
|
183
|
+
puts "Usage: decision_agent dmn export <ruleset> <output.xml>"
|
|
184
|
+
exit 1
|
|
185
|
+
end
|
|
186
|
+
dmn_export(ARGV[2], ARGV[3])
|
|
187
|
+
|
|
188
|
+
else
|
|
189
|
+
puts "❌ Unknown DMN subcommand: #{subcommand || '(none)'}"
|
|
190
|
+
puts ""
|
|
191
|
+
puts "DMN Commands:"
|
|
192
|
+
puts " import FILE Import a DMN XML file"
|
|
193
|
+
puts " export RULESET OUTPUT Export a ruleset to DMN XML"
|
|
194
|
+
exit 1
|
|
195
|
+
end
|
|
196
|
+
|
|
93
197
|
when "version"
|
|
94
198
|
puts "DecisionAgent version #{DecisionAgent::VERSION}"
|
|
95
199
|
|
data/lib/decision_agent/agent.rb
CHANGED
|
@@ -6,6 +6,17 @@ module DecisionAgent
|
|
|
6
6
|
class Agent
|
|
7
7
|
attr_reader :evaluators, :scoring_strategy, :audit_adapter
|
|
8
8
|
|
|
9
|
+
# Thread-safe cache for deterministic hash computation
|
|
10
|
+
# This significantly improves performance when the same context/evaluations
|
|
11
|
+
# are processed multiple times (common in benchmarks and high-throughput scenarios)
|
|
12
|
+
@hash_cache = {}
|
|
13
|
+
@hash_cache_mutex = Mutex.new
|
|
14
|
+
@hash_cache_max_size = 1000 # Limit cache size to prevent memory bloat
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :hash_cache, :hash_cache_mutex, :hash_cache_max_size
|
|
18
|
+
end
|
|
19
|
+
|
|
9
20
|
def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil, validate_evaluations: nil)
|
|
10
21
|
@evaluators = Array(evaluators)
|
|
11
22
|
@scoring_strategy = scoring_strategy || Scoring::WeightedAverage.new
|
|
@@ -129,8 +140,68 @@ module DecisionAgent
|
|
|
129
140
|
|
|
130
141
|
def compute_deterministic_hash(payload)
|
|
131
142
|
hashable = payload.slice(:context, :evaluations, :decision, :confidence, :scoring_strategy)
|
|
143
|
+
|
|
144
|
+
# Use fast hash (MD5) as cache key to avoid expensive canonicalization on cache hits
|
|
145
|
+
# Optimized: Use direct JSON.to_json instead of recursive sorting for speed
|
|
146
|
+
# The cache key doesn't need perfect determinism, just good enough to catch duplicates
|
|
147
|
+
# This avoids the expensive sort_hash_keys recursion on every call
|
|
148
|
+
json_str = hashable.to_json
|
|
149
|
+
fast_key = Digest::MD5.hexdigest(json_str)
|
|
150
|
+
|
|
151
|
+
# Fast path: check cache without lock first (unsafe read, but acceptable for cache)
|
|
152
|
+
# This allows concurrent reads without mutex overhead
|
|
153
|
+
cache = self.class.hash_cache
|
|
154
|
+
cached_hash = cache[fast_key]
|
|
155
|
+
return cached_hash if cached_hash
|
|
156
|
+
|
|
157
|
+
# Cache miss - compute canonical JSON (required for deterministic hashing)
|
|
158
|
+
# This is expensive, but only happens on cache misses
|
|
132
159
|
canonical = canonical_json(hashable)
|
|
133
|
-
|
|
160
|
+
|
|
161
|
+
# Compute SHA256 hash (also expensive, but only on cache misses)
|
|
162
|
+
computed_hash = Digest::SHA256.hexdigest(canonical)
|
|
163
|
+
|
|
164
|
+
# Store in cache (thread-safe, with size limit)
|
|
165
|
+
# Only lock when we need to write
|
|
166
|
+
self.class.hash_cache_mutex.synchronize do
|
|
167
|
+
# Double-check after acquiring lock (another thread may have added it)
|
|
168
|
+
return self.class.hash_cache[fast_key] if self.class.hash_cache[fast_key]
|
|
169
|
+
|
|
170
|
+
# Clear cache if it gets too large (simple FIFO eviction)
|
|
171
|
+
if self.class.hash_cache.size >= self.class.hash_cache_max_size
|
|
172
|
+
# Remove oldest 10% of entries (simple approximation)
|
|
173
|
+
keys_to_remove = self.class.hash_cache.keys.first(self.class.hash_cache_max_size / 10)
|
|
174
|
+
keys_to_remove.each { |key| self.class.hash_cache.delete(key) }
|
|
175
|
+
end
|
|
176
|
+
self.class.hash_cache[fast_key] = computed_hash
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
computed_hash
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Fast hash key generation using MD5 (much faster than canonical JSON + SHA256)
|
|
183
|
+
# Used as cache key to avoid expensive canonicalization on cache hits
|
|
184
|
+
# MD5 is sufficient for cache keys (collision resistance not critical, speed is)
|
|
185
|
+
def fast_hash_key(hashable)
|
|
186
|
+
# Create a deterministic string representation for hashing
|
|
187
|
+
# Use sorted JSON to ensure determinism (though not RFC 8785 canonical)
|
|
188
|
+
json_str = sort_hash_keys(hashable).to_json
|
|
189
|
+
Digest::MD5.hexdigest(json_str)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Recursively sort hash keys for deterministic hashing
|
|
193
|
+
# This is faster than canonical JSON but still deterministic
|
|
194
|
+
# Note: This is still used by canonical_json indirectly, but fast_hash_key avoids it
|
|
195
|
+
def sort_hash_keys(obj)
|
|
196
|
+
case obj
|
|
197
|
+
when Hash
|
|
198
|
+
sorted = obj.sort.to_h
|
|
199
|
+
sorted.transform_values { |v| sort_hash_keys(v) }
|
|
200
|
+
when Array
|
|
201
|
+
obj.map { |v| sort_hash_keys(v) }
|
|
202
|
+
else
|
|
203
|
+
obj
|
|
204
|
+
end
|
|
134
205
|
end
|
|
135
206
|
|
|
136
207
|
# Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
|
|
@@ -4,6 +4,7 @@ module DecisionAgent
|
|
|
4
4
|
|
|
5
5
|
def initialize(data)
|
|
6
6
|
# Create a deep copy before freezing to avoid mutating the original
|
|
7
|
+
# This is necessary for thread-safety even if it adds some overhead
|
|
7
8
|
data_hash = data.is_a?(Hash) ? data : {}
|
|
8
9
|
@data = deep_freeze(deep_dup(data_hash))
|
|
9
10
|
end
|