decision_agent 1.1.0 → 1.2.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -0
  3. data/README.md +3 -2
  4. data/lib/decision_agent/ab_testing/ab_test.rb +0 -0
  5. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +0 -0
  6. data/lib/decision_agent/ab_testing/ab_test_manager.rb +0 -0
  7. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +0 -0
  8. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +0 -3
  9. data/lib/decision_agent/ab_testing/storage/adapter.rb +0 -0
  10. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +0 -0
  11. data/lib/decision_agent/agent.rb +0 -0
  12. data/lib/decision_agent/audit/adapter.rb +0 -0
  13. data/lib/decision_agent/audit/logger_adapter.rb +0 -0
  14. data/lib/decision_agent/audit/null_adapter.rb +0 -0
  15. data/lib/decision_agent/auth/access_audit_logger.rb +0 -0
  16. data/lib/decision_agent/auth/authenticator.rb +0 -0
  17. data/lib/decision_agent/auth/password_reset_manager.rb +0 -0
  18. data/lib/decision_agent/auth/password_reset_token.rb +0 -0
  19. data/lib/decision_agent/auth/permission.rb +0 -0
  20. data/lib/decision_agent/auth/permission_checker.rb +0 -0
  21. data/lib/decision_agent/auth/rbac_adapter.rb +0 -0
  22. data/lib/decision_agent/auth/rbac_config.rb +0 -0
  23. data/lib/decision_agent/auth/role.rb +0 -0
  24. data/lib/decision_agent/auth/session.rb +0 -0
  25. data/lib/decision_agent/auth/session_manager.rb +0 -0
  26. data/lib/decision_agent/auth/user.rb +0 -0
  27. data/lib/decision_agent/context.rb +0 -0
  28. data/lib/decision_agent/decision.rb +0 -0
  29. data/lib/decision_agent/dmn/adapter.rb +0 -0
  30. data/lib/decision_agent/dmn/cache.rb +0 -0
  31. data/lib/decision_agent/dmn/decision_graph.rb +0 -0
  32. data/lib/decision_agent/dmn/decision_tree.rb +0 -0
  33. data/lib/decision_agent/dmn/errors.rb +0 -0
  34. data/lib/decision_agent/dmn/exporter.rb +41 -2
  35. data/lib/decision_agent/dmn/feel/evaluator.rb +0 -4
  36. data/lib/decision_agent/dmn/feel/functions.rb +0 -0
  37. data/lib/decision_agent/dmn/feel/parser.rb +0 -0
  38. data/lib/decision_agent/dmn/feel/simple_parser.rb +0 -0
  39. data/lib/decision_agent/dmn/feel/transformer.rb +0 -0
  40. data/lib/decision_agent/dmn/feel/types.rb +0 -0
  41. data/lib/decision_agent/dmn/importer.rb +0 -0
  42. data/lib/decision_agent/dmn/model.rb +0 -0
  43. data/lib/decision_agent/dmn/parser.rb +0 -0
  44. data/lib/decision_agent/dmn/testing.rb +0 -4
  45. data/lib/decision_agent/dmn/validator.rb +3 -7
  46. data/lib/decision_agent/dmn/versioning.rb +41 -15
  47. data/lib/decision_agent/dmn/visualizer.rb +0 -0
  48. data/lib/decision_agent/dsl/condition_evaluator.rb +0 -0
  49. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +0 -0
  50. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +0 -0
  51. data/lib/decision_agent/dsl/helpers/date_helpers.rb +0 -0
  52. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +0 -0
  53. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +0 -0
  54. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +0 -0
  55. data/lib/decision_agent/dsl/helpers/template_helpers.rb +0 -0
  56. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +0 -0
  57. data/lib/decision_agent/dsl/operators/base.rb +2 -2
  58. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +0 -0
  59. data/lib/decision_agent/dsl/operators/collection_operators.rb +0 -0
  60. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +0 -0
  61. data/lib/decision_agent/dsl/operators/date_time_operators.rb +1 -1
  62. data/lib/decision_agent/dsl/operators/duration_operators.rb +0 -0
  63. data/lib/decision_agent/dsl/operators/financial_operators.rb +0 -0
  64. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +0 -0
  65. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +0 -0
  66. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +0 -0
  67. data/lib/decision_agent/dsl/operators/numeric_operators.rb +0 -0
  68. data/lib/decision_agent/dsl/operators/rate_operators.rb +0 -0
  69. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +0 -0
  70. data/lib/decision_agent/dsl/operators/string_aggregations.rb +0 -0
  71. data/lib/decision_agent/dsl/operators/string_operators.rb +0 -0
  72. data/lib/decision_agent/dsl/operators/time_component_operators.rb +0 -0
  73. data/lib/decision_agent/dsl/rule_parser.rb +0 -0
  74. data/lib/decision_agent/dsl/schema_validator.rb +0 -0
  75. data/lib/decision_agent/errors.rb +0 -0
  76. data/lib/decision_agent/evaluation.rb +0 -0
  77. data/lib/decision_agent/evaluation_validator.rb +0 -0
  78. data/lib/decision_agent/evaluators/base.rb +0 -0
  79. data/lib/decision_agent/evaluators/dmn_evaluator.rb +0 -0
  80. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -24
  81. data/lib/decision_agent/evaluators/static_evaluator.rb +0 -0
  82. data/lib/decision_agent/explainability/condition_trace.rb +0 -0
  83. data/lib/decision_agent/explainability/explainability_result.rb +0 -0
  84. data/lib/decision_agent/explainability/rule_trace.rb +0 -0
  85. data/lib/decision_agent/explainability/trace_collector.rb +0 -0
  86. data/lib/decision_agent/monitoring/alert_manager.rb +0 -0
  87. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +0 -0
  88. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +0 -0
  89. data/lib/decision_agent/monitoring/dashboard/public/index.html +0 -0
  90. data/lib/decision_agent/monitoring/dashboard_server.rb +0 -0
  91. data/lib/decision_agent/monitoring/metrics_collector.rb +0 -0
  92. data/lib/decision_agent/monitoring/monitored_agent.rb +0 -0
  93. data/lib/decision_agent/monitoring/prometheus_exporter.rb +0 -0
  94. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +0 -0
  95. data/lib/decision_agent/monitoring/storage/base_adapter.rb +0 -0
  96. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +1 -1
  97. data/lib/decision_agent/replay/replay.rb +0 -0
  98. data/lib/decision_agent/scoring/base.rb +0 -0
  99. data/lib/decision_agent/scoring/consensus.rb +0 -0
  100. data/lib/decision_agent/scoring/max_weight.rb +0 -0
  101. data/lib/decision_agent/scoring/threshold.rb +0 -0
  102. data/lib/decision_agent/scoring/weighted_average.rb +0 -0
  103. data/lib/decision_agent/simulation/errors.rb +0 -0
  104. data/lib/decision_agent/simulation/impact_analyzer.rb +0 -2
  105. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +0 -2
  106. data/lib/decision_agent/simulation/replay_engine.rb +0 -2
  107. data/lib/decision_agent/simulation/scenario_engine.rb +0 -0
  108. data/lib/decision_agent/simulation/scenario_library.rb +0 -0
  109. data/lib/decision_agent/simulation/shadow_test_engine.rb +0 -0
  110. data/lib/decision_agent/simulation/what_if_analyzer.rb +0 -2
  111. data/lib/decision_agent/simulation.rb +0 -0
  112. data/lib/decision_agent/testing/batch_test_importer.rb +0 -4
  113. data/lib/decision_agent/testing/batch_test_runner.rb +0 -2
  114. data/lib/decision_agent/testing/test_coverage_analyzer.rb +0 -0
  115. data/lib/decision_agent/testing/test_result_comparator.rb +54 -63
  116. data/lib/decision_agent/testing/test_scenario.rb +0 -0
  117. data/lib/decision_agent/version.rb +1 -1
  118. data/lib/decision_agent/versioning/activerecord_adapter.rb +64 -2
  119. data/lib/decision_agent/versioning/adapter.rb +33 -0
  120. data/lib/decision_agent/versioning/file_storage_adapter.rb +77 -7
  121. data/lib/decision_agent/versioning/version_manager.rb +40 -2
  122. data/lib/decision_agent/web/dmn_editor/serialization.rb +0 -0
  123. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +0 -0
  124. data/lib/decision_agent/web/dmn_editor.rb +0 -6
  125. data/lib/decision_agent/web/middleware/auth_middleware.rb +0 -0
  126. data/lib/decision_agent/web/middleware/permission_middleware.rb +0 -0
  127. data/lib/decision_agent/web/public/app.js +0 -0
  128. data/lib/decision_agent/web/public/batch_testing.html +0 -0
  129. data/lib/decision_agent/web/public/dmn-editor.css +0 -0
  130. data/lib/decision_agent/web/public/dmn-editor.html +0 -0
  131. data/lib/decision_agent/web/public/dmn-editor.js +5 -0
  132. data/lib/decision_agent/web/public/index.html +0 -0
  133. data/lib/decision_agent/web/public/login.html +0 -0
  134. data/lib/decision_agent/web/public/sample_batch.csv +0 -0
  135. data/lib/decision_agent/web/public/sample_impact.csv +0 -0
  136. data/lib/decision_agent/web/public/sample_replay.csv +0 -0
  137. data/lib/decision_agent/web/public/sample_rules.json +0 -0
  138. data/lib/decision_agent/web/public/sample_shadow.csv +0 -0
  139. data/lib/decision_agent/web/public/sample_whatif.csv +0 -0
  140. data/lib/decision_agent/web/public/simulation.html +0 -0
  141. data/lib/decision_agent/web/public/simulation_impact.html +0 -0
  142. data/lib/decision_agent/web/public/simulation_replay.html +0 -0
  143. data/lib/decision_agent/web/public/simulation_shadow.html +0 -0
  144. data/lib/decision_agent/web/public/simulation_whatif.html +0 -0
  145. data/lib/decision_agent/web/public/styles.css +0 -0
  146. data/lib/decision_agent/web/public/users.html +0 -0
  147. data/lib/decision_agent/web/rack_helpers.rb +0 -0
  148. data/lib/decision_agent/web/rack_request_helpers.rb +0 -0
  149. data/lib/decision_agent/web/server.rb +8 -2
  150. data/lib/decision_agent.rb +0 -0
  151. data/lib/generators/decision_agent/install/install_generator.rb +0 -0
  152. data/lib/generators/decision_agent/install/templates/README +0 -0
  153. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +0 -0
  154. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +0 -0
  155. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +0 -0
  156. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +0 -0
  157. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +0 -0
  158. data/lib/generators/decision_agent/install/templates/decision_log.rb +0 -0
  159. data/lib/generators/decision_agent/install/templates/error_metric.rb +0 -0
  160. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +0 -0
  161. data/lib/generators/decision_agent/install/templates/migration.rb +14 -0
  162. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +0 -2
  163. data/lib/generators/decision_agent/install/templates/performance_metric.rb +0 -0
  164. data/lib/generators/decision_agent/install/templates/rule.rb +0 -0
  165. data/lib/generators/decision_agent/install/templates/rule_version.rb +0 -0
  166. data/lib/generators/decision_agent/install/templates/rule_version_tag.rb +23 -0
  167. data/lib/generators/decision_agent/install/templates/versioning_migration.rb +44 -0
  168. data/lib/generators/decision_agent/monitoring_migration/monitoring_migration_generator.rb +67 -0
  169. data/lib/generators/decision_agent/versioning_migration/versioning_migration_generator.rb +57 -0
  170. metadata +10 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6c760652b22f7cf3e4524aec1757f1fba1fc21c206c960ad3836eed18279f13
