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.
Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  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 +78 -9
  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 +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  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 +130 -114
  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 +242 -1375
  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 +37 -14
  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 +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  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 +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -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 +108 -43
  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 +186 -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 +69 -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 +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Simulation and What-If Analysis module
4
+ # Provides tools for scenario testing, historical replay, impact analysis, shadow testing, and Monte Carlo simulation
5
+
6
+ require_relative "simulation/errors"
7
+ require_relative "simulation/replay_engine"
8
+ require_relative "simulation/what_if_analyzer"
9
+ require_relative "simulation/impact_analyzer"
10
+ require_relative "simulation/shadow_test_engine"
11
+ require_relative "simulation/scenario_engine"
12
+ require_relative "simulation/scenario_library"
13
+ require_relative "simulation/monte_carlo_simulator"
14
+
15
+ module DecisionAgent
16
+ module Simulation
17
+ # Main entry point for simulation features
18
+ end
19
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "csv"
2
4
  require "roo"
3
5
 
@@ -44,8 +46,9 @@ module DecisionAgent
44
46
  if options[:progress_callback]
45
47
  begin
46
48
  total_rows = count_csv_rows(file_path, options[:skip_header])
47
- rescue StandardError
49
+ rescue StandardError => e
48
50
  # If counting fails, continue without progress tracking
51
+ warn "[DecisionAgent] Failed to count CSV rows: #{e.message}"
49
52
  total_rows = nil
50
53
  end
51
54
  end
@@ -364,8 +367,9 @@ module DecisionAgent
364
367
  count += 1
365
368
  end
366
369
  count
367
- rescue StandardError
370
+ rescue StandardError => e
368
371
  # If we can't count, return nil (progress tracking will be disabled)
372
+ warn "[DecisionAgent] Failed to count CSV rows for '#{file_path}': #{e.message}"
369
373
  nil
370
374
  end
371
375
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
 
3
5
  module DecisionAgent
@@ -153,7 +155,7 @@ module DecisionAgent
153
155
  loop do
154
156
  scenario = begin
155
157
  queue.pop(true)
156
- rescue StandardError
158
+ rescue ThreadError
157
159
  nil
158
160
  end
159
161
  break unless scenario
@@ -228,7 +230,8 @@ module DecisionAgent
228
230
  data
229
231
  rescue JSON::ParserError
230
232
  { completed_scenario_ids: [], last_updated: nil }
231
- rescue StandardError
233
+ rescue StandardError => e
234
+ warn "[DecisionAgent] Failed to load checkpoint file: #{e.message}"
232
235
  { completed_scenario_ids: [], last_updated: nil }
233
236
  end
234
237
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "set"
2
4
 
3
5
  module DecisionAgent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Testing
3
5
  # Comparison result for a single test scenario
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Testing
3
5
  # Represents a single test scenario with context and expected results
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  # Semantic version: MAJOR.MINOR.PATCH
3
5
  # MAJOR: Incremented for incompatible API changes
4
6
  # MINOR: Incremented for backward-compatible functionality additions
5
7
  # PATCH: Incremented for backward-compatible bug fixes
6
- VERSION = "0.3.0".freeze
8
+ VERSION = "1.1.0"
7
9
 
8
10
  # Validate version format (semantic versioning)
9
11
  unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "adapter"
2
4
  require_relative "file_storage_adapter"
3
5
 
@@ -16,40 +18,43 @@ module DecisionAgent
16
18
  end
17
19
 
18
20
  def create_version(rule_id:, content:, metadata: {})
19
- # Use a transaction with pessimistic locking to prevent race conditions
20
- version = nil
21
-
22
21
  # Validate status if provided
23
22
  status = metadata[:status] || "active"
24
23
  validate_status!(status)
25
24
 
