decision_agent 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -12,6 +12,10 @@ require_relative "../testing/test_coverage_analyzer"
12
12
  require_relative "../evaluators/json_rule_evaluator"
13
13
  require_relative "../agent"
14
14
 
15
+ # DMN components
16
+ require_relative "../dmn/importer"
17
+ require_relative "../dmn/exporter"
18
+
15
19
  # Auth components
16
20
  require_relative "../auth/user"
17
21
  require_relative "../auth/role"
@@ -24,6 +28,15 @@ require_relative "../auth/access_audit_logger"
24
28
  require_relative "middleware/auth_middleware"
25
29
  require_relative "middleware/permission_middleware"
26
30
 
31
+ # Simulation components
32
+ require_relative "../simulation/replay_engine"
33
+ require_relative "../simulation/what_if_analyzer"
34
+ require_relative "../simulation/impact_analyzer"
35
+ require_relative "../simulation/shadow_test_engine"
36
+ require_relative "../simulation/scenario_engine"
37
+ require_relative "../simulation/scenario_library"
38
+ require_relative "../versioning/version_manager"
39
+
27
40
  module DecisionAgent
28
41
  module Web
29
42
  # rubocop:disable Metrics/ClassLength
@@ -37,25 +50,27 @@ module DecisionAgent
37
50
  @batch_test_storage = {}
38
51
  @batch_test_storage_mutex = Mutex.new
39
52
 
53
+ # In-memory storage for simulation runs
54
+ @simulation_storage = {}
55
+ @simulation_storage_mutex = Mutex.new
56
+
40
57
  # Auth components
41
58
  @authenticator = nil
42
59
  @permission_checker = nil
43
60
  @access_audit_logger = nil
44
-
45
- def self.batch_test_storage
46
- @batch_test_storage ||= {}
47
- end
48
-
49
- def self.batch_test_storage_mutex
50
- @batch_test_storage_mutex ||= Mutex.new
51
- end
61
+ @auth_mutex = Mutex.new
52
62
 
53
63
  class << self
64
+ attr_reader :batch_test_storage, :batch_test_storage_mutex, :simulation_storage, :simulation_storage_mutex
54
65
  attr_writer :authenticator
55
66
  end
56
67
 
57
68
  def self.authenticator
58
- @authenticator ||= Auth::Authenticator.new
69
+ return @authenticator if @authenticator
70
+
71
+ @auth_mutex.synchronize do
72
+ @authenticator ||= Auth::Authenticator.new
73
+ end
59
74
  end
60
75
 
61
76
  class << self
@@ -63,7 +78,11 @@ module DecisionAgent
63
78
  end
64
79
 
65
80
  def self.permission_checker
66
- @permission_checker ||= Auth::PermissionChecker.new(adapter: DecisionAgent.rbac_config.adapter)
81
+ return @permission_checker if @permission_checker
82
+
83
+ @auth_mutex.synchronize do
84
+ @permission_checker ||= Auth::PermissionChecker.new(adapter: DecisionAgent.rbac_config.adapter)
85
+ end
67
86
  end
68
87
 
69
88
  class << self
@@ -71,7 +90,11 @@ module DecisionAgent
71
90
  end
72
91
 
73
92
  def self.access_audit_logger
74
- @access_audit_logger ||= Auth::AccessAuditLogger.new
93
+ return @access_audit_logger if @access_audit_logger
94
+
95
+ @auth_mutex.synchronize do
96
+ @access_audit_logger ||= Auth::AccessAuditLogger.new
97
+ end
75
98
  end
76
99
 
77
100
  # Enable CORS for API calls
@@ -200,19 +223,55 @@ module DecisionAgent
200
223
  result = evaluator.evaluate(DecisionAgent::Context.new(context))
201
224
 
202
225
  if result
203
- {
204
- success: true,
205
- decision: result.decision,
206
- weight: result.weight,
207
- reason: result.reason,
208
- evaluator_name: result.evaluator_name,
209
- metadata: result.metadata
210
- }.to_json
226
+ # Get explainability data from metadata if available
227
+ explainability = result.metadata[:explainability] if result.metadata.is_a?(Hash)
228
+
229
+ # Structure response as explainability by default
230
+ # This makes explainability the primary format for decision results
231
+ response = if explainability
232
+ {
233
+ success: true,
234
+ decision: explainability[:decision] || result.decision,
235
+ because: explainability[:because] || [],
236
+ failed_conditions: explainability[:failed_conditions] || [],
237
+ # Include additional metadata for completeness
238
+ confidence: result.weight,
239
+ reason: result.reason,
240
+ evaluator_name: result.evaluator_name,
241
+ # Full explainability data (includes rule_traces in verbose mode)
242
+ explainability: explainability
243
+ }
244
+ else
245
+ # Fallback if explainability is not available
246
+ {
247
+ success: true,
248
+ decision: result.decision,
249
+ because: [],
250
+ failed_conditions: [],
251
+ confidence: result.weight,
252
+ reason: result.reason,
253
+ evaluator_name: result.evaluator_name,
254
+ explainability: {
255
+ decision: result.decision,
256
+ because: [],
257
+ failed_conditions: []
258
+ }
259
+ }
260
+ end
261
+
262
+ response.to_json
211
263
  else