4
- data.tar.gz: e0c2f21dfac36d7a4ce7935d6ea38375265c64e15eec4c276c514a1bf759dd6e
3
+ metadata.gz: 93d5c128215a2a4a834f609111870adafa6f74bebdc05ada319399e5945f13de
4
+ data.tar.gz: 61133113389fd9da5b52833d4c2d640f3a7b4eb866979da9d4c783d4a2f95468
5
5
  SHA512:
6
- metadata.gz: d9a402f62fa86a4cf83d727f048b6de8a8d67ab317cbc3dd2d681cd537a2a1a68a1bbe77b2f34a4c6f4a805babace2e7c6d449435d8fef9831dff884152b441a
7
- data.tar.gz: 2b00ae31f1e89caa62c004c6e649296e1c6a193faf90ded7ef2fd6e930b3c5547113d028349de2b6904bfb9e2371fd37335251b6391bac4315cda1ca6407c4e8
6
+ metadata.gz: f9c6fb182e3a729baa5773796121d5e202bc40fc3d9979e137dd7c8c8ab892446067e64489be7431b77196d64cae0df52fa653f90b80f1436ebb70281ec32710
7
+ data.tar.gz: 7f44db70f5a2f3368eecb91e11fc6879d1d36142d787ba10f2d1be9216acdac46c54db45dd401cc67f6e35faaa643e0a12652235909ed6c4f677f63670049c4a
data/LICENSE.txt CHANGED
File without changes
data/README.md CHANGED
@@ -118,7 +118,7 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
118
118
  - **Grafana Integration** - Pre-built dashboards and alert rules