26
- rule_version_class.transaction do
27
- # Lock the last version for this rule to prevent concurrent reads
28
- # This ensures only one thread can calculate the next version number at a time
29
- last_version = rule_version_class.where(rule_id: rule_id)
30
- .order(version_number: :desc)
31
- .lock
32
- .first
33
- next_version_number = last_version ? last_version.version_number + 1 : 1
34
-
35
- # Deactivate previous active versions
36
- # Use update! instead of update_all to trigger validations
37
- rule_version_class.where(rule_id: rule_id, status: "active").find_each do |v|
38
- v.update!(status: "archived")
25
+ # Retry on SQLite busy exceptions (common with concurrent operations)
26
+ retry_with_backoff(max_retries: 10) do
27
+ # Use a transaction with pessimistic locking to prevent race conditions
28
+ version = nil
29
+
30
+ rule_version_class.transaction do
31
+ # Lock the last version for this rule to prevent concurrent reads
32
+ # This ensures only one thread can calculate the next version number at a time
33
+ last_version = rule_version_class.where(rule_id: rule_id)
34
+ .order(version_number: :desc)
35
+ .lock
36
+ .first
37
+ next_version_number = last_version ? last_version.version_number + 1 : 1
38
+
39
+ # Deactivate previous active versions
40
+ # Use update_all for better concurrency (avoids SQLite locking issues)
41
+ # Status "archived" is valid, so no need to trigger validations
42
+ rule_version_class.where(rule_id: rule_id, status: "active")
43
+ .update_all(status: "archived")
44
+
45
+ # Create new version
46
+ version = rule_version_class.create!(
47
+ rule_id: rule_id,
48
+ version_number: next_version_number,
49
+ content: content.to_json,
50
+ created_by: metadata[:created_by] || "system",
51
+ changelog: metadata[:changelog] || "Version #{next_version_number}",
52
+ status: status
53
+ )
39
54
  end
40
55
 
41
- # Create new version
42
- version = rule_version_class.create!(
43
- rule_id: rule_id,
44
- version_number: next_version_number,
45
- content: content.to_json,
46
- created_by: metadata[:created_by] || "system",
47
- changelog: metadata[:changelog] || "Version #{next_version_number}",
48
- status: status
49
- )
56
+ serialize_version(version)
50
57
  end
51
-
52
- serialize_version(version)
53
58
  end
54
59
 
55
60
  def list_versions(rule_id:, limit: nil)
@@ -60,6 +65,13 @@ module DecisionAgent
60
65
  query.map { |v| serialize_version(v) }
61
66
  end
62
67
 
68
+ def list_all_versions(limit: nil)
69
+ query = rule_version_class.order(created_at: :desc)
70
+ query = query.limit(limit) if limit
71
+
72
+ query.map { |v| serialize_version(v) }
73
+ end
74
+
63
75
  def get_version(version_id:)
64
76
  version = rule_version_class.find_by(id: version_id)
65
77
  version ? serialize_version(version) : nil
@@ -79,26 +91,44 @@ module DecisionAgent
79
91
  end
80
92
 
81
93
  def activate_version(version_id:)
82
- version = nil
83
-
84
- rule_version_class.transaction do
85
- # Find and lock the version to activate
86
- version = rule_version_class.lock.find(version_id)
87
-
88
- # Deactivate all other versions for this rule within the same transaction
89
- # The lock ensures only one thread can perform this operation at a time
90
- # Use update! instead of update_all to trigger validations
91
- rule_version_class.where(rule_id: version.rule_id, status: "active")
92
- .where.not(id: version_id)
93
- .find_each do |v|
94
- v.update!(status: "archived")
94
+ # Retry on SQLite busy exceptions (common with concurrent operations)
95
+ retry_with_backoff(max_retries: 10) do
96
+ version = nil
97
+
98
+ rule_version_class.transaction do
99
+ # Find and lock the version to activate
100
+ version = rule_version_class.lock.find(version_id)
101
+
102
+ # Deactivate all other versions for this rule within the same transaction
103
+ # The lock ensures only one thread can perform this operation at a time
104
+ # Use update_all for better concurrency (avoids SQLite locking issues)
105
+ # Status "archived" is valid, so no need to trigger validations
106
+ rule_version_class.where(rule_id: version.rule_id, status: "active")
107
+ .where.not(id: version_id)
108
+ .update_all(status: "archived")
109
+
110
+ # Activate this version
111
+ version.update!(status: "active")
95
112
  end
