decision_agent 1.0.1 → 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.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +61 -106
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +49 -51
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +13 -0
  27. data/lib/decision_agent/decision.rb +11 -2
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +102 -108
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +197 -1473
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +9 -24
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +2 -0
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +2 -17
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +2 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +2 -4
  81. data/lib/decision_agent/explainability/rule_trace.rb +2 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +2 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +2 -15
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +2 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +3 -1
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +11 -8
  97. data/lib/decision_agent/simulation/replay_engine.rb +3 -1
  98. data/lib/decision_agent/simulation/scenario_engine.rb +3 -1
  99. data/lib/decision_agent/simulation/scenario_library.rb +2 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +3 -16
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +17 -11
  102. data/lib/decision_agent/simulation.rb +2 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +97 -47
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +67 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +20 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +23 -7
  131. data/lib/decision_agent/web/public/simulation_impact.html +37 -20
  132. data/lib/decision_agent/web/public/simulation_replay.html +19 -23
  133. data/lib/decision_agent/web/public/simulation_shadow.html +36 -21
  134. data/lib/decision_agent/web/public/simulation_whatif.html +38 -21
  135. data/lib/decision_agent/web/public/users.html +1 -1
  136. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  137. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  138. data/lib/decision_agent/web/server.rb +2032 -1851
  139. data/lib/decision_agent.rb +3 -43
  140. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  141. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  147. metadata +57 -23
  148. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +0 -86
  149. data/lib/decision_agent/data_enrichment/cache_adapter.rb +0 -49
  150. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +0 -135
  151. data/lib/decision_agent/data_enrichment/client.rb +0 -220
  152. data/lib/decision_agent/data_enrichment/config.rb +0 -78
  153. data/lib/decision_agent/data_enrichment/errors.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a016bb964d8daeb5676d84ba597f07af9e7b4816a8f075d3ca9feb6f1ddd2f44
4
- data.tar.gz: ccc960d23a5c863b8e9be429b08188f32abe630d83e2008a3e70534401779d92
3
+ metadata.gz: c6c760652b22f7cf3e4524aec1757f1fba1fc21c206c960ad3836eed18279f13
4
+ data.tar.gz: e0c2f21dfac36d7a4ce7935d6ea38375265c64e15eec4c276c514a1bf759dd6e
5
5
  SHA512:
6
- metadata.gz: 4f84301ecb298b3fdc3d39b2ca02e850345d2df13eb0c8834555f1b3f08241f243dee54fd044dfd0a37bd2fa32e690dca7583c69049af68d0e951572ca222ad0
7
- data.tar.gz: a52598d8c6358ea03ed16a3fd23d68fbea1ef9619f1c4dec613359427a11e9290cd6f4b9f8816fb6673d436b59bdd23b986f816a23ecac185eae1e27b86e6dee
6
+ metadata.gz: d9a402f62fa86a4cf83d727f048b6de8a8d67ab317cbc3dd2d681cd537a2a1a68a1bbe77b2f34a4c6f4a805babace2e7c6d449435d8fef9831dff884152b441a
7
+ data.tar.gz: 2b00ae31f1e89caa62c004c6e649296e1c6a193faf90ded7ef2fd6e930b3c5547113d028349de2b6904bfb9e2371fd37335251b6391bac4315cda1ca6407c4e8
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/decision_agent.svg)](https://badge.fury.io/rb/decision_agent)
4
4
  [![CI](https://github.com/samaswin/decision_agent/actions/workflows/ci.yml/badge.svg)](https://github.com/samaswin/decision_agent/actions/workflows/ci.yml)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7.0-red.svg)](https://www.ruby-lang.org)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-red.svg)](https://www.ruby-lang.org)
7
7
 
8
8
  A production-grade, deterministic, explainable, and auditable decision engine for Ruby.
9
9
 
@@ -72,7 +72,6 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
72
72
  - **Conflict Resolution** - Weighted average, consensus, threshold, max weight
73
73
  - **Rich Context** - Nested data, dot notation, flexible operators
74
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
76
75
 
77
76
  ### Auditability & Compliance
78
77
  - **Complete Audit Trails** - Every decision fully logged
@@ -105,7 +104,7 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
105
104
 
106
105
  ### Developer Experience
107
106
  - **Pluggable Architecture** - Custom evaluators, scoring, audit adapters
108
- - **Framework Agnostic** - Works with Rails, Sinatra, or standalone
107
+ - **Framework Agnostic** - Works with Rails, Rack, or standalone
109
108
  - **JSON Rule DSL** - Non-technical users can write rules
110
109
  - **DMN 1.3 Support** - Industry-standard Decision Model and Notation with full FEEL expression language
111
110
  - **Visual Rule Builder** - Web UI for rule management and DMN modeler
@@ -131,6 +130,8 @@ decision_agent web
131
130
 
132
131
  Open [http://localhost:4567](http://localhost:4567) in your browser.
133
132
 
133
+ The Web UI includes a **DMN visual modeler** at `/dmn/editor` for building and editing decision tables.
134
+
134
135
  ### Integration
135
136
 
136
137
  **Rails:**
@@ -141,7 +142,7 @@ Rails.application.routes.draw do
141
142
  end
142
143
  ```
143
144
 
144
- **Rack/Sinatra:**
145
+ **Rack:**
145
146
  ```ruby
146
147
  require 'decision_agent/web/server'
147
148
  map '/decision_agent' do
@@ -149,7 +150,7 @@ map '/decision_agent' do
149
150
  end
150
151
  ```
151
152
 
152
- See [Web UI Integration Guide](docs/WEB_UI_RAILS_INTEGRATION.md) for detailed setup.
153
+ See [Web UI Integration Guide](docs/WEB_UI_INTEGRATION.md) for detailed setup with Rails, Sinatra, Hanami, and other frameworks.
153
154
 
154
155
  ## DMN (Decision Model and Notation) Support
155
156
 
@@ -185,57 +186,6 @@ result = agent.decide(context: { amount: 50000, credit_score: 750 })
185
186
 
186
187
  See [DMN Guide](docs/DMN_GUIDE.md) for complete documentation and [DMN Examples](examples/dmn/README.md) for working examples.
187
188
 
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
-
239
189
  ## Monitoring & Analytics
240
190
 
241
191
  Real-time monitoring, metrics, and alerting for production environments.
@@ -268,31 +218,32 @@ DecisionAgent provides comprehensive simulation capabilities to test rule change
268
218
 
269
219
  ```ruby
270
220
  require 'decision_agent/simulation/replay_engine'
221
+ require 'decision_agent/simulation/what_if_analyzer'
222
+ require 'decision_agent/simulation/impact_analyzer'
271
223
 
272
224
  # Replay historical decisions with new rules
273
225
  replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
274
226
  agent: agent,
275
227
  version_manager: version_manager
276
228
  )
277
-
278
229
  results = replay_engine.replay(historical_data: "decisions.csv")
279
230
 
280
- # What-if analysis
231
+ # What-if analysis (scenarios = array of context hashes)
281
232
  whatif = DecisionAgent::Simulation::WhatIfAnalyzer.new(agent: agent)
282
233
  analysis = whatif.analyze(
283
- base_context: { credit_score: 700, amount: 50000 },
284
234
  scenarios: [
285
- { credit_score: 750 },
286
- { credit_score: 650 }
235
+ { credit_score: 700, amount: 50000 },
236
+ { credit_score: 750, amount: 50000 },
237
+ { credit_score: 650, amount: 50000 }
287
238
  ]
288
239
  )
289
240
 
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
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
296
247
  )
297
248
  ```
298
249
 
@@ -348,23 +299,22 @@ Test rules against large datasets with comprehensive analysis:
348
299
 
349
300
  ```ruby
350
301
  require 'decision_agent/testing/batch_test_runner'
302
+ require 'decision_agent/testing/batch_test_importer'
351
303
 
352
- runner = DecisionAgent::Testing::BatchTestRunner.new(agent: agent)
304
+ runner = DecisionAgent::Testing::BatchTestRunner.new(agent)
353
305
 
354
- # Import from CSV or Excel
306
+ # Import from CSV or Excel (context columns default to all except id/expected_*)
355
307
  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
- })
308
+ scenarios = importer.import_csv("test_data.csv")
360
309
 