119
119
  - **Version Control** - Full rule version control, rollback, and history ([Versioning Guide](docs/VERSIONING.md))
120
120
  - **Thread-Safe** - Safe for multi-threaded servers and background jobs
121
- - **High Performance** - 10,000+ decisions/second, ~0.1ms latency
121
+ - **High Performance** - 8,000–9,000+ decisions/second, ~0.1ms latency (hardware-dependent; see [benchmarks](docs/PERFORMANCE_AND_THREAD_SAFETY.md))
122
122
 
123
123
  ## Web UI - Visual Rule Builder
124
124
 
@@ -413,6 +413,7 @@ See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
413
413
  ### Reference
414
414
  - [API Contract](docs/API_CONTRACT.md) - Full API reference
415
415
  - [Changelog](docs/CHANGELOG.md) - Version history
416
+ - [Roadmap](docs/ROADMAP.md) - Shipped, in-progress, and deferred features
416
417
  - [Code Coverage Report](coverage.md) - Test coverage statistics
417
418
 
418
419
  ### More Resources
@@ -423,7 +424,7 @@ See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
423
424
 
424
425
  DecisionAgent is designed to be **thread-safe and FAST** for use in multi-threaded environments:
425
426
 
426
- - **10,000+ decisions/second** throughput
427
+ - **8,000–9,000+ decisions/second** throughput (hardware-dependent)
427
428
  - **~0.1ms average latency** per decision