96
113
 
97
- # Activate this version
98
- version.update!(status: "active")
114
+ serialize_version(version)
99
115
  end
116
+ end
117
+
118
+ def delete_version(version_id:)
119
+ version = rule_version_class.find_by(id: version_id)
120
+
121
+ # Version not found
122
+ raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
100
123
 
101
- serialize_version(version)
124
+ # Prevent deletion of active versions
125
+ raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first." if version.status == "active"
126
+
127
+ # Delete the version
128
+ version.destroy
129
+ true
130
+ rescue ActiveRecord::RecordNotFound
131
+ raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
102
132
  end
103
133
 
104
134
  private
@@ -113,6 +143,41 @@ module DecisionAgent
113
143
  end
114
144
  end
115
145
 
146
+ # Retry database operations that may encounter SQLite busy exceptions
147
+ # This is especially important for concurrent operations on different rules
148
+ def retry_with_backoff(max_retries: 10, base_delay: 0.01)
149
+ retries = 0
150
+ begin
151
+ yield
152
+ rescue ActiveRecord::StatementInvalid => e
153
+ # Check if it's a SQLite busy exception
154
+ # Handle different SQLite adapter implementations
155
+ is_busy = begin
156
+ # Check the underlying exception type
157
+ cause = e.cause
158
+ if cause
159
+ cause.class.name.include?("BusyException") ||
160
+ cause.class.name.include?("SQLite3::BusyException")
161
+ else
162
+ false
163
+ end
164
+ rescue StandardError => cause_check_error
165
+ warn "[DecisionAgent] Error checking busy exception cause: #{cause_check_error.message}"
166
+ false
167
+ end || e.message.include?("database is locked") ||
168
+ e.message.include?("SQLite3::BusyException") ||
169
+ e.message.include?("BusyException")
170
+
171
+ raise unless is_busy && retries < max_retries
172
+
173
+ retries += 1
174
+ # Exponential backoff with jitter
175
+ delay = (base_delay * (2**retries)) + (rand * base_delay)
176
+ sleep(delay)
177
+ retry
178
+ end
179
+ end
180
+
116
181
  def serialize_version(version)
117
182
  # Parse JSON content with proper error handling
118
183
  parsed_content = begin
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Versioning
3
5
  # Abstract base class for version storage adapters
@@ -20,6 +22,13 @@ module DecisionAgent
20
22
  raise NotImplementedError, "#{self.class} must implement #list_versions"
21
23
  end
22
24
 
25
+ # List all versions across all rules
26
+ # @param limit [Integer, nil] Optional limit for number of versions
27
+ # @return [Array<Hash>] Array of version hashes
28
+ def list_all_versions(limit: nil)
29
+ raise NotImplementedError, "#{self.class} must implement #list_all_versions"
30
+ end
31
+
23
32
  # Get a specific version by ID
24
33
  # @param version_id [String, Integer] The version identifier
25
34
  # @return [Hash, nil] The version hash or nil if not found
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "adapter"
2
4
  require "json"
3
5
  require "fileutils"
@@ -86,12 +88,20 @@ module DecisionAgent
86
88
  end
87
89
  end
88
90
 
91
+ def list_all_versions(limit: nil)
92
+ @version_index_lock.synchronize do
93
+ versions = all_versions_unsafe
94
+ limit ? versions.take(limit) : versions
95
+ end
96
+ end
97
+
89
98
  def get_version(version_id:)
90
99
  # Use index to find rule_id quickly - O(1) instead of O(n)
91
100
  begin
92
101
  rule_id = get_rule_id_from_index(version_id)
93
- rescue StandardError
102
+ rescue StandardError => e
94
103
  # If index lookup fails, version doesn't exist
104
+ warn "[DecisionAgent] Version index lookup failed for '#{version_id}': #{e.message}"
95
105
  return nil
96
106
  end
97
107
  return nil unless rule_id