212
264
  {
213
265
  success: true,
214
266
  decision: nil,
215
- message: "No rules matched the given context"
267
+ because: [],
268
+ failed_conditions: [],
269
+ message: "No rules matched the given context",
270
+ explainability: {
271
+ decision: nil,
272
+ because: [],
273
+ failed_conditions: []
274
+ }
216
275
  }.to_json
217
276
  end
218
277
  rescue StandardError => e
@@ -1141,6 +1200,516 @@ module DecisionAgent
1141
1200
  "Batch testing page not found"
1142
1201
  end
1143
1202
 
1203
+ # Simulation API Endpoints
1204
+
1205
+ # POST /api/simulation/replay - Historical replay/backtesting
1206
+ post "/api/simulation/replay" do
1207
+ content_type :json
1208
+
1209
+ begin
1210
+ request_body = request.body.read
1211
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1212
+
1213
+ historical_data = data["historical_data"]
1214
+ rule_version = data["rule_version"]
1215
+ compare_with = data["compare_with"]
1216
+ options = data["options"] || {}
1217
+
1218
+ unless historical_data
1219
+ status 400
1220
+ return { error: "historical_data is required" }.to_json
1221
+ end
1222
+
1223
+ # Get rules for agent creation
1224
+ rules_json = data["rules"]
1225
+ unless rules_json
1226
+ status 400
1227
+ return { error: "rules JSON is required" }.to_json
1228
+ end
1229
+
1230
+ # Create agent
1231
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
1232
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
1233
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1234
+
1235
+ # Create replay engine
1236
+ replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
1237
+ agent: agent,
1238
+ version_manager: version_manager
1239
+ )
1240
+
1241
+ # Convert historical data if it's a file path (for future file upload support)
1242
+ contexts = if historical_data.is_a?(Array)
1243
+ historical_data
1244
+ else
1245
+ # Assume it's a file path - load it
1246
+ raise ArgumentError, "File not found: #{historical_data}" unless File.exist?(historical_data)
1247
+
1248
+ if historical_data.end_with?(".json")
1249
+ JSON.parse(File.read(historical_data))
1250
+ elsif historical_data.end_with?(".csv")
1251
+ # Simple CSV parsing
1252
+ require "csv"
1253
+ csv_data = CSV.read(historical_data, headers: true)
1254
+ csv_data.map(&:to_h)
1255
+ else
1256
+ raise ArgumentError, "Unsupported file format"
1257
+ end
1258
+
1259
+ end
1260
+
1261
+ # Execute replay
1262
+ results = if compare_with
1263
+ replay_engine.replay(
1264
+ historical_data: contexts,
1265
+ rule_version: rule_version,
1266
+ compare_with: compare_with
1267
+ )
1268
+ else
1269
+ replay_engine.replay(
1270
+ historical_data: contexts,
1271
+ rule_version: rule_version,
1272
+ options: options
1273
+ )
1274
+ end
1275
+
1276
+ # Store results
1277
+ replay_id = SecureRandom.uuid
1278
+ self.class.simulation_storage_mutex.synchronize do
1279
+ self.class.simulation_storage[replay_id] = {
1280
+ id: replay_id,
1281
+ type: "replay",
1282
+ status: "completed",
1283
+ created_at: Time.now.utc.iso8601,
1284
+ results: results
1285
+ }
1286
+ end
1287
+
1288
+ {
1289
+ replay_id: replay_id,
1290
+ results: results
1291
+ }.to_json
1292
+ rescue StandardError => e
1293
+ status 500
1294
+ { error: "Replay failed: #{e.message}" }.to_json
1295
+ end
1296
+ end
1297
+
1298
+ # POST /api/simulation/whatif - What-if analysis
1299
+ post "/api/simulation/whatif" do
1300
+ content_type :json
1301
+
1302
+ begin
1303
+ request_body = request.body.read
1304
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1305
+
1306
+ scenarios = data["scenarios"]
1307
+ rule_version = data["rule_version"]
1308
+ options = data["options"] || {}
1309
+
1310
+ unless scenarios.is_a?(Array)
1311
+ status 400
1312
+ return { error: "scenarios array is required" }.to_json
1313
+ end
1314
+
1315
+ # Get rules for agent creation
1316
+ rules_json = data["rules"]
1317
+ unless rules_json
1318
+ status 400
1319
+ return { error: "rules JSON is required" }.to_json
1320
+ end
1321
+
1322
+ # Create agent
1323
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
1324
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
1325
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1326
+
1327
+ # Create what-if analyzer
1328
+ analyzer = DecisionAgent::Simulation::WhatIfAnalyzer.new(
1329
+ agent: agent,
1330
+ version_manager: version_manager
1331
+ )
1332
+
1333
+ # Execute analysis
1334
+ results = analyzer.analyze(
1335
+ scenarios: scenarios,
1336
+ rule_version: rule_version,
1337
+ options: options
1338
+ )
1339
+
1340
+ # Store results
1341
+ analysis_id = SecureRandom.uuid
1342
+ self.class.simulation_storage_mutex.synchronize do
1343
+ self.class.simulation_storage[analysis_id] = {
1344
+ id: analysis_id,
1345
+ type: "whatif",
1346
+ status: "completed",
1347
+ created_at: Time.now.utc.iso8601,
1348
+ results: results
1349
+ }
1350
+ end
1351
+
1352
+ {
1353
+ analysis_id: analysis_id,
1354
+ results: results
1355
+ }.to_json
1356
+ rescue StandardError => e
1357
+ status 500
1358
+ { error: "What-if analysis failed: #{e.message}" }.to_json
1359
+ end
1360
+ end
1361
+
1362
+ # POST /api/simulation/whatif/sensitivity - Sensitivity analysis
1363
+ post "/api/simulation/whatif/sensitivity" do
1364
+ content_type :json
1365
+
1366
+ begin
1367
+ request_body = request.body.read
1368
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1369
+
1370
+ base_scenario = data["base_scenario"]
1371
+ variations = data["variations"]
1372
+ rule_version = data["rule_version"]
1373
+
1374
+ unless base_scenario && variations
1375
+ status 400
1376
+ return { error: "base_scenario and variations are required" }.to_json
1377
+ end
1378
+
1379
+ # Get rules for agent creation
1380
+ rules_json = data["rules"]
1381
+ unless rules_json
1382
+ status 400
1383
+ return { error: "rules JSON is required" }.to_json
1384
+ end
1385
+
1386
+ # Create agent
1387
+ evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
1388
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
1389
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1390
+
1391
+ # Create what-if analyzer
1392
+ analyzer = DecisionAgent::Simulation::WhatIfAnalyzer.new(
1393
+ agent: agent,
1394
+ version_manager: version_manager
1395
+ )
1396
+
1397
+ # Execute sensitivity analysis
1398
+ results = analyzer.sensitivity_analysis(
1399
+ base_scenario: base_scenario,
1400
+ variations: variations,
1401
+ rule_version: rule_version
1402
+ )
1403
+
1404
+ {
1405
+ results: results
1406
+ }.to_json
1407
+ rescue StandardError => e
1408
+ status 500
1409
+ { error: "Sensitivity analysis failed: #{e.message}" }.to_json
1410
+ end
1411
+ end
1412
+
1413
+ # POST /api/simulation/impact - Impact analysis
1414
+ post "/api/simulation/impact" do
1415
+ content_type :json
1416
+
1417
+ begin
1418
+ request_body = request.body.read
1419
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1420
+
1421
+ baseline_version = data["baseline_version"]
1422
+ proposed_version = data["proposed_version"]
1423
+ test_data = data["test_data"]
1424
+ options = data["options"] || {}
1425
+
1426
+ unless baseline_version && proposed_version && test_data
1427
+ status 400
1428
+ return { error: "baseline_version, proposed_version, and test_data are required" }.to_json
1429
+ end
1430
+
1431
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1432
+
1433
+ # Create impact analyzer
1434
+ analyzer = DecisionAgent::Simulation::ImpactAnalyzer.new(
1435
+ version_manager: version_manager
1436
+ )
1437
+
1438
+ # Execute impact analysis
1439
+ results = analyzer.analyze(
1440
+ baseline_version: baseline_version,
1441
+ proposed_version: proposed_version,
1442
+ test_data: test_data,
1443
+ options: options
1444
+ )
1445
+
1446
+ # Store results
1447
+ impact_id = SecureRandom.uuid
1448
+ self.class.simulation_storage_mutex.synchronize do
1449
+ self.class.simulation_storage[impact_id] = {
1450
+ id: impact_id,
1451
+ type: "impact",
1452
+ status: "completed",
1453
+ created_at: Time.now.utc.iso8601,
1454
+ results: results
1455
+ }
1456
+ end
1457
+
1458
+ {
1459
+ impact_id: impact_id,
1460
+ results: results
1461
+ }.to_json
1462
+ rescue StandardError => e
1463
+ status 500
1464
+ { error: "Impact analysis failed: #{e.message}" }.to_json
1465
+ end
1466
+ end
1467
+
1468
+ # POST /api/simulation/shadow - Shadow testing
1469
+ post "/api/simulation/shadow" do
1470
+ content_type :json
1471
+
1472
+ begin
1473
+ request_body = request.body.read
1474
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1475
+
1476
+ context = data["context"]
1477
+ shadow_version = data["shadow_version"]
1478
+ production_rules = data["production_rules"]
1479
+ shadow_rules = data["shadow_rules"]
1480
+ options = data["options"] || {}
1481
+
1482
+ unless context
1483
+ status 400
1484
+ return { error: "context is required" }.to_json
1485
+ end
1486
+
1487
+ unless (production_rules && shadow_rules) || shadow_version
1488
+ status 400
1489
+ return { error: "Either (production_rules and shadow_rules) or shadow_version is required" }.to_json
1490
+ end
1491
+
1492
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1493
+
1494
+ # Create production agent
1495
+ if production_rules
1496
+ prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: production_rules)
1497
+ production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
1498
+ else
1499
+ # Use active version
1500
+ active_version = version_manager.get_active_version
1501
+ if active_version
1502
+ prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: active_version[:content])
1503
+ production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
1504
+ else
1505
+ status 400
1506
+ return { error: "No active version found and production_rules not provided" }.to_json
1507
+ end
1508
+ end
1509
+
1510
+ # Create shadow test engine
1511
+ shadow_engine = DecisionAgent::Simulation::ShadowTestEngine.new(
1512
+ production_agent: production_agent,
1513
+ version_manager: version_manager
1514
+ )
1515
+
1516
+ # Execute shadow test
1517
+ if shadow_rules
1518
+ # Create a temporary version for shadow rules
1519
+ temp_version = {
1520
+ content: shadow_rules,
1521
+ rule_id: "shadow_temp",
1522
+ version_number: 1
1523
+ }
1524
+ result = shadow_engine.test(
1525
+ context: context,
1526
+ shadow_version: temp_version,
1527
+ options: options
1528
+ )
1529
+ else
1530
+ result = shadow_engine.test(
1531
+ context: context,
1532
+ shadow_version: shadow_version,
1533
+ options: options
1534
+ )
1535
+ end
1536
+
1537
+ {
1538
+ result: result
1539
+ }.to_json
1540
+ rescue StandardError => e
1541
+ status 500
1542
+ { error: "Shadow test failed: #{e.message}" }.to_json
1543
+ end
1544
+ end
1545
+
1546
+ # POST /api/simulation/shadow/batch - Batch shadow testing
1547
+ post "/api/simulation/shadow/batch" do
1548
+ content_type :json
1549
+
1550
+ begin
1551
+ request_body = request.body.read
1552
+ data = request_body.empty? ? {} : JSON.parse(request_body)
1553
+
1554
+ contexts = data["contexts"]
1555
+ shadow_version = data["shadow_version"]
1556
+ production_rules = data["production_rules"]
1557
+ shadow_rules = data["shadow_rules"]
1558
+ options = data["options"] || {}
1559
+
1560
+ unless contexts.is_a?(Array)
1561
+ status 400
1562
+ return { error: "contexts array is required" }.to_json
1563
+ end
1564
+
1565
+ unless (production_rules && shadow_rules) || shadow_version
1566
+ status 400
1567
+ return { error: "Either (production_rules and shadow_rules) or shadow_version is required" }.to_json
1568
+ end
1569
+
1570
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1571
+
1572
+ # Create production agent
1573
+ if production_rules
1574
+ prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: production_rules)
1575
+ production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
1576
+ else
1577
+ # Use active version
1578
+ active_version = version_manager.get_active_version
1579
+ if active_version
1580
+ prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: active_version[:content])
1581
+ production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
1582
+ else
1583
+ status 400
1584
+ return { error: "No active version found and production_rules not provided" }.to_json
1585
+ end
1586
+ end
1587
+
1588
+ # Create shadow test engine
1589
+ shadow_engine = DecisionAgent::Simulation::ShadowTestEngine.new(
1590
+ production_agent: production_agent,
1591
+ version_manager: version_manager
1592
+ )
1593
+
1594
+ # Execute batch shadow test
1595
+ if shadow_rules
1596
+ # Create a temporary version for shadow rules
1597
+ temp_version = {
1598
+ content: shadow_rules,
1599
+ rule_id: "shadow_temp",
1600
+ version_number: 1
1601
+ }
1602
+ results = shadow_engine.batch_test(
1603
+ contexts: contexts,
1604
+ shadow_version: temp_version,
1605
+ options: options
1606
+ )
1607
+ else
1608
+ results = shadow_engine.batch_test(
1609
+ contexts: contexts,
1610
+ shadow_version: shadow_version,
1611
+ options: options
1612
+ )
1613
+ end
1614
+
1615
+ {
1616
+ results: results
1617
+ }.to_json
1618
+ rescue StandardError => e
1619
+ status 500
1620
+ { error: "Batch shadow test failed: #{e.message}" }.to_json
1621
+ end
1622
+ end
1623
+
1624
+ # GET /api/simulation/:id - Get simulation results
1625
+ get "/api/simulation/:id" do
1626
+ content_type :json
1627
+
1628
+ begin
1629
+ sim_id = params[:id]
1630
+
1631
+ sim_data = nil
1632
+ self.class.simulation_storage_mutex.synchronize do
1633
+ sim_data = self.class.simulation_storage[sim_id]
1634
+ end
1635
+
1636
+ unless sim_data
1637
+ status 404
1638
+ return { error: "Simulation not found" }.to_json
1639
+ end
1640
+
1641
+ sim_data.to_json
1642
+ rescue StandardError => e
1643
+ status 500
1644
+ { error: e.message }.to_json
1645
+ end
1646
+ end
1647
+
1648
+ # GET /api/versions - List all versions (for simulation dropdowns)
1649
+ get "/api/versions" do
1650
+ content_type :json
1651
+
1652
+ begin
1653
+ version_manager = DecisionAgent::Versioning::VersionManager.new
1654
+ versions = version_manager.list_versions
1655
+
1656
+ {
1657
+ versions: versions.map do |v|
1658
+ {
1659
+ id: v[:id] || v["id"],
1660
+ rule_id: v[:rule_id] || v["rule_id"],
1661
+ version_number: v[:version_number] || v["version_number"],
1662
+ status: v[:status] || v["status"],
1663
+ created_at: v[:created_at] || v["created_at"]
1664
+ }
1665
+ end
1666
+ }.to_json
1667
+ rescue StandardError => e
1668
+ status 500
1669
+ { error: e.message }.to_json
1670
+ end
1671
+ end
1672
+
1673
+ # GET /simulation - Simulation dashboard UI page
1674
+ get "/simulation" do
1675
+ send_file File.join(settings.public_folder, "simulation.html")
1676
+ rescue StandardError
1677
+ status 404
1678
+ "Simulation page not found"
1679
+ end
1680
+
1681
+ # GET /simulation/replay - Historical replay UI page
1682
+ get "/simulation/replay" do
1683
+ send_file File.join(settings.public_folder, "simulation_replay.html")
1684
+ rescue StandardError
1685
+ status 404
1686
+ "Historical replay page not found"
1687
+ end
1688
+
1689
+ # GET /simulation/whatif - What-if analysis UI page
1690
+ get "/simulation/whatif" do
1691
+ send_file File.join(settings.public_folder, "simulation_whatif.html")
1692
+ rescue StandardError
1693
+ status 404
1694
+ "What-if analysis page not found"
1695
+ end
1696
+
1697
+ # GET /simulation/impact - Impact analysis UI page
1698
+ get "/simulation/impact" do
1699
+ send_file File.join(settings.public_folder, "simulation_impact.html")
1700
+ rescue StandardError
1701
+ status 404
1702
+ "Impact analysis page not found"
1703
+ end
1704
+
1705
+ # GET /simulation/shadow - Shadow testing UI page
1706
+ get "/simulation/shadow" do
1707
+ send_file File.join(settings.public_folder, "simulation_shadow.html")
1708
+ rescue StandardError
1709
+ status 404
1710
+ "Shadow testing page not found"
1711
+ end
1712
+
1144
1713
  # GET /auth/login - Login page