428
429
  - **Zero performance overhead** from thread-safety
429
430
  - **Linear scalability** with thread count
File without changes
File without changes
File without changes
File without changes
@@ -79,14 +79,11 @@ module DecisionAgent
79
79
  .map { |record| to_assignment(record) }
80
80
  end
81
81
 
82
- # rubocop:disable Naming/PredicateMethod
83
82
  def delete_test(test_id)
84
83
  record = ::ABTestModel.find(test_id)
85
84
  ::ABTestAssignmentModel.where(ab_test_id: test_id).delete_all
86
85
  record.destroy
87
- true
88
86
  end
89
- # rubocop:enable Naming/PredicateMethod
90
87
 
91
88
  private
92
89
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -13,6 +13,27 @@ module DecisionAgent
13
13
  @version_manager = version_manager || Versioning::VersionManager.new
14
14
  end
15
15
 
16
+ # Serialize an in-memory DMN Model object to DMN XML.
17
+ # Unlike #export, this does NOT look up any stored version — it converts
18
+ # the live model directly. Use this when saving a new version.
19
+ # @param model [DecisionAgent::Dmn::Model]
20
+ # @return [String] DMN XML
21
+ def serialize_model(model)
22
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
23
+ xml.definitions(
24
+ "xmlns" => "https://www.omg.org/spec/DMN/20191111/MODEL/",
25
+ "xmlns:dmndi" => "https://www.omg.org/spec/DMN/20191111/DMNDI/",
26
+ "xmlns:dc" => "http://www.omg.org/spec/DMN/20180521/DC/",
27
+ "id" => "definitions_#{model.id}",
28
+ "name" => model.name,
29
+ "namespace" => model.namespace
30
+ ) do
31
+ model.decisions.each { |d| serialize_decision_node(xml, d) }
32
+ end
33
+ end
34
+ builder.to_xml
35
+ end
36
+
16
37
  # Export ruleset to DMN XML
17
38
  # @param rule_id [String] Rule ID to export
18
39
  # @param output_path [String, nil] Optional file path to write
@@ -33,12 +54,31 @@ module DecisionAgent
33
54
 
34
55
  private
35
56
 
57
+ def serialize_decision_node(xml, decision)
58
+ xml.decision(id: decision.id, name: decision.name) do
59
+ xml.description(decision.description) if decision.description
60
+ if (dt = decision.decision_table)
61
+ xml.decisionTable(id: dt.id, hitPolicy: dt.hit_policy) do
62
+ dt.inputs.each do |inp|
63
+ xml.input(id: inp.id, label: inp.label) do
64
+ xml.inputExpression(typeRef: inp.type_ref) do
65
+ xml.text_ inp.expression
66
+ end
67
+ end
68
+ end
69
+ dt.outputs.each do |out|
70
+ xml.output(id: out.id, label: out.label, name: out.name, typeRef: out.type_ref)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
36
77
  # Helper to get hash value with both string and symbol key support