@@ -103,8 +113,9 @@ module DecisionAgent
103
113
  versions = list_versions_unsafe(rule_id: rule_id)
104
114
  versions.find { |v| v[:id] == version_id }
105
115
  end
106
- rescue StandardError
116
+ rescue StandardError => e
107
117
  # If any error occurs during lookup, treat as version not found
118
+ warn "[DecisionAgent] Version lookup failed for '#{version_id}': #{e.message}"
108
119
  nil
109
120
  end
110
121
  end
@@ -152,8 +163,9 @@ module DecisionAgent
152
163
  # Use index to find rule_id quickly - O(1) instead of O(n)
153
164
  begin
154
165
  rule_id = get_rule_id_from_index(version_id)
155
- rescue StandardError
166
+ rescue StandardError => e
156
167
  # If index lookup fails, version doesn't exist
168
+ warn "[DecisionAgent] Version index lookup failed for '#{version_id}': #{e.message}"
157
169
  raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
158
170
  end
159
171
 
@@ -218,15 +230,16 @@ module DecisionAgent
218
230
  rescue DecisionAgent::ValidationError, DecisionAgent::NotFoundError
219
231
  # Re-raise expected errors
220
232
  raise
221
- rescue StandardError
233
+ rescue StandardError => e
222
234
  # If any unexpected error occurs during the lock operation, treat as version not found
223
235
  # This prevents 500 errors from propagating when version doesn't exist or is in an invalid state
224
236
  # This handles ThreadError (deadlocks, recursive locks), SystemCallError (file system issues), etc.
225
237
  # This is safe because if the version existed and was valid, we would have found it above
238
+ warn "[DecisionAgent] Version delete lock operation failed for '#{version_id}': #{e.message}"
226
239
  begin
227
240
  remove_from_index(version_id)
228
- rescue StandardError
229
- nil
241
+ rescue StandardError => cleanup_error
242
+ warn "[DecisionAgent] Failed to clean up index for '#{version_id}': #{cleanup_error.message}"
230
243
  end
231
244
  raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
232
245
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DecisionAgent
2
4
  module Versioning
3
5
  # High-level service for managing rule versions
@@ -40,6 +42,13 @@ module DecisionAgent
40
42
  @adapter.list_versions(rule_id: rule_id, limit: limit)
41
43
  end
42
44
 
45
+ # Get all versions across all rules
46
+ # @param limit [Integer, nil] Optional limit
47
+ # @return [Array<Hash>] Array of versions
48
+ def list_all_versions(limit: nil)
49
+ @adapter.list_all_versions(limit: limit)
50
+ end
51
+
43
52
  # Get a specific version
44
53
  # @param version_id [String, Integer] The version identifier