361
310
  # Run batch test
362
- results = runner.run(scenarios: scenarios)
311
+ results = runner.run(scenarios)
363
312
 
364
- puts "Total: #{results[:total]}"
365
- puts "Passed: #{results[:passed]}"
366
- puts "Failed: #{results[:failed]}"
367
- puts "Coverage: #{results[:coverage]}"
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)}%"
368
318
  ```
369
319
 
370
320
  **Features:**
@@ -382,19 +332,23 @@ See [Batch Testing Guide](docs/BATCH_TESTING.md) for complete documentation.
382
332
  Compare rule versions with statistical analysis:
383
333
 
384
334
  ```ruby
385
- require 'decision_agent/testing/ab_test_manager'
335
+ require 'decision_agent/ab_testing/ab_test_manager'
386
336
 
387
- ab_manager = DecisionAgent::Testing::AbTestManager.new(version_manager: version_manager)
337
+ ab_manager = DecisionAgent::ABTesting::ABTestManager.new(version_manager: version_manager)
388
338
 
389
339
  test = ab_manager.create_test(
390
340
  name: "loan_approval_v2",
391
- champion_version: champion_version_id,
392
- challenger_version: challenger_version_id,
393
- traffic_split: 0.5
341
+ champion_version_id: champion_version_id,
342
+ challenger_version_id: challenger_version_id,
343
+ traffic_split: { champion: 90, challenger: 10 }
394
344
  )
395
345
 
396
- results = ab_manager.run_test(test_id: test.id, contexts: test_contexts)
397
- ab_manager.analyze_results(test_id: test.id)
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)
398
352
  ```
399
353
 
400
354
  **Features:**
@@ -430,7 +384,6 @@ See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
430
384
  ### Core Features
431
385
  - [Explainability Layer](docs/EXPLAINABILITY.md) - Machine-readable decision explanations with condition-level tracing
432
386
  - [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
387
  - [DMN Guide](docs/DMN_GUIDE.md) - Complete DMN 1.3 support guide
435
388
  - [DMN API Reference](docs/DMN_API.md) - DMN API documentation
436
389
  - [FEEL Reference](docs/FEEL_REFERENCE.md) - FEEL expression language reference
@@ -444,7 +397,7 @@ See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
444
397
  - [RBAC Quick Reference](docs/RBAC_QUICK_REFERENCE.md) - Quick reference for RBAC configuration
445
398
  - [Web UI](docs/WEB_UI.md) - Visual rule builder
446
399
  - [Web UI Setup](docs/WEB_UI_SETUP.md) - Setup guide
447
- - [Web UI Rails Integration](docs/WEB_UI_RAILS_INTEGRATION.md) - Mount in Rails/Rack apps
400
+ - [Web UI Integration](docs/WEB_UI_INTEGRATION.md) - Mount in Rails, Sinatra, Hanami, and other Rack frameworks
448
401
  - [Monitoring & Analytics](docs/MONITORING_AND_ANALYTICS.md) - Real-time monitoring, metrics, and alerting
449
402
  - [Monitoring Architecture](docs/MONITORING_ARCHITECTURE.md) - System architecture and design
450
403
  - [Persistent Monitoring](docs/PERSISTENT_MONITORING.md) - Database storage for long-term analytics
@@ -454,6 +407,9 @@ See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
454
407
  - [Thread-Safety Implementation](docs/THREAD_SAFETY.md) - Technical implementation guide
455
408
  - [Benchmarks](benchmarks/README.md) - Comprehensive benchmark suite and performance testing
456
409
 
410
+ ### Development
411
+ - [Development Setup](docs/DEVELOPMENT_SETUP.md) - Development environment setup, testing, and tools
412
+
457
413
  ### Reference
458
414
  - [API Contract](docs/API_CONTRACT.md) - Full API reference
459
415
  - [Changelog](docs/CHANGELOG.md) - Version history
@@ -478,7 +434,7 @@ See [Thread-Safety Guide](docs/THREAD_SAFETY.md) and [Performance Analysis](docs
478
434
 
479
435
  **Run Benchmarks:**
480
436
  ```bash