37
78
  def hash_get(hash, key)
38
79
  hash[key.to_s] || hash[key.to_sym]
39
80
  end
40
81
 
41
- # rubocop:disable Metrics/MethodLength
42
82
  def convert_to_dmn(rules_json, rule_id)
43
83
  # Handle both string and symbol keys
44
84
  ruleset_name = rules_json["ruleset"] || rules_json[:ruleset] || rule_id
@@ -85,7 +125,6 @@ module DecisionAgent
85
125
 
86
126
  builder.to_xml
87
127
  end
88
- # rubocop:enable Metrics/MethodLength
89
128
 
90
129
  def extract_inputs(rules)
91
130
  # Extract all unique field names used in conditions
@@ -15,7 +15,6 @@ module DecisionAgent
15
15
  # Phase 2A: Basic comparisons, ranges, list membership (regex-based)
16
16
  # Phase 2B: Arithmetic, logical operators, functions (enhanced parser)
17
17
  # Maps FEEL expressions to DecisionAgent ConditionEvaluator
18
- # rubocop:disable Metrics/ClassLength
19
18
  class Evaluator
20
19
  def initialize
21
20
  @simple_parser = SimpleParser.new
@@ -493,7 +492,6 @@ module DecisionAgent
493
492
  end
494
493
 
495
494
  # Evaluate Parslet AST node (Phase 2B - full FEEL support)
496
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
497
495
  def evaluate_ast_node(node, context)
498
496
  return node unless node.is_a?(Hash)
499
497
 
@@ -549,7 +547,6 @@ module DecisionAgent
549
547
  raise FeelEvaluationError, "Unknown AST node type: #{node[:type]}"
550
548
  end
551
549
  end
552
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
553
550
 
554
551
  # Get field value from context
555
552
  def get_field_value(field_name, context)
@@ -807,7 +804,6 @@ module DecisionAgent
807
804
  start_check && end_check
808
805
  end
809
806
  end
810
- # rubocop:enable Metrics/ClassLength
811
807
  end
812
808
  end
813
809
  end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -43,7 +43,6 @@ module DecisionAgent
43
43
  end
44
44
 
45
45
  # Run a single test scenario
46
- # rubocop:disable Metrics/MethodLength
47
46
  def run_test(scenario, index = nil)
48
47
  decision = @model.find_decision(scenario[:decision_id])
49
48
 
@@ -92,11 +91,9 @@ module DecisionAgent
92
91
  passed: false
93
92
  }
94
93
  end
95
- # rubocop:enable Metrics/MethodLength
96
94
  end
97
95
 
98
96
  # Generate test coverage report
99
- # rubocop:disable Metrics/AbcSize
100
97
  def generate_coverage_report
101
98
  coverage = {
102
99
  total_decisions: @model.decisions.size,
@@ -328,7 +325,6 @@ module DecisionAgent
328
325
  model_results: results
329
326
  }
330
327
  end
331
- # rubocop:enable Metrics/AbcSize
332
328
  end
333
329
  end
334
330
  end
@@ -16,10 +16,9 @@ module DecisionAgent
16
16
  @feel_evaluator = Feel::Evaluator.new
17
17
  end
18
18
 
19
- # rubocop:disable Naming/PredicateMethod
20
19
  def validate(model = nil)
21
20
  @model = model if model
22
- return false unless @model
21
+ return unless @model
23
22
 
24
23
  @errors = []
25
24
  @warnings = []
@@ -35,18 +34,15 @@ module DecisionAgent
35
34
  # Business rule validation
36
35
  validate_decision_tables
37
36
 
38
- @errors.empty?
37
+ nil
39
38
  end
40
- # rubocop:enable Naming/PredicateMethod
41
39
 
42
- # rubocop:disable Naming/PredicateMethod
43
40
  def validate!
44
41
  validate
45
42
  raise InvalidDmnModelError, format_errors if @errors.any?
46
43
 
47
- true
44
+ nil
48
45
  end
49
- # rubocop:enable Naming/PredicateMethod
50
46
 
51
47
  def valid?
52
48
  @errors.empty?
@@ -16,12 +16,17 @@ module DecisionAgent
16
16
  end
17
17
 
