decision_agent 0.3.0 → 1.1.0
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 +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6c760652b22f7cf3e4524aec1757f1fba1fc21c206c960ad3836eed18279f13
|
|
4
|
+
data.tar.gz: e0c2f21dfac36d7a4ce7935d6ea38375265c64e15eec4c276c514a1bf759dd6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9a402f62fa86a4cf83d727f048b6de8a8d67ab317cbc3dd2d681cd537a2a1a68a1bbe77b2f34a4c6f4a805babace2e7c6d449435d8fef9831dff884152b441a
|
|
7
|
+
data.tar.gz: 2b00ae31f1e89caa62c004c6e649296e1c6a193faf90ded7ef2fd6e930b3c5547113d028349de2b6904bfb9e2371fd37335251b6391bac4315cda1ca6407c4e8
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/decision_agent)
|
|
4
4
|
[](https://github.com/samaswin/decision_agent/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE.txt)
|
|
6
|
-
[](https://www.ruby-lang.org)
|
|
7
7
|
|
|
8
8
|
A production-grade, deterministic, explainable, and auditable decision engine for Ruby.
|
|
9
9
|
|
|
@@ -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.
|
|
@@ -72,23 +75,48 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
|
|
|
72
75
|
|
|
73
76
|
### Auditability & Compliance
|
|
74
77
|
- **Complete Audit Trails** - Every decision fully logged
|
|
78
|
+
- **Explainability Layer** - Machine-readable condition traces for every decision
|
|
79
|
+
- `result.because` - Conditions that led to the decision
|
|
80
|
+
- `result.failed_conditions` - Conditions that failed
|
|
81
|
+
- `result.explainability` - Complete machine-readable explainability data
|
|
75
82
|
- **Deterministic Replay** - Reproduce historical decisions exactly
|
|
76
83
|
- **RFC 8785 Canonical JSON** - Industry-standard deterministic hashing
|
|
77
84
|
- **Compliance Ready** - HIPAA, SOX, regulatory compliance support
|
|
78
85
|
|
|
86
|
+
### Testing & Simulation
|
|
87
|
+
- **Simulation & What-If Analysis** - Test rule changes before deployment
|
|
88
|
+
- **Historical Replay / Backtesting** - Replay past decisions with new rules (CSV, JSON, database import)
|
|
89
|
+
- **What-If Analysis** - Simulate scenarios and sensitivity analysis with decision boundary visualization
|
|
90
|
+
- **Impact Analysis** - Quantify rule change effects (decision distribution, confidence shifts, performance impact)
|
|
91
|
+
- **Shadow Testing** - Compare new rules against production without affecting outcomes
|
|
92
|
+
- **Monte Carlo Simulation** - Model probabilistic inputs and understand decision outcome probabilities
|
|
93
|
+
- **Batch Testing** - Test rules against large datasets with CSV/Excel import, coverage analysis, and resume capability
|
|
94
|
+
- **A/B Testing** - Champion/Challenger testing with statistical significance analysis
|
|
95
|
+
|
|
96
|
+
### Security & Access Control
|
|
97
|
+
- **Role-Based Access Control (RBAC)** - Enterprise-grade authentication and authorization
|
|
98
|
+
- Built-in user/role system with bcrypt password hashing
|
|
99
|
+
- Configurable adapters for Devise, CanCanCan, Pundit, or custom auth systems
|
|
100
|
+
- 5 default roles (Admin, Editor, Viewer, Auditor, Approver) with 7 permissions
|
|
101
|
+
- Password reset functionality with secure token management
|
|
102
|
+
- Comprehensive access audit logging for compliance
|
|
103
|
+
- Web UI integration with login and user management pages
|
|
104
|
+
|
|
79
105
|
### Developer Experience
|
|
80
106
|
- **Pluggable Architecture** - Custom evaluators, scoring, audit adapters
|
|
81
|
-
- **Framework Agnostic** - Works with Rails,
|
|
107
|
+
- **Framework Agnostic** - Works with Rails, Rack, or standalone
|
|
82
108
|
- **JSON Rule DSL** - Non-technical users can write rules
|
|
83
109
|
- **DMN 1.3 Support** - Industry-standard Decision Model and Notation with full FEEL expression language
|
|
84
110
|
- **Visual Rule Builder** - Web UI for rule management and DMN modeler
|
|
111
|
+
- **CLI Tools** - Command-line interface for DMN import/export and web server
|
|
85
112
|
|
|
86
113
|
### Production Features
|
|
87
114
|
- **Real-time Monitoring** - Live dashboard with WebSocket updates
|
|
115
|
+
- **Persistent Monitoring** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
88
116
|
- **Prometheus Export** - Industry-standard metrics format
|
|
89
117
|
- **Intelligent Alerting** - Anomaly detection with customizable rules
|
|
90
118
|
- **Grafana Integration** - Pre-built dashboards and alert rules
|
|
91
|
-
- **Version Control** - Full rule version control and
|
|
119
|
+
- **Version Control** - Full rule version control, rollback, and history ([Versioning Guide](docs/VERSIONING.md))
|
|
92
120
|
- **Thread-Safe** - Safe for multi-threaded servers and background jobs
|
|
93
121
|
- **High Performance** - 10,000+ decisions/second, ~0.1ms latency
|
|
94
122
|
|
|
@@ -102,6 +130,8 @@ decision_agent web
|
|
|
102
130
|
|
|
103
131
|
Open [http://localhost:4567](http://localhost:4567) in your browser.
|
|
104
132
|
|
|
133
|
+
The Web UI includes a **DMN visual modeler** at `/dmn/editor` for building and editing decision tables.
|
|
134
|
+
|
|
105
135
|
### Integration
|
|
106
136
|
|
|
107
137
|
**Rails:**
|
|
@@ -112,7 +142,7 @@ Rails.application.routes.draw do
|
|
|
112
142
|
end
|
|
113
143
|
```
|
|
114
144
|
|
|
115
|
-
**Rack
|
|
145
|
+
**Rack:**
|
|
116
146
|
```ruby
|
|
117
147
|
require 'decision_agent/web/server'
|
|
118
148
|
map '/decision_agent' do
|
|
@@ -120,7 +150,7 @@ map '/decision_agent' do
|
|
|
120
150
|
end
|
|
121
151
|
```
|
|
122
152
|
|
|
123
|
-
See [Web UI Integration Guide](docs/
|
|
153
|
+
See [Web UI Integration Guide](docs/WEB_UI_INTEGRATION.md) for detailed setup with Rails, Sinatra, Hanami, and other frameworks.
|
|
124
154
|
|
|
125
155
|
## DMN (Decision Model and Notation) Support
|
|
126
156
|
|
|
@@ -175,11 +205,159 @@ Open [http://localhost:4568](http://localhost:4568) for the monitoring dashboard
|
|
|
175
205
|
|
|
176
206
|
**Features:**
|
|
177
207
|
- Real-time dashboard with WebSocket updates
|
|
208
|
+
- **Persistent Storage** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
178
209
|
- Prometheus metrics export
|
|
179
210
|
- Intelligent alerting with anomaly detection
|
|
180
211
|
- Grafana integration with pre-built dashboards
|
|
181
212
|
|
|
182
|
-
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complete documentation.
|
|
213
|
+
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) and [Persistent Monitoring Guide](docs/PERSISTENT_MONITORING.md) for complete documentation.
|
|
214
|
+
|
|
215
|
+
## Simulation & What-If Analysis
|
|
216
|
+
|
|
217
|
+
DecisionAgent provides comprehensive simulation capabilities to test rule changes before deployment:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
require 'decision_agent/simulation/replay_engine'
|
|
221
|
+
require 'decision_agent/simulation/what_if_analyzer'
|
|
222
|
+
require 'decision_agent/simulation/impact_analyzer'
|
|
223
|
+
|
|
224
|
+
# Replay historical decisions with new rules
|
|
225
|
+
replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
|
|
226
|
+
agent: agent,
|
|
227
|
+
version_manager: version_manager
|
|
228
|
+
)
|
|
229
|
+
results = replay_engine.replay(historical_data: "decisions.csv")
|
|
230
|
+
|
|
231
|
+
# What-if analysis (scenarios = array of context hashes)
|
|
232
|
+
whatif = DecisionAgent::Simulation::WhatIfAnalyzer.new(agent: agent)
|
|
233
|
+
analysis = whatif.analyze(
|
|
234
|
+
scenarios: [
|
|
235
|
+
{ credit_score: 700, amount: 50000 },
|
|
236
|
+
{ credit_score: 750, amount: 50000 },
|
|
237
|
+
{ credit_score: 650, amount: 50000 }
|
|
238
|
+
]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Impact analysis (compare two rule versions on test data)
|
|
242
|
+
impact = DecisionAgent::Simulation::ImpactAnalyzer.new(version_manager: version_manager)
|
|
243
|
+
comparison = impact.analyze(
|
|
244
|
+
baseline_version: baseline_version_id,
|
|
245
|
+
proposed_version: proposed_version_id,
|
|
246
|
+
test_data: test_contexts
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Features:**
|
|
251
|
+
- **Historical Replay** - Replay past decisions with CSV/JSON/database import
|
|
252
|
+
- **What-If Analysis** - Scenario simulation with decision boundary visualization
|
|
253
|
+
- **Impact Analysis** - Quantify rule change effects (decisions, confidence, performance)
|
|
254
|
+
- **Shadow Testing** - Test new rules in production without affecting outcomes
|
|
255
|
+
- **Monte Carlo Simulation** - Probabilistic decision modeling
|
|
256
|
+
- **Web UI** - Complete simulation dashboard at `/simulation`
|
|
257
|
+
|
|
258
|
+
See [Simulation Guide](docs/SIMULATION.md) for complete documentation and [Simulation Example](examples/simulation_example.rb) for working examples.
|
|
259
|
+
|
|
260
|
+
## Role-Based Access Control (RBAC)
|
|
261
|
+
|
|
262
|
+
Enterprise-grade authentication and authorization system:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
require 'decision_agent'
|
|
266
|
+
|
|
267
|
+
# Configure RBAC (works with any auth system)
|
|
268
|
+
DecisionAgent.configure_rbac(:devise_cancan, ability_class: Ability)
|
|
269
|
+
|
|
270
|
+
# Or use built-in RBAC
|
|
271
|
+
authenticator = DecisionAgent::Auth::Authenticator.new
|
|
272
|
+
admin = authenticator.create_user(
|
|
273
|
+
email: "admin@example.com",
|
|
274
|
+
password: "secure_password",
|
|
275
|
+
roles: [:admin]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
session = authenticator.login("admin@example.com", "secure_password")
|
|
279
|
+
|
|
280
|
+
# Permission checks
|
|
281
|
+
checker = DecisionAgent.permission_checker
|
|
282
|
+
checker.can?(admin, :write) # => true
|
|
283
|
+
checker.can?(admin, :approve) # => true
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Features:**
|
|
287
|
+
- **Built-in User System** - User management with bcrypt password hashing
|
|
288
|
+
- **5 Default Roles** - Admin, Editor, Viewer, Auditor, Approver
|
|
289
|
+
- **Configurable Adapters** - Devise, CanCanCan, Pundit, or custom
|
|
290
|
+
- **Password Reset** - Secure token-based password reset
|
|
291
|
+
- **Access Audit Logging** - Comprehensive audit trail for compliance
|
|
292
|
+
- **Web UI Integration** - Login page and user management interface
|
|
293
|
+
|
|
294
|
+
See [RBAC Configuration Guide](docs/RBAC_CONFIGURATION.md) for complete documentation and [RBAC Examples](examples/rbac_configuration_examples.rb) for integration examples.
|
|
295
|
+
|
|
296
|
+
## Batch Testing
|
|
297
|
+
|
|
298
|
+
Test rules against large datasets with comprehensive analysis:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
require 'decision_agent/testing/batch_test_runner'
|
|
302
|
+
require 'decision_agent/testing/batch_test_importer'
|
|
303
|
+
|
|
304
|
+
runner = DecisionAgent::Testing::BatchTestRunner.new(agent)
|
|
305
|
+
|
|
306
|
+
# Import from CSV or Excel (context columns default to all except id/expected_*)
|
|
307
|
+
importer = DecisionAgent::Testing::BatchTestImporter.new
|
|
308
|
+
scenarios = importer.import_csv("test_data.csv")
|
|
309
|
+
|
|
310
|
+
# Run batch test
|
|
311
|
+
results = runner.run(scenarios)
|
|
312
|
+
|
|
313
|
+
stats = runner.statistics
|
|
314
|
+
puts "Total: #{stats[:total]}"
|
|
315
|
+
puts "Passed: #{stats[:successful]}"
|
|
316
|
+
puts "Failed: #{stats[:failed]}"
|
|
317
|
+
puts "Success rate: #{(stats[:success_rate] * 100).round(2)}%"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Features:**
|
|
321
|
+
- **CSV/Excel Import** - Import test scenarios from files
|
|
322
|
+
- **Database Import** - Load test data from databases
|
|
323
|
+
- **Coverage Analysis** - Identify untested rule combinations
|
|
324
|
+
- **Resume Capability** - Continue interrupted tests from checkpoint
|
|
325
|
+
- **Progress Tracking** - Real-time progress updates for large imports
|
|
326
|
+
- **Web UI** - Complete batch testing interface with file upload
|
|
327
|
+
|
|
328
|
+
See [Batch Testing Guide](docs/BATCH_TESTING.md) for complete documentation.
|
|
329
|
+
|
|
330
|
+
## A/B Testing
|
|
331
|
+
|
|
332
|
+
Compare rule versions with statistical analysis:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
require 'decision_agent/ab_testing/ab_test_manager'
|
|
336
|
+
|
|
337
|
+
ab_manager = DecisionAgent::ABTesting::ABTestManager.new(version_manager: version_manager)
|
|
338
|
+
|
|
339
|
+
test = ab_manager.create_test(
|
|
340
|
+
name: "loan_approval_v2",
|
|
341
|
+
champion_version_id: champion_version_id,
|
|
342
|
+
challenger_version_id: challenger_version_id,
|
|
343
|
+
traffic_split: { champion: 90, challenger: 10 }
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Assign variant per request, then record decisions; when done, get results
|
|
347
|
+
assignment = ab_manager.assign_variant(test_id: test.id, user_id: "user_123")
|
|
348
|
+
# ... run agent with assignment[:version_id], then:
|
|
349
|
+
ab_manager.record_decision(assignment_id: assignment[:assignment_id], decision: "approve", confidence: 0.9)
|
|
350
|
+
|
|
351
|
+
results = ab_manager.get_results(test.id)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Features:**
|
|
355
|
+
- **Champion/Challenger Testing** - Compare baseline vs proposed rules
|
|
356
|
+
- **Statistical Significance** - P-value calculation and confidence intervals
|
|
357
|
+
- **Traffic Splitting** - Configurable split ratios
|
|
358
|
+
- **Decision Distribution Comparison** - Visualize differences in outcomes
|
|
359
|
+
|
|
360
|
+
See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
|
|
183
361
|
|
|
184
362
|
## When to Use DecisionAgent
|
|
185
363
|
|
|
@@ -204,6 +382,7 @@ See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complet
|
|
|
204
382
|
- [Examples Directory](examples/README.md) - Working examples with explanations
|
|
205
383
|
|
|
206
384
|
### Core Features
|
|
385
|
+
- [Explainability Layer](docs/EXPLAINABILITY.md) - Machine-readable decision explanations with condition-level tracing
|
|
207
386
|
- [Advanced Operators](docs/ADVANCED_OPERATORS.md) - String, numeric, date/time, collection, and geospatial operators
|
|
208
387
|
- [DMN Guide](docs/DMN_GUIDE.md) - Complete DMN 1.3 support guide
|
|
209
388
|
- [DMN API Reference](docs/DMN_API.md) - DMN API documentation
|
|
@@ -211,16 +390,25 @@ See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complet
|
|
|
211
390
|
- [DMN Migration Guide](docs/DMN_MIGRATION_GUIDE.md) - Migrating from JSON to DMN
|
|
212
391
|
- [DMN Best Practices](docs/DMN_BEST_PRACTICES.md) - DMN modeling best practices
|
|
213
392
|
- [Versioning System](docs/VERSIONING.md) - Version control for rules
|
|
393
|
+
- [Simulation & What-If Analysis](docs/SIMULATION.md) - Historical replay, what-if analysis, impact analysis, and shadow testing
|
|
214
394
|
- [A/B Testing](docs/AB_TESTING.md) - Compare rule versions with statistical analysis
|
|
395
|
+
- [Batch Testing](docs/BATCH_TESTING.md) - Test rules against large datasets with CSV/Excel import
|
|
396
|
+
- [RBAC Configuration](docs/RBAC_CONFIGURATION.md) - Role-based access control setup and integration
|
|
397
|
+
- [RBAC Quick Reference](docs/RBAC_QUICK_REFERENCE.md) - Quick reference for RBAC configuration
|
|
215
398
|
- [Web UI](docs/WEB_UI.md) - Visual rule builder
|
|
216
399
|
- [Web UI Setup](docs/WEB_UI_SETUP.md) - Setup guide
|
|
217
|
-
- [Web UI
|
|
400
|
+
- [Web UI Integration](docs/WEB_UI_INTEGRATION.md) - Mount in Rails, Sinatra, Hanami, and other Rack frameworks
|
|
218
401
|
- [Monitoring & Analytics](docs/MONITORING_AND_ANALYTICS.md) - Real-time monitoring, metrics, and alerting
|
|
219
402
|
- [Monitoring Architecture](docs/MONITORING_ARCHITECTURE.md) - System architecture and design
|
|
403
|
+
- [Persistent Monitoring](docs/PERSISTENT_MONITORING.md) - Database storage for long-term analytics
|
|
220
404
|
|
|
221
405
|
### Performance & Thread-Safety
|
|
222
406
|
- [Performance & Thread-Safety Summary](docs/PERFORMANCE_AND_THREAD_SAFETY.md) - Benchmarks and production readiness
|
|
223
407
|
- [Thread-Safety Implementation](docs/THREAD_SAFETY.md) - Technical implementation guide
|
|
408
|
+
- [Benchmarks](benchmarks/README.md) - Comprehensive benchmark suite and performance testing
|
|
409
|
+
|
|
410
|
+
### Development
|
|
411
|
+
- [Development Setup](docs/DEVELOPMENT_SETUP.md) - Development environment setup, testing, and tools
|
|
224
412
|
|
|
225
413
|
### Reference
|
|
226
414
|
- [API Contract](docs/API_CONTRACT.md) - Full API reference
|
|
@@ -244,12 +432,44 @@ All data structures are deeply frozen to prevent mutation, ensuring safe concurr
|
|
|
244
432
|
|
|
245
433
|
See [Thread-Safety Guide](docs/THREAD_SAFETY.md) and [Performance Analysis](docs/PERFORMANCE_AND_THREAD_SAFETY.md) for details.
|
|
246
434
|
|
|
435
|
+
**Run Benchmarks:**
|
|
436
|
+
```bash
|
|
437
|
+
# Run all benchmarks (single Ruby version)
|
|
438
|
+
rake benchmark:all
|
|
439
|
+
|
|
440
|
+
# Run specific benchmarks
|
|
441
|
+
rake benchmark:basic # Basic decision performance
|
|
442
|
+
rake benchmark:threads # Thread-safety and scalability
|
|
443
|
+
rake benchmark:regression # Compare against baseline
|
|
444
|
+
|
|
445
|
+
# Run benchmarks across all Ruby versions (3.0.7, 3.1.6, 3.2.5, 3.3.5)
|
|
446
|
+
./scripts/benchmark_all_ruby_versions.sh
|
|
447
|
+
|
|
448
|
+
# See [Benchmarks Guide](benchmarks/README.md) for complete documentation
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Latest Benchmark Results
|
|
452
|
+
|
|
453
|
+
Run `rake benchmark:regression` to generate results for your environment. Example (Ruby 3.3, typical hardware):
|
|
454
|
+
|
|
455
|
+
| Metric | Typical range |
|
|
456
|
+
|--------|----------------|
|
|
457
|
+
| Basic throughput | ~9,000+ decisions/sec |
|
|
458
|
+
| Basic latency | ~0.1 ms |
|
|
459
|
+
| Multi-threaded (50 threads) | ~8,500+ decisions/sec |
|
|
460
|
+
|
|
461
|
+
> 💡 **Note:** See [Benchmarks Guide](benchmarks/README.md) and run `rake benchmark:all` or `rake benchmark:regression` for current numbers.
|
|
247
462
|
## Contributing
|
|
248
463
|
|
|
249
464
|
1. Fork the repository
|
|
250
465
|
2. Create a feature branch
|
|
251
|
-
3.
|
|
252
|
-
4.
|
|
466
|
+
3. Set up development environment (see [Development Setup](docs/DEVELOPMENT_SETUP.md))
|
|
467
|
+
4. Add tests (maintain 90%+ coverage)
|
|
468
|
+
5. Run tests across all Ruby versions: `./scripts/test_all_ruby_versions.sh`
|
|
469
|
+
6. Run benchmarks across all Ruby versions: `./scripts/benchmark_all_ruby_versions.sh`
|
|
470
|
+
7. Submit a pull request
|
|
471
|
+
|
|
472
|
+
See [Development Setup Guide](docs/DEVELOPMENT_SETUP.md) for detailed setup instructions, testing workflows, and development best practices.
|
|
253
473
|
|
|
254
474
|
## Support
|
|
255
475
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
1
5
|
module DecisionAgent
|
|
2
6
|
module ABTesting
|
|
3
7
|
# Represents an A/B test configuration for comparing rule versions
|
|
@@ -41,7 +45,7 @@ module DecisionAgent
|
|
|
41
45
|
|
|
42
46
|
if user_id
|
|
43
47
|
# Consistent hashing: same user always gets same variant
|
|
44
|
-
hash_value = Digest::SHA256.hexdigest("#{@id}:#{user_id}").to_i(16)
|
|
48
|
+
hash_value = OpenSSL::Digest::SHA256.hexdigest("#{@id}:#{user_id}").to_i(16)
|
|
45
49
|
percentage = hash_value % 100
|
|
46
50
|
else
|
|
47
51
|
# Random assignment
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_record"
|
|
2
4
|
|
|
3
5
|
module DecisionAgent
|
|
@@ -86,19 +88,6 @@ module DecisionAgent
|
|
|
86
88
|
end
|
|
87
89
|
# rubocop:enable Naming/PredicateMethod
|
|
88
90
|
|
|
89
|
-
# Get statistics from database
|
|
90
|
-
def get_test_statistics(test_id)
|
|
91
|
-
assignments = ::ABTestAssignmentModel.where(ab_test_id: test_id)
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
total_assignments: assignments.count,
|
|
95
|
-
champion_count: assignments.where(variant: "champion").count,
|
|
96
|
-
challenger_count: assignments.where(variant: "challenger").count,
|
|
97
|
-
with_decisions: assignments.where.not(decision_result: nil).count,
|
|
98
|
-
avg_confidence: assignments.where.not(confidence: nil).average(:confidence)&.to_f
|
|
99
|
-
}
|
|
100
|
-
end
|
|
101
|
-
|
|
102
91
|
private
|
|
103
92
|
|
|
104
93
|
def to_ab_test(record)
|
data/lib/decision_agent/agent.rb
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "json"
|
|
3
4
|
require "json/canonicalization"
|
|
5
|
+
require "openssl"
|
|
4
6
|
|
|
5
7
|
module DecisionAgent
|
|
8
|
+
# Agent runs multiple evaluators over a context, scores their evaluations,
|
|
9
|
+
# and returns a single {Decision} with the chosen outcome and confidence.
|
|
6
10
|
class Agent
|
|
7
11
|
attr_reader :evaluators, :scoring_strategy, :audit_adapter
|
|
8
12
|
|
|
13
|
+
# Thread-safe cache for deterministic hash computation
|
|
14
|
+
# This significantly improves performance when the same context/evaluations
|
|
15
|
+
# are processed multiple times (common in benchmarks and high-throughput scenarios)
|
|
16
|
+
@hash_cache = {}
|
|
17
|
+
@hash_cache_mutex = Mutex.new
|
|
18
|
+
@hash_cache_max_size = 1000 # Limit cache size to prevent memory bloat
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
attr_reader :hash_cache, :hash_cache_mutex, :hash_cache_max_size
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param evaluators [Array<#evaluate>] Objects that respond to #evaluate(context, feedback:)
|
|
25
|
+
# @param scoring_strategy [Scoring::Base, nil] Strategy to score evaluations (default: WeightedAverage)
|
|
26
|
+
# @param audit_adapter [Audit::Base, nil] Adapter for recording decisions (default: NullAdapter)
|
|
27
|
+
# @param validate_evaluations [Boolean, nil] If true, validate evaluations; nil = validate unless production
|
|
9
28
|
def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil, validate_evaluations: nil)
|
|
10
29
|
@evaluators = Array(evaluators)
|
|
11
30
|
@scoring_strategy = scoring_strategy || Scoring::WeightedAverage.new
|
|
@@ -19,6 +38,12 @@ module DecisionAgent
|
|
|
19
38
|
@evaluators.freeze
|
|
20
39
|
end
|
|
21
40
|
|
|
41
|
+
# Runs all evaluators on the context, scores results, and returns a single decision.
|
|
42
|
+
#
|
|
43
|
+
# @param context [Context, Hash] Input data; converted to {Context} if a Hash
|
|
44
|
+
# @param feedback [Hash] Optional feedback passed to each evaluator
|
|
45
|
+
# @return [Decision] The chosen decision with confidence, explanations, and audit payload
|
|
46
|
+
# @raise [NoEvaluationsError] when no evaluator returns a valid evaluation
|
|
22
47
|
def decide(context:, feedback: {})
|
|
23
48
|
ctx = context.is_a?(Context) ? context : Context.new(context)
|
|
24
49
|
|
|
@@ -76,7 +101,8 @@ module DecisionAgent
|
|
|
76
101
|
def collect_evaluations(context, feedback)
|
|
77
102
|
@evaluators.map do |evaluator|
|
|
78
103
|
evaluator.evaluate(context, feedback: feedback)
|
|
79
|
-
rescue StandardError
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
warn "[DecisionAgent] Evaluator #{evaluator.class} failed: #{e.message}"
|
|
80
106
|
nil
|
|
81
107
|
end.compact
|
|
82
108
|
end
|
|
@@ -89,20 +115,20 @@ module DecisionAgent
|
|
|
89
115
|
explanations << "Decision: #{final_decision} (confidence: #{confidence.round(2)})"
|
|
90
116
|
|
|
91
117
|
if matching_evals.size == 1
|
|
92
|
-
|
|
93
|
-
explanations << "#{
|
|
118
|
+
evaluation = matching_evals.first
|
|
119
|
+
explanations << "#{evaluation.evaluator_name}: #{evaluation.reason} (weight: #{evaluation.weight})"
|
|
94
120
|
elsif matching_evals.size > 1
|
|
95
121
|
explanations << "Based on #{matching_evals.size} evaluators:"
|
|
96
|
-
matching_evals.each do |
|
|
97
|
-
explanations << " - #{
|
|
122
|
+
matching_evals.each do |evaluation|
|
|
123
|
+
explanations << " - #{evaluation.evaluator_name}: #{evaluation.reason} (weight: #{evaluation.weight})"
|
|
98
124
|
end
|
|
99
125
|
end
|
|
100
126
|
|
|
101
127
|
conflicting_evals = evaluations.reject { |e| e.decision == final_decision }
|
|
102
128
|
if conflicting_evals.any?
|
|
103
129
|
explanations << "Conflicting evaluations resolved by #{@scoring_strategy.class.name.split('::').last}:"
|
|
104
|
-
conflicting_evals.each do |
|
|
105
|
-
explanations << " - #{
|
|
130
|
+
conflicting_evals.each do |evaluation|
|
|
131
|
+
explanations << " - #{evaluation.evaluator_name}: suggested '#{evaluation.decision}' (weight: #{evaluation.weight})"
|
|
106
132
|
end
|
|
107
133
|
end
|
|
108
134
|
|
|
@@ -129,8 +155,51 @@ module DecisionAgent
|
|
|
129
155
|
|
|
130
156
|
def compute_deterministic_hash(payload)
|
|
131
157
|
hashable = payload.slice(:context, :evaluations, :decision, :confidence, :scoring_strategy)
|
|
158
|
+
|
|
159
|
+
# Use fast hash (MD5) as cache key to avoid expensive canonicalization on cache hits
|
|
160
|
+
# The cache key doesn't need perfect determinism, just good enough to catch duplicates
|
|
161
|
+
# Use OpenSSL::Digest to avoid "Digest::Base cannot be directly inherited" on some Ruby/digest setups
|
|
162
|
+
json_str = hashable.to_json
|
|
163
|
+
fast_key = OpenSSL::Digest::MD5.hexdigest(json_str)
|
|
164
|
+
|
|
165
|
+
# Fast path: check cache without lock first (unsafe read, but acceptable for cache)
|
|
166
|
+
cached_hash = lookup_cached_hash(fast_key)
|
|
167
|
+
return cached_hash if cached_hash
|
|
168
|
+
|
|
169
|
+
# Cache miss - compute canonical JSON and hash
|
|
170
|
+
computed_hash = compute_canonical_hash(hashable)
|
|
171
|
+
|
|
172
|
+
# Store in cache (thread-safe, with size limit)
|
|
173
|
+
cache_hash(fast_key, computed_hash)
|
|
174
|
+
|
|
175
|
+
computed_hash
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def lookup_cached_hash(fast_key)
|
|
179
|
+
self.class.hash_cache[fast_key]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def compute_canonical_hash(hashable)
|
|
132
183
|
canonical = canonical_json(hashable)
|
|
133
|
-
Digest::SHA256.hexdigest(canonical)
|
|
184
|
+
OpenSSL::Digest::SHA256.hexdigest(canonical)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def cache_hash(fast_key, computed_hash)
|
|
188
|
+
self.class.hash_cache_mutex.synchronize do
|
|
189
|
+
# Double-check after acquiring lock (another thread may have added it)
|
|
190
|
+
return self.class.hash_cache[fast_key] if self.class.hash_cache[fast_key]
|
|
191
|
+
|
|
192
|
+
evict_cache_if_needed
|
|
193
|
+
self.class.hash_cache[fast_key] = computed_hash
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def evict_cache_if_needed
|
|
198
|
+
return unless self.class.hash_cache.size >= self.class.hash_cache_max_size
|
|
199
|
+
|
|
200
|
+
# Remove oldest 10% of entries (simple FIFO eviction)
|
|
201
|
+
keys_to_remove = self.class.hash_cache.keys.first(self.class.hash_cache_max_size / 10)
|
|
202
|
+
keys_to_remove.each { |key| self.class.hash_cache.delete(key) }
|
|
134
203
|
end
|
|
135
204
|
|
|
136
205
|
# Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
|