1145
1714
  get "/auth/login" do
1146
1715
  send_file File.join(settings.public_folder, "login.html")
@@ -1157,8 +1726,469 @@ module DecisionAgent
1157
1726
  "User management page not found"
1158
1727
  end
1159
1728
 
1729
+ # DMN Editor Routes
1730
+
1731
+ # GET /dmn/editor - DMN Editor UI page
1732
+ get "/dmn/editor" do
1733
+ send_file File.join(settings.public_folder, "dmn-editor.html")
1734
+ rescue StandardError
1735
+ status 404
1736
+ "DMN Editor page not found"
1737
+ end
1738
+
1739
+ # API: List all DMN models
1740
+ get "/api/dmn/models" do
1741
+ content_type :json
1742
+ dmn_editor.list_models.to_json
1743
+ end
1744
+
1745
+ # API: Create new DMN model
1746
+ post "/api/dmn/models" do
1747
+ content_type :json
1748
+
1749
+ begin
1750
+ request_body = request.body.read
1751
+ data = JSON.parse(request_body)
1752
+
1753
+ model = dmn_editor.create_model(
1754
+ name: data["name"],
1755
+ namespace: data["namespace"]
1756
+ )
1757
+
1758
+ status 201
1759
+ model.to_json
1760
+ rescue StandardError => e
1761
+ status 500
1762
+ { error: e.message }.to_json
1763
+ end
1764
+ end
1765
+
1766
+ # API: Get DMN model
1767
+ get "/api/dmn/models/:id" do
1768
+ content_type :json
1769
+
1770
+ model = dmn_editor.get_model(params[:id])
1771
+ if model
1772
+ model.to_json
1773
+ else
1774
+ status 404
1775
+ { error: "Model not found" }.to_json
1776
+ end
1777
+ end
1778
+
1779
+ # API: Update DMN model
1780
+ put "/api/dmn/models/:id" do
1781
+ content_type :json
1782
+
1783
+ begin
1784
+ request_body = request.body.read
1785
+ data = JSON.parse(request_body)
1786
+
1787
+ model = dmn_editor.update_model(
1788
+ params[:id],
1789
+ name: data["name"],
1790
+ namespace: data["namespace"]
1791
+ )
1792
+
1793
+ if model
1794
+ model.to_json
1795
+ else
1796
+ status 404
1797
+ { error: "Model not found" }.to_json
1798
+ end
1799
+ rescue StandardError => e
1800
+ status 500
1801
+ { error: e.message }.to_json
1802
+ end
1803
+ end
1804
+
1805
+ # API: Delete DMN model
1806
+ delete "/api/dmn/models/:id" do
1807
+ content_type :json
1808
+
1809
+ result = dmn_editor.delete_model(params[:id])
1810
+ { success: result }.to_json
1811
+ end
1812
+
1813
+ # API: Add decision to model
1814
+ post "/api/dmn/models/:model_id/decisions" do
1815
+ content_type :json
1816
+
1817
+ begin
1818
+ request_body = request.body.read
1819
+ data = JSON.parse(request_body)
1820
+
1821
+ decision = dmn_editor.add_decision(
1822
+ model_id: params[:model_id],
1823
+ decision_id: data["decision_id"],
1824
+ name: data["name"],
1825
+ type: data["type"] || "decision_table"
1826
+ )
1827
+
1828
+ if decision
1829
+ status 201
1830
+ decision.to_json
1831
+ else
1832
+ status 404
1833
+ { error: "Model not found" }.to_json
1834
+ end
1835
+ rescue StandardError => e
1836
+ status 500
1837
+ { error: e.message }.to_json
1838
+ end
1839
+ end
1840
+
1841
+ # API: Update decision
1842
+ put "/api/dmn/models/:model_id/decisions/:decision_id" do
1843
+ content_type :json
1844
+
1845
+ begin
1846
+ request_body = request.body.read
1847
+ data = JSON.parse(request_body)
1848
+
1849
+ decision = dmn_editor.update_decision(
1850
+ model_id: params[:model_id],
1851
+ decision_id: params[:decision_id],
1852
+ name: data["name"],
1853
+ logic: data["logic"]
1854
+ )
1855
+
1856
+ if decision
1857
+ decision.to_json
1858
+ else
1859
+ status 404
1860
+ { error: "Decision not found" }.to_json
1861
+ end
1862
+ rescue StandardError => e
1863
+ status 500
1864
+ { error: e.message }.to_json
1865
+ end
1866
+ end
1867
+
1868
+ # API: Delete decision
1869
+ delete "/api/dmn/models/:model_id/decisions/:decision_id" do
1870
+ content_type :json
1871
+
1872
+ result = dmn_editor.delete_decision(
1873
+ model_id: params[:model_id],
1874
+ decision_id: params[:decision_id]
1875
+ )
1876
+
1877
+ { success: result }.to_json
1878
+ end
1879
+
1880
+ # API: Add input column
1881
+ post "/api/dmn/models/:model_id/decisions/:decision_id/inputs" do
1882
+ content_type :json
1883
+
1884
+ begin
1885
+ request_body = request.body.read
1886
+ data = JSON.parse(request_body)
1887
+
1888
+ input = dmn_editor.add_input(
1889
+ model_id: params[:model_id],
1890
+ decision_id: params[:decision_id],
1891
+ input_id: data["input_id"],
1892
+ label: data["label"],
1893
+ type_ref: data["type_ref"],
1894
+ expression: data["expression"]
1895
+ )
1896
+
1897
+ if input
1898
+ status 201
1899
+ input.to_json
1900
+ else
1901
+ status 404
1902
+ { error: "Decision not found" }.to_json
1903
+ end
1904
+ rescue StandardError => e
1905
+ status 500
1906
+ { error: e.message }.to_json
1907
+ end
1908
+ end
1909
+
1910
+ # API: Add output column
1911
+ post "/api/dmn/models/:model_id/decisions/:decision_id/outputs" do
1912
+ content_type :json
1913
+
1914
+ begin
1915
+ request_body = request.body.read
1916
+ data = JSON.parse(request_body)
1917
+
1918
+ output = dmn_editor.add_output(
1919
+ model_id: params[:model_id],
1920
+ decision_id: params[:decision_id],
1921
+ output_id: data["output_id"],
1922
+ label: data["label"],
1923
+ type_ref: data["type_ref"],
1924
+ name: data["name"]
1925
+ )
1926
+
1927
+ if output
1928
+ status 201
1929
+ output.to_json
1930
+ else
1931
+ status 404
1932
+ { error: "Decision not found" }.to_json
1933
+ end
1934
+ rescue StandardError => e
1935
+ status 500
1936
+ { error: e.message }.to_json
1937
+ end
1938
+ end
1939
+
1940
+ # API: Add rule
1941
+ post "/api/dmn/models/:model_id/decisions/:decision_id/rules" do
1942
+ content_type :json
1943
+
1944
+ begin
1945
+ request_body = request.body.read
1946
+ data = JSON.parse(request_body)
1947
+
1948
+ rule = dmn_editor.add_rule(
1949
+ model_id: params[:model_id],
1950
+ decision_id: params[:decision_id],
1951
+ rule_id: data["rule_id"],
1952
+ input_entries: data["input_entries"],
1953
+ output_entries: data["output_entries"],
1954
+ description: data["description"]
1955
+ )
1956
+
1957
+ if rule
1958
+ status 201
1959
+ rule.to_json
1960
+ else
1961
+ status 404
1962
+ { error: "Decision not found" }.to_json
1963
+ end
1964
+ rescue StandardError => e
1965
+ status 500
1966
+ { error: e.message }.to_json
1967
+ end
1968
+ end
1969
+
1970
+ # API: Update rule
1971
+ put "/api/dmn/models/:model_id/decisions/:decision_id/rules/:rule_id" do
1972
+ content_type :json
1973
+
1974
+ begin
1975
+ request_body = request.body.read
1976
+ data = JSON.parse(request_body)
1977
+
1978
+ rule = dmn_editor.update_rule(
1979
+ model_id: params[:model_id],
1980
+ decision_id: params[:decision_id],
1981
+ rule_id: params[:rule_id],
1982
+ input_entries: data["input_entries"],
1983
+ output_entries: data["output_entries"],
1984
+ description: data["description"]
1985
+ )
1986
+
1987
+ if rule
1988
+ rule.to_json
1989
+ else
1990
+ status 404
1991
+ { error: "Rule not found" }.to_json
1992
+ end
1993
+ rescue StandardError => e
1994
+ status 500
1995
+ { error: e.message }.to_json
1996
+ end
1997
+ end
1998
+
1999
+ # API: Delete rule
2000
+ delete "/api/dmn/models/:model_id/decisions/:decision_id/rules/:rule_id" do
2001
+ content_type :json
2002
+
2003
+ result = dmn_editor.delete_rule(
2004
+ model_id: params[:model_id],
2005
+ decision_id: params[:decision_id],
2006
+ rule_id: params[:rule_id]
2007
+ )
2008
+
2009
+ { success: result }.to_json
2010
+ end
2011
+
2012
+ # API: Validate DMN model
2013
+ get "/api/dmn/models/:id/validate" do
2014
+ content_type :json
2015
+ dmn_editor.validate_model(params[:id]).to_json
2016
+ end
2017
+
2018
+ # API: Export DMN model to XML
2019
+ get "/api/dmn/models/:id/export" do
2020
+ content_type :xml
2021
+
2022
+ xml = dmn_editor.export_to_xml(params[:id])
2023
+ if xml
2024
+ xml
2025
+ else
2026
+ status 404
2027
+ "Model not found"
2028
+ end
2029
+ end
2030
+
2031
+ # API: Import DMN model from XML
2032
+ post "/api/dmn/models/import" do
2033
+ content_type :json
2034
+
2035
+ begin
2036
+ request_body = request.body.read
2037
+ data = JSON.parse(request_body)
2038
+
2039
+ model = dmn_editor.import_from_xml(data["xml"], name: data["name"])
2040
+
2041
+ if model
2042
+ status 201
2043
+ model.to_json
2044
+ else
2045
+ status 400
2046
+ { error: "Failed to import model" }.to_json
2047
+ end
2048
+ rescue StandardError => e
2049
+ status 500
2050
+ { error: e.message }.to_json
2051
+ end
2052
+ end
2053
+
2054
+ # API: Visualize decision tree
2055
+ get "/api/dmn/models/:model_id/decisions/:decision_id/visualize/tree" do
2056
+ format = params[:format] || "svg"
2057
+
2058
+ visualization = dmn_editor.visualize_tree(
2059
+ model_id: params[:model_id],
2060
+ decision_id: params[:decision_id],
2061
+ format: format
2062
+ )
2063
+
2064
+ if visualization
2065
+ content_type format == "svg" ? "image/svg+xml" : :text
2066
+ visualization
2067
+ else
2068
+ status 404
2069
+ "Decision not found or not a tree"
2070
+ end
2071
+ end
2072
+
2073
+ # API: Visualize decision graph
2074
+ get "/api/dmn/models/:id/visualize/graph" do
2075
+ format = params[:format] || "svg"
2076
+
2077
+ visualization = dmn_editor.visualize_graph(
2078
+ model_id: params[:id],
2079
+ format: format
2080
+ )
2081
+
2082
+ if visualization
2083
+ content_type format == "svg" ? "image/svg+xml" : :text
2084
+ visualization
2085
+ else
2086
+ status 404
2087
+ "Model not found"
2088
+ end
2089
+ end
2090
+
2091
+ # API: Import DMN file (uploads and imports to versioning system)
2092
+ post "/api/dmn/import" do
2093
+ content_type :json
2094
+
2095
+ begin
2096
+ # Check if request has multipart form data (file upload)
2097
+ if params[:file] && params[:file][:tempfile]
2098
+ # File upload
2099
+ file = params[:file][:tempfile]
2100
+ xml_content = file.read
2101
+ ruleset_name = params[:ruleset_name] || params[:file][:filename]&.gsub(/\.dmn$/i, "")
2102
+ created_by = @current_user ? @current_user.id.to_s : params[:created_by] || "system"
2103
+ elsif request.content_type&.include?("application/json")
2104
+ # JSON body with XML content
2105
+ request_body = request.body.read
2106
+ data = JSON.parse(request_body)
2107
+ xml_content = data["xml"] || data["content"]
2108
+ ruleset_name = data["ruleset_name"] || data["name"]
2109
+ created_by = @current_user ? @current_user.id.to_s : data["created_by"] || "system"
2110
+ elsif request.content_type&.include?("application/xml") || request.content_type&.include?("text/xml")
2111
+ # Direct XML upload
2112
+ xml_content = request.body.read
2113
+ ruleset_name = params[:ruleset_name] || "imported_dmn"
2114
+ created_by = @current_user ? @current_user.id.to_s : params[:created_by] || "system"
2115
+ else
2116
+ status 400
2117
+ return { error: "Invalid request. Expected file upload, JSON with 'xml' field, or XML content." }.to_json
2118
+ end
2119
+
2120
+ raise ArgumentError, "DMN XML content is required" if xml_content.nil? || xml_content.strip.empty?
2121
+
2122
+ # Import using DMN Importer
2123
+ importer = Dmn::Importer.new(version_manager: version_manager)
2124
+ result = importer.import_from_xml(
2125
+ xml_content,
2126
+ ruleset_name: ruleset_name,
2127
+ created_by: created_by
2128
+ )
2129
+
2130
+ status 201
2131
+ {
2132
+ success: true,
2133
+ ruleset_name: ruleset_name,
2134
+ decisions_imported: result[:decisions_imported],
2135
+ model: {
2136
+ id: result[:model].id,
2137
+ name: result[:model].name,
2138
+ namespace: result[:model].namespace,
2139
+ decisions: result[:model].decisions.map do |d|
2140
+ {
2141
+ id: d.id,
2142
+ name: d.name
2143
+ }
2144
+ end
2145
+ },
2146
+ versions: result[:versions].map do |v|
2147
+ {
2148
+ version: v[:version],
2149
+ rule_id: v[:rule_id],
2150
+ created_by: v[:created_by],
2151
+ created_at: v[:created_at]
2152
+ }
2153
+ end
2154
+ }.to_json
2155
+ rescue Dmn::InvalidDmnModelError, Dmn::DmnParseError => e
2156
+ status 400
2157
+ { error: "DMN validation error", message: e.message }.to_json
2158
+ rescue StandardError => e
2159
+ status 500
2160
+ { error: "Import failed", message: e.message }.to_json
2161
+ end
2162
+ end
2163
+
2164
+ # API: Export ruleset as DMN XML
2165
+ get "/api/dmn/export/:ruleset_id" do
2166
+ content_type :xml
2167
+
2168
+ begin
2169
+ ruleset_id = params[:ruleset_id]
2170
+ exporter = Dmn::Exporter.new(version_manager: version_manager)
2171
+ dmn_xml = exporter.export(ruleset_id)
2172
+
2173
+ headers["Content-Disposition"] = "attachment; filename=\"#{ruleset_id}.dmn\""
2174
+ dmn_xml
2175
+ rescue Dmn::InvalidDmnModelError => e
2176
+ status 404
2177
+ content_type :json
2178
+ { error: "Ruleset not found", message: e.message }.to_json
2179
+ rescue StandardError => e
2180
+ status 500
2181
+ content_type :json
2182
+ { error: "Export failed", message: e.message }.to_json
2183
+ end
2184
+ end
2185
+
1160
2186
  private