18
18
  # Save a DMN model as a new version
19
- def save_dmn_version(model:, created_by: "system", changelog: nil)
20
- # Export DMN model to XML
19
+ # @param model [Object] The DMN model to save
20
+ # @param created_by [String] Who is saving this version
21
+ # @param changelog [String, nil] Human-readable description of what changed
22
+ # @param tag [String, nil] Optional tag name to apply to the new version at creation time
23
+ # @return [Hash] The created version
24
+ def save_dmn_version(model:, created_by: "system", changelog: nil, tag: nil)
25
+ # Serialize the in-memory DMN model to XML (no DB lookup needed)
21
26
  exporter = Exporter.new
22
- xml_content = exporter.export(model)
27
+ xml_content = exporter.serialize_model(model)
23
28
 
24
- # Save as version
29
+ # Save as version (with optional tag applied atomically)
25
30
  @version_manager.save_version(
26
31
  rule_id: model.id,
27
32
  rule_content: {
@@ -31,7 +36,8 @@ module DecisionAgent
31
36
  namespace: model.namespace
32
37
  },
33
38
  created_by: created_by,
34
- changelog: changelog || "DMN model updated"
39
+ changelog: changelog || "DMN model updated",
40
+ tag: tag
35
41
  )
36
42
  end
37
43
 
@@ -136,17 +142,37 @@ module DecisionAgent
136
142
  @version_manager.delete_version(version_id: version_id)
137
143
  end
138
144
 
139
- # Tag a DMN version
140
- # rubocop:disable Naming/PredicateMethod
141
- def tag_dmn_version(version_id:, tag:)
142
- _tag = tag # TODO: Implement tag functionality
143
- version = @version_manager.get_version(version_id: version_id)
144
- return false unless version
145
- # rubocop:enable Naming/PredicateMethod
145
+ # Tag a specific DMN version after the fact.
146
+ # Creates the tag if it does not exist; re-points it if the name is already in use.
147
+ # @param model_id [String] The model identifier
148
+ # @param version_id [String] The version to tag
149
+ # @param name [String] The tag name (e.g. "release-candidate")
150
+ # @return [Hash] The tag ({ name:, version_id:, created_at: })
151
+ def tag_dmn!(model_id:, version_id:, name:)
152
+ @version_manager.tag!(model_id, version_id, name)
153
+ end
154
+
155
+ # Resolve a tag to its tag hash for the given model.
156
+ # @param model_id [String] The model identifier
157
+ # @param name [String] The tag name
158
+ # @return [Hash, nil] Tag hash or nil if not found
159
+ def get_dmn_tag(model_id:, name:)
160
+ @version_manager.get_tag(model_id: model_id, name: name)
161
+ end
162
+
163
+ # List all tags for a DMN model.
164
+ # @param model_id [String] The model identifier
165
+ # @return [Array<Hash>] Tag hashes sorted by name
166
+ def list_dmn_tags(model_id:)
167
+ @version_manager.list_tags(model_id: model_id)
168
+ end
146
169
 
147
- # Add tag to metadata (this would need to be implemented in VersionManager)
148
- # For now, we'll use changelog to append the tag
149
- true
170
+ # Delete a tag by name.
171
+ # @param model_id [String] The model identifier
172
+ # @param name [String] The tag name
173
+ # @return [Boolean] True if deleted, false if the tag did not exist
174
+ def delete_dmn_tag(model_id:, name:)
175
+ @version_manager.delete_tag(model_id: model_id, name: name)
150
176
  end
151
177
 
152
178
  # Get active DMN version for a model
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -61,8 +61,8 @@ module DecisionAgent
61
61
  end
62
62
 
63
63
  # Epsilon comparison for floating point numbers
64
- def self.epsilon_equal?(a, b, epsilon = 1e-10) # rubocop:disable Naming/MethodParameterName
65
- (a - b).abs < epsilon
64
+ def self.epsilon_equal?(lhs, rhs, epsilon = 1e-10)
65
+ (lhs - rhs).abs < epsilon
66
66
  end
67
67
  end
68
68
  end
@@ -5,7 +5,7 @@ module DecisionAgent
5
5
  module Operators
6
6
  # Handles date/time operators: before_date, after_date, within_days, day_of_week
7
7
  module DateTimeOperators