45
54
  # @return [Hash, nil] The version or nil
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Web
5
+ class DmnEditor
6
+ module Serialization
7
+ def serialize_model(model)
8
+ {
9
+ id: model.id,
10
+ name: model.name,
11
+ namespace: model.namespace,
12
+ decisions: model.decisions.map { |d| serialize_decision(d) }
13
+ }
14
+ end
15
+
16
+ def serialize_decision(decision)
17
+ result = {
18
+ id: decision.id,
19
+ name: decision.name
20
+ }
21
+
22
+ if decision.decision_table
23
+ result[:decision_table] = serialize_decision_table(decision.decision_table)
24
+ elsif decision.decision_tree
25
+ result[:decision_tree] = decision.decision_tree.to_h
26
+ elsif decision.instance_variable_get(:@literal_expression)
27
+ result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
28
+ end
29
+
30
+ result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
31
+
32
+ result
33
+ end
34
+
35
+ def serialize_decision_table(table)
36
+ {
37
+ id: table.id,
38
+ hit_policy: table.hit_policy,
39
+ inputs: table.inputs.map { |i| serialize_input(i) },
40
+ outputs: table.outputs.map { |o| serialize_output(o) },
41
+ rules: table.rules.map { |r| serialize_rule(r) }
42
+ }
43
+ end
44
+
45
+ def serialize_input(input)
46
+ {
47
+ id: input.id,
48
+ label: input.label,
49
+ type_ref: input.type_ref,
50
+ expression: input.expression
51
+ }
52
+ end
53
+
54
+ def serialize_output(output)
55
+ {
56
+ id: output.id,
57
+ label: output.label,
58
+ type_ref: output.type_ref,
59
+ name: output.name
60
+ }
61
+ end
62
+
63
+ def serialize_rule(rule)
64
+ {
65
+ id: rule.id,
66
+ input_entries: rule.input_entries,
67
+ output_entries: rule.output_entries,
68
+ description: rule.description
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Web
5
+ class DmnEditor
6
+ module XmlBuilder
7
+ def generate_dmn_xml(model)
8
+ require "nokogiri"
9
+
10
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
11
+ xml.definitions(
12
+ "xmlns" => "https://www.omg.org/spec/DMN/20191111/MODEL/",
13
+ "xmlns:dmndi" => "https://www.omg.org/spec/DMN/20191111/DMNDI/",
14
+ "xmlns:dc" => "http://www.omg.org/spec/DMN/20180521/DC/",
15
+ "id" => "definitions_#{model.id}",
16
+ "name" => model.name,
17
+ "namespace" => model.namespace || "http://decision_agent.local"
18
+ ) do
19
+ model.decisions.each do |decision|
20
+ xml.decision(id: decision.id, name: decision.name) do
21
+ if decision.decision_table
22
+ build_decision_table_xml(xml, decision.decision_table)
23
+ elsif decision.decision_tree
24
+ xml.comment "Decision Tree (not fully supported in DMN XML export yet)"
25
+ elsif decision.instance_variable_get(:@literal_expression)
26
+ xml.literalExpression do
27
+ xml.text decision.instance_variable_get(:@literal_expression)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ builder.to_xml
36
+ end
37
+
38
+ def build_decision_table_xml(xml, table)
39
+ xml.decisionTable(
40
+ id: table.id,
41
+ hitPolicy: table.hit_policy || "FIRST",
42
+ outputLabel: "output"
43
+ ) do
44
+ build_inputs_xml(xml, table)
45
+ build_outputs_xml(xml, table)
46
+ build_rules_xml(xml, table)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def build_inputs_xml(xml, table)
53
+ table.inputs.each do |input|
54
+ xml.input(id: input.id, label: input.label) do
55
+ xml.inputExpression(typeRef: input.type_ref || "string") do
56
+ text_node = Nokogiri::XML::Node.new("text", xml.doc)
57
+ text_node.content = input.expression || input.label
58
+ xml.parent.add_child(text_node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def build_outputs_xml(xml, table)
65
+ table.outputs.each do |output|
66
+ xml.output(
67
+ id: output.id,
68
+ label: output.label,
69
+ name: output.name || output.label,
70
+ typeRef: output.type_ref || "string"
71
+ )
72
+ end
73
+ end
74
+
75
+ def build_rules_xml(xml, table)
76
+ table.rules.each { |rule| build_rule_xml(xml, rule) }
77
+ end
78
+
79
+ def build_rule_xml(xml, rule)
80
+ xml.rule(id: rule.id) do
81
+ rule.input_entries.each_with_index do |entry, idx|
82
+ add_entry_element(xml, "inputEntry", "#{rule.id}_input_#{idx + 1}", entry)
83
+ end
84
+ rule.output_entries.each_with_index do |entry, idx|
85
+ add_entry_element(xml, "outputEntry", "#{rule.id}_output_#{idx + 1}", entry)
86
+ end
87
+ add_rule_description(xml, rule)
88
+ end
89
+ end
90
+
91
+ def add_entry_element(xml, tag, id, content)
92
+ xml.send(tag, id: id) do
93
+ text_node = Nokogiri::XML::Node.new("text", xml.doc)
94
+ text_node.content = content.to_s
95
+ xml.parent.add_child(text_node)
96
+ end
97
+ end
98
+
99
+ def add_rule_description(xml, rule)
100
+ return if rule.description.nil? || rule.description.empty?
101
+
102
+ xml.description { xml.text rule.description }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end