1161
2187
 
2188
+ def dmn_editor
2189
+ @dmn_editor ||= DmnEditor.new
2190
+ end
2191
+
1162
2192
  def version_manager
1163
2193
  @version_manager ||= DecisionAgent::Versioning::VersionManager.new
1164
2194
  end
@@ -1195,10 +2225,14 @@ module DecisionAgent
1195
2225
  return true if permissions_disabled?
1196
2226
 
1197
2227
  checker = self.class.permission_checker
1198
- unless checker.can?(@current_user, permission, resource)
2228
+ granted = checker.can?(@current_user, permission, resource)
2229
+
2230
+ unless granted
2231
+ # Log the permission denial, but don't let logging failures prevent the denial
1199
2232
  begin
2233
+ user_id = checker.user_id(@current_user)
1200
2234
  self.class.access_audit_logger.log_permission_check(
1201
- user_id: checker.user_id(@current_user),
2235
+ user_id: user_id,
1202
2236
  permission: permission,
1203
2237
  resource_type: resource&.class&.name,
1204
2238
  resource_id: resource&.id,
@@ -1214,9 +2248,11 @@ module DecisionAgent
1214
2248
  halt 403, { error: "Permission denied: #{permission}" }.to_json
1215
2249
  end
1216
2250
 
2251
+ # Log successful permission check, but don't let logging failures prevent access
1217
2252
  begin
2253
+ user_id = checker.user_id(@current_user)
1218
2254
  self.class.access_audit_logger.log_permission_check(
1219
- user_id: checker.user_id(@current_user),
2255
+ user_id: user_id,
1220
2256
  permission: permission,
1221
2257
  resource_type: resource&.class&.name,
1222
2258
  resource_id: resource&.id,