8
- def self.handle(op, actual_value, expected_value, date_cache: nil, date_cache_mutex: nil) # rubocop:disable Lint/UnusedMethodArgument
8
+ def self.handle(op, actual_value, expected_value, _date_cache: nil, _date_cache_mutex: nil)
9
9
  case op
10
10
  when "before_date"
11
11
  # Checks if date is before specified date
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -13,6 +13,11 @@ module DecisionAgent
13
13
  @ruleset_name = @ruleset["ruleset"] || "unknown"
14
14
  @name = name || "JsonRuleEvaluator(#{@ruleset_name})"
15
15
 
16
+ # Pre-build O(1) rule lookup map keyed by rule_id
17
+ @rules_by_id = (@ruleset["rules"] || []).each_with_index.to_h do |rule, i|
18
+ [rule["id"] || "rule_#{i}", rule]
19
+ end.freeze
20
+
16
21
  # Freeze ruleset to ensure thread-safety
17
22
  deep_freeze(@ruleset)
18
23
  @rules_json.freeze
@@ -30,9 +35,8 @@ module DecisionAgent
30
35
  matched_rule_trace = explainability_result&.matched_rules&.first
31
36
  return nil unless matched_rule_trace
32
37
 
33
- # Find the original rule to get the then clause
34
- rules = @ruleset["rules"] || []
35
- matched_rule = rules.find { |r| (r["id"] || "rule_#{rules.index(r)}") == matched_rule_trace.rule_id }
38
+ # Find the original rule to get the then clause (O(1) lookup)
39
+ matched_rule = @rules_by_id[matched_rule_trace.rule_id]
36
40
  return nil unless matched_rule
37
41
 
38
42
  then_clause = matched_rule["then"]
@@ -59,37 +63,35 @@ module DecisionAgent
59
63
 
60
64
  def collect_explainability(context)
61
65
  rules = @ruleset["rules"] || []
62
- rule_traces = []
63
66
 
64
- rules.each do |rule|
65
- rule_id = rule["id"] || "rule_#{rules.index(rule)}"
66
- if_clause = rule["if"]
67
- next unless if_clause
67
+ # Fast pass: find the first matching rule without building trace objects.
68
+ # This avoids allocating TraceCollector + RuleTrace for every non-matching rule.
69
+ matched_index = nil
70
+ rules.each_with_index do |rule, i|
71
+ next unless rule["if"]
68
72
 
69
- # Create trace collector for this rule
70
- trace_collector = Explainability::TraceCollector.new
71
-
72
- # Evaluate condition with tracing
73
- matched = Dsl::ConditionEvaluator.evaluate(
74
- if_clause,
75
- context,
76
- trace_collector: trace_collector
77
- )
73
+ if Dsl::ConditionEvaluator.evaluate(rule["if"], context)
74
+ matched_index = i
75
+ break
76
+ end
77
+ end
78
78
 
79
+ # Trace pass: re-evaluate only the matched rule with full condition tracing.
80
+ rule_traces = []
81
+ if matched_index
82
+ rule = rules[matched_index]
83
+ rule_id = rule["id"] || "rule_#{matched_index}"
84
+ trace_collector = Explainability::TraceCollector.new
85
+ Dsl::ConditionEvaluator.evaluate(rule["if"], context, trace_collector: trace_collector)
79
86
  then_clause = rule["then"] || {}
80
- rule_trace = Explainability::RuleTrace.new(
87
+ rule_traces << Explainability::RuleTrace.new(
81
88
  rule_id: rule_id,
82
- matched: matched,
89
+ matched: true,
83
90
  condition_traces: trace_collector.traces,
84
91
  decision: then_clause["decision"],
85
92
  weight: then_clause["weight"],
86
93
  reason: then_clause["reason"]
87
94
  )
88
-
89
- rule_traces << rule_trace
90
-
91
- # Stop after first match (short-circuit evaluation)
92
- break if matched
93
95
  end
94
96
 
95
97
  Explainability::ExplainabilityResult.new(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -97,7 +97,7 @@ module DecisionAgent
97
97
  def time_series(metric_type, bucket_size: 60, time_range: 3600)
98
98
  synchronize do
99
99
  cutoff = Time.now - time_range
100
- metrics = @metrics[metric_type].select { |m| m[:timestamp] >= cutoff }
100
+ metrics = (@metrics[metric_type] || []).select { |m| m[:timestamp] >= cutoff }
101
101
 
102
102
  buckets = Hash.new(0)
103
103
  metrics.each do |metric|
File without changes
File without changes
File without changes