481
- # Run all benchmarks
437
+ # Run all benchmarks (single Ruby version)
482
438
  rake benchmark:all
483
439
 
484
440
  # Run specific benchmarks
@@ -486,35 +442,34 @@ rake benchmark:basic # Basic decision performance
486
442
  rake benchmark:threads # Thread-safety and scalability
487
443
  rake benchmark:regression # Compare against baseline
488
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
+
489
448
  # See [Benchmarks Guide](benchmarks/README.md) for complete documentation
490
449
  ```
491
450
 
492
451
  ### Latest Benchmark Results
493
452
 
494
- **Last Updated:** 2026-01-06T04:03:29Z
495
-
496
- #### Performance Comparison
453
+ Run `rake benchmark:regression` to generate results for your environment. Example (Ruby 3.3, typical hardware):
497
454
 
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) |
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 |
504
460
 
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.
461
+ > 💡 **Note:** See [Benchmarks Guide](benchmarks/README.md) and run `rake benchmark:all` or `rake benchmark:regression` for current numbers.
512
462
  ## Contributing
513
463
 
514
464
  1. Fork the repository
515
465
  2. Create a feature branch
516
- 3. Add tests (maintain 90%+ coverage)
517
- 4. Submit a pull request
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.
518
473
 
519
474
  ## Support
520
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
  module DecisionAgent
2
4
  module ABTesting
3
5
  # Tracks individual assignments of users/requests to A/B test variants
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
4
 
3
5
  module DecisionAgent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module ABTesting
3
5
  # Agent wrapper that adds A/B testing capabilities to the standard Agent
@@ -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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module ABTesting
3
5
  module Storage
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
4
 
3
5
  module DecisionAgent
@@ -1,8 +1,12 @@
1
- require "digest"
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
 
@@ -17,6 +21,10 @@ module DecisionAgent
17
21
  attr_reader :hash_cache, :hash_cache_mutex, :hash_cache_max_size
18
22
  end
19
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
20
28
  def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil, validate_evaluations: nil)
21
29
  @evaluators = Array(evaluators)
22
30
  @scoring_strategy = scoring_strategy || Scoring::WeightedAverage.new
@@ -30,6 +38,12 @@ module DecisionAgent
30
38
  @evaluators.freeze
31
39
  end
32
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
33
47
  def decide(context:, feedback: {})
34
48
  ctx = context.is_a?(Context) ? context : Context.new(context)
35
49
 
@@ -87,7 +101,8 @@ module DecisionAgent
87
101
  def collect_evaluations(context, feedback)
88
102
  @evaluators.map do |evaluator|
89
103
  evaluator.evaluate(context, feedback: feedback)
90
- rescue StandardError
104
+ rescue StandardError => e
105
+ warn "[DecisionAgent] Evaluator #{evaluator.class} failed: #{e.message}"
91
106
  nil
92
107
  end.compact
93
108
  end
@@ -100,20 +115,20 @@ module DecisionAgent
100
115
  explanations << "Decision: #{final_decision} (confidence: #{confidence.round(2)})"
101
116
 
102
117
  if matching_evals.size == 1
103
- eval = matching_evals.first
104
- explanations << "#{eval.evaluator_name}: #{eval.reason} (weight: #{eval.weight})"
118
+ evaluation = matching_evals.first
119
+ explanations << "#{evaluation.evaluator_name}: #{evaluation.reason} (weight: #{evaluation.weight})"
105
120
  elsif matching_evals.size > 1
106
121
  explanations << "Based on #{matching_evals.size} evaluators:"
107
- matching_evals.each do |eval|
108
- explanations << " - #{eval.evaluator_name}: #{eval.reason} (weight: #{eval.weight})"
122
+ matching_evals.each do |evaluation|
123
+ explanations << " - #{evaluation.evaluator_name}: #{evaluation.reason} (weight: #{evaluation.weight})"
109
124
  end
110
125
  end
111
126
 
112
127
  conflicting_evals = evaluations.reject { |e| e.decision == final_decision }
113
128
  if conflicting_evals.any?
114
129
  explanations << "Conflicting evaluations resolved by #{@scoring_strategy.class.name.split('::').last}:"
115
- conflicting_evals.each do |eval|
116
- explanations << " - #{eval.evaluator_name}: suggested '#{eval.decision}' (weight: #{eval.weight})"
130
+ conflicting_evals.each do |evaluation|
131
+ explanations << " - #{evaluation.evaluator_name}: suggested '#{evaluation.decision}' (weight: #{evaluation.weight})"
117
132
  end
118
133
  end
119
134
 
@@ -142,66 +157,49 @@ module DecisionAgent
142
157
  hashable = payload.slice(:context, :evaluations, :decision, :confidence, :scoring_strategy)
143
158
 
144
159
  # 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
160
  # 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
161
+ # Use OpenSSL::Digest to avoid "Digest::Base cannot be directly inherited" on some Ruby/digest setups
148
162
  json_str = hashable.to_json
149
- fast_key = Digest::MD5.hexdigest(json_str)
163
+ fast_key = OpenSSL::Digest::MD5.hexdigest(json_str)
150
164
 
151
165
  # 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]
166
+ cached_hash = lookup_cached_hash(fast_key)
155
167
  return cached_hash if cached_hash
156
168
 
157
- # Cache miss - compute canonical JSON (required for deterministic hashing)
158
- # This is expensive, but only happens on cache misses
159
- canonical = canonical_json(hashable)
160
-
161
- # Compute SHA256 hash (also expensive, but only on cache misses)
162
- computed_hash = Digest::SHA256.hexdigest(canonical)
169
+ # Cache miss - compute canonical JSON and hash
170
+ computed_hash = compute_canonical_hash(hashable)
163
171
 
164
172
  # Store in cache (thread-safe, with size limit)
165
- # Only lock when we need to write
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)
183
+ canonical = canonical_json(hashable)
184
+ OpenSSL::Digest::SHA256.hexdigest(canonical)
185
+ end
186
+
187
+ def cache_hash(fast_key, computed_hash)
166
188
  self.class.hash_cache_mutex.synchronize do
167
189
  # Double-check after acquiring lock (another thread may have added it)
168
190
  return self.class.hash_cache[fast_key] if self.class.hash_cache[fast_key]
169
191
 
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
192
+ evict_cache_if_needed
176
193
  self.class.hash_cache[fast_key] = computed_hash
177
194
  end
178
-
179
- computed_hash
180
195
  end
181
196
 
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
197
+ def evict_cache_if_needed
198
+ return unless self.class.hash_cache.size >= self.class.hash_cache_max_size
191
199
 
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
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) }
205
203
  end
206
204
 
207
205
  # Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Audit
3
5
  class Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logger"
2
4
  require "json"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Audit
3
5
  class NullAdapter < Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require_relative "../audit/adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class Authenticator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class PasswordResetManager
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
 
3
5
  module DecisionAgent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class Permission
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class PermissionChecker
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  # Base adapter interface for RBAC integration
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  # Configuration class for RBAC adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class Role
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
 
3
5
  module DecisionAgent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Auth
3
5
  class SessionManager