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.
- checksums.yaml +4 -4
- data/LICENSE.txt +0 -0
- data/README.md +3 -2
- data/lib/decision_agent/ab_testing/ab_test.rb +0 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +0 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +0 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +0 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +0 -3
- data/lib/decision_agent/ab_testing/storage/adapter.rb +0 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +0 -0
- data/lib/decision_agent/agent.rb +0 -0
- data/lib/decision_agent/audit/adapter.rb +0 -0
- data/lib/decision_agent/audit/logger_adapter.rb +0 -0
- data/lib/decision_agent/audit/null_adapter.rb +0 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +0 -0
- data/lib/decision_agent/auth/authenticator.rb +0 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +0 -0
- data/lib/decision_agent/auth/password_reset_token.rb +0 -0
- data/lib/decision_agent/auth/permission.rb +0 -0
- data/lib/decision_agent/auth/permission_checker.rb +0 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +0 -0
- data/lib/decision_agent/auth/rbac_config.rb +0 -0
- data/lib/decision_agent/auth/role.rb +0 -0
- data/lib/decision_agent/auth/session.rb +0 -0
- data/lib/decision_agent/auth/session_manager.rb +0 -0
- data/lib/decision_agent/auth/user.rb +0 -0
- data/lib/decision_agent/context.rb +0 -0
- data/lib/decision_agent/decision.rb +0 -0
- data/lib/decision_agent/dmn/adapter.rb +0 -0
- data/lib/decision_agent/dmn/cache.rb +0 -0
- data/lib/decision_agent/dmn/decision_graph.rb +0 -0
- data/lib/decision_agent/dmn/decision_tree.rb +0 -0
- data/lib/decision_agent/dmn/errors.rb +0 -0
- data/lib/decision_agent/dmn/exporter.rb +41 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +0 -4
- data/lib/decision_agent/dmn/feel/functions.rb +0 -0
- data/lib/decision_agent/dmn/feel/parser.rb +0 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +0 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +0 -0
- data/lib/decision_agent/dmn/feel/types.rb +0 -0
- data/lib/decision_agent/dmn/importer.rb +0 -0
- data/lib/decision_agent/dmn/model.rb +0 -0
- data/lib/decision_agent/dmn/parser.rb +0 -0
- data/lib/decision_agent/dmn/testing.rb +0 -4
- data/lib/decision_agent/dmn/validator.rb +3 -7
- data/lib/decision_agent/dmn/versioning.rb +41 -15
- data/lib/decision_agent/dmn/visualizer.rb +0 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +0 -0
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +0 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +0 -0
- data/lib/decision_agent/dsl/operators/base.rb +2 -2
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +1 -1
- data/lib/decision_agent/dsl/operators/duration_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +0 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +0 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +0 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +0 -0
- data/lib/decision_agent/dsl/rule_parser.rb +0 -0
- data/lib/decision_agent/dsl/schema_validator.rb +0 -0
- data/lib/decision_agent/errors.rb +0 -0
- data/lib/decision_agent/evaluation.rb +0 -0
- data/lib/decision_agent/evaluation_validator.rb +0 -0
- data/lib/decision_agent/evaluators/base.rb +0 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +0 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -24
- data/lib/decision_agent/evaluators/static_evaluator.rb +0 -0
- data/lib/decision_agent/explainability/condition_trace.rb +0 -0
- data/lib/decision_agent/explainability/explainability_result.rb +0 -0
- data/lib/decision_agent/explainability/rule_trace.rb +0 -0
- data/lib/decision_agent/explainability/trace_collector.rb +0 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +0 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +0 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +0 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +0 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +0 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +0 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +0 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +0 -0
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +0 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +0 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +1 -1
- data/lib/decision_agent/replay/replay.rb +0 -0
- data/lib/decision_agent/scoring/base.rb +0 -0
- data/lib/decision_agent/scoring/consensus.rb +0 -0
- data/lib/decision_agent/scoring/max_weight.rb +0 -0
- data/lib/decision_agent/scoring/threshold.rb +0 -0
- data/lib/decision_agent/scoring/weighted_average.rb +0 -0
- data/lib/decision_agent/simulation/errors.rb +0 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +0 -2
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +0 -2
- data/lib/decision_agent/simulation/replay_engine.rb +0 -2
- data/lib/decision_agent/simulation/scenario_engine.rb +0 -0
- data/lib/decision_agent/simulation/scenario_library.rb +0 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +0 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +0 -2
- data/lib/decision_agent/simulation.rb +0 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +0 -4
- data/lib/decision_agent/testing/batch_test_runner.rb +0 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +0 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +54 -63
- data/lib/decision_agent/testing/test_scenario.rb +0 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +64 -2
- data/lib/decision_agent/versioning/adapter.rb +33 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +77 -7
- data/lib/decision_agent/versioning/version_manager.rb +40 -2
- data/lib/decision_agent/web/dmn_editor/serialization.rb +0 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +0 -0
- data/lib/decision_agent/web/dmn_editor.rb +0 -6
- data/lib/decision_agent/web/middleware/auth_middleware.rb +0 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +0 -0
- data/lib/decision_agent/web/public/app.js +0 -0
- data/lib/decision_agent/web/public/batch_testing.html +0 -0
- data/lib/decision_agent/web/public/dmn-editor.css +0 -0
- data/lib/decision_agent/web/public/dmn-editor.html +0 -0
- data/lib/decision_agent/web/public/dmn-editor.js +5 -0
- data/lib/decision_agent/web/public/index.html +0 -0
- data/lib/decision_agent/web/public/login.html +0 -0
- data/lib/decision_agent/web/public/sample_batch.csv +0 -0
- data/lib/decision_agent/web/public/sample_impact.csv +0 -0
- data/lib/decision_agent/web/public/sample_replay.csv +0 -0
- data/lib/decision_agent/web/public/sample_rules.json +0 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +0 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +0 -0
- data/lib/decision_agent/web/public/simulation.html +0 -0
- data/lib/decision_agent/web/public/simulation_impact.html +0 -0
- data/lib/decision_agent/web/public/simulation_replay.html +0 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +0 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +0 -0
- data/lib/decision_agent/web/public/styles.css +0 -0
- data/lib/decision_agent/web/public/users.html +0 -0
- data/lib/decision_agent/web/rack_helpers.rb +0 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +0 -0
- data/lib/decision_agent/web/server.rb +8 -2
- data/lib/decision_agent.rb +0 -0
- data/lib/generators/decision_agent/install/install_generator.rb +0 -0
- data/lib/generators/decision_agent/install/templates/README +0 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +0 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +0 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +0 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +0 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +0 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +0 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +14 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +0 -2
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +0 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +0 -0
- data/lib/generators/decision_agent/install/templates/rule_version_tag.rb +23 -0
- data/lib/generators/decision_agent/install/templates/versioning_migration.rb +44 -0
- data/lib/generators/decision_agent/monitoring_migration/monitoring_migration_generator.rb +67 -0
- data/lib/generators/decision_agent/versioning_migration/versioning_migration_generator.rb +57 -0
- metadata +10 -3
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,7 +5,6 @@ require_relative "errors"
|
|
|
5
5
|
module DecisionAgent
|
|
6
6
|
module Simulation
|
|
7
7
|
# Analyzer for quantifying rule change impact
|
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
|
9
8
|
class ImpactAnalyzer
|
|
10
9
|
attr_reader :version_manager
|
|
11
10
|
|
|
@@ -494,7 +493,6 @@ module DecisionAgent
|
|
|
494
493
|
"#{parts.join('. ')}."
|
|
495
494
|
end
|
|
496
495
|
end
|
|
497
|
-
# rubocop:enable Metrics/ClassLength
|
|
498
496
|
end
|
|
499
497
|
end
|
|
500
498
|
end
|
|
@@ -27,7 +27,6 @@ module DecisionAgent
|
|
|
27
27
|
#
|
|
28
28
|
# puts "Decision probabilities: #{results[:decision_probabilities]}"
|
|
29
29
|
# puts "Average confidence: #{results[:average_confidence]}"
|
|
30
|
-
# rubocop:disable Metrics/ClassLength
|
|
31
30
|
class MonteCarloSimulator
|
|
32
31
|
attr_reader :agent, :version_manager
|
|
33
32
|
|
|
@@ -632,7 +631,6 @@ module DecisionAgent
|
|
|
632
631
|
end
|
|
633
632
|
target[last_key.to_sym] = value
|
|
634
633
|
end
|
|
635
|
-
# rubocop:enable Metrics/ClassLength
|
|
636
634
|
end
|
|
637
635
|
end
|
|
638
636
|
end
|
|
@@ -14,7 +14,6 @@ end
|
|
|
14
14
|
module DecisionAgent
|
|
15
15
|
module Simulation
|
|
16
16
|
# Engine for replaying historical decisions and backtesting rule changes
|
|
17
|
-
# rubocop:disable Metrics/ClassLength
|
|
18
17
|
class ReplayEngine
|
|
19
18
|
attr_reader :agent, :version_manager
|
|
20
19
|
|
|
@@ -482,7 +481,6 @@ module DecisionAgent
|
|
|
482
481
|
errors: results.count { |r| r[:error] }
|
|
483
482
|
}
|
|
484
483
|
end
|
|
485
|
-
# rubocop:enable Metrics/ClassLength
|
|
486
484
|
end
|
|
487
485
|
end
|
|
488
486
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,7 +5,6 @@ require_relative "errors"
|
|
|
5
5
|
module DecisionAgent
|
|
6
6
|
module Simulation
|
|
7
7
|
# Analyzer for what-if scenario simulation
|
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
|
9
8
|
class WhatIfAnalyzer
|
|
10
9
|
attr_reader :agent, :version_manager
|
|
11
10
|
|
|
@@ -1002,7 +1001,6 @@ module DecisionAgent
|
|
|
1002
1001
|
"<div class='legend-item'><div class='legend-color' style='background: #{color};'></div><span>#{decision}</span></div>"
|
|
1003
1002
|
end.join
|
|
1004
1003
|
end
|
|
1005
|
-
# rubocop:enable Metrics/ClassLength
|
|
1006
1004
|
end
|
|
1007
1005
|
end
|
|
1008
1006
|
end
|
|
File without changes
|
|
@@ -24,7 +24,6 @@ module DecisionAgent
|
|
|
24
24
|
# - :skip_header [Boolean] Skip first row (default: true)
|
|
25
25
|
# - :progress_callback [Proc] Callback for progress updates (called with { processed: N, total: M, percentage: X })
|
|
26
26
|
# @return [Array<TestScenario>] Array of test scenarios
|
|
27
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
28
27
|
def import_csv(file_path, options = {})
|
|
29
28
|
@errors = []
|
|
30
29
|
@warnings = []
|
|
@@ -101,7 +100,6 @@ module DecisionAgent
|
|
|
101
100
|
|
|
102
101
|
scenarios
|
|
103
102
|
end
|
|
104
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
105
103
|
|
|
106
104
|
# Import test scenarios from an Excel file (.xlsx, .xls)
|
|
107
105
|
# @param file_path [String] Path to Excel file
|
|
@@ -109,7 +107,6 @@ module DecisionAgent
|
|
|
109
107
|
# - :sheet [String|Integer] Sheet name or index (default: first sheet)
|
|
110
108
|
# - :progress_callback [Proc] Callback for progress updates
|
|
111
109
|
# @return [Array<TestScenario>] Array of test scenarios
|
|
112
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
113
110
|
def import_excel(file_path, options = {})
|
|
114
111
|
@errors = []
|
|
115
112
|
@warnings = []
|
|
@@ -223,7 +220,6 @@ module DecisionAgent
|
|
|
223
220
|
raise ImportError, "Failed to read Excel file: #{e.message}"
|
|
224
221
|
end
|
|
225
222
|
end
|
|
226
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
227
223
|
|
|
228
224
|
# Import test scenarios from an array of hashes (for programmatic use)
|
|
229
225
|
# @param data [Array<Hash>] Array of hashes with test data
|
|
@@ -55,7 +55,6 @@ module DecisionAgent
|
|
|
55
55
|
# - :feedback [Hash] Optional feedback to pass to agent
|
|
56
56
|
# - :checkpoint_file [String] Path to checkpoint file for resume capability (optional)
|
|
57
57
|
# @return [Array<TestResult>] Array of test results
|
|
58
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
59
58
|
def run(scenarios, options = {})
|
|
60
59
|
@results = []
|
|
61
60
|
@checkpoint_file = options[:checkpoint_file]
|
|
@@ -141,7 +140,6 @@ module DecisionAgent
|
|
|
141
140
|
total_execution_time_ms: execution_times.sum
|
|
142
141
|
}
|
|
143
142
|
end
|
|
144
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
145
143
|
|
|
146
144
|
private
|
|
147
145
|
|
|
File without changes
|
|
@@ -6,7 +6,6 @@ module DecisionAgent
|
|
|
6
6
|
class ComparisonResult
|
|
7
7
|
attr_reader :scenario_id, :match, :decision_match, :confidence_match, :differences, :actual, :expected
|
|
8
8
|
|
|
9
|
-
# rubocop:disable Metrics/ParameterLists
|
|
10
9
|
def initialize(scenario_id:, match:, decision_match:, confidence_match:, differences:, actual:, expected:)
|
|
11
10
|
@scenario_id = scenario_id.to_s.freeze
|
|
12
11
|
@match = match
|
|
@@ -18,7 +17,6 @@ module DecisionAgent
|
|
|
18
17
|
|
|
19
18
|
freeze
|
|
20
19
|
end
|
|
21
|
-
# rubocop:enable Metrics/ParameterLists
|
|
22
20
|
|
|
23
21
|
def to_h
|
|
24
22
|
{
|
|
@@ -140,78 +138,71 @@ module DecisionAgent
|
|
|
140
138
|
|
|
141
139
|
private
|
|
142
140
|
|
|
143
|
-
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
144
141
|
def compare_single(scenario, result)
|
|
142
|
+
return failed_comparison_result(scenario, result) if result.nil? || !result.success?
|
|
143
|
+
|
|
145
144
|
differences = []
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
decision_match = compare_decision(scenario, result, differences)
|
|
146
|
+
confidence_match = compare_confidence(scenario, result, differences)
|
|
147
|
+
|
|
148
|
+
ComparisonResult.new(
|
|
149
|
+
scenario_id: scenario.id,
|
|
150
|
+
match: decision_match && confidence_match,
|
|
151
|
+
decision_match: decision_match,
|
|
152
|
+
confidence_match: confidence_match,
|
|
153
|
+
differences: differences,
|
|
154
|
+
actual: { decision: result.decision&.to_s, confidence: result.confidence },
|
|
155
|
+
expected: { decision: scenario.expected_decision&.to_s, confidence: scenario.expected_confidence }
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def failed_comparison_result(scenario, result)
|
|
160
|
+
ComparisonResult.new(
|
|
161
|
+
scenario_id: scenario.id,
|
|
162
|
+
match: false,
|
|
163
|
+
decision_match: false,
|
|
164
|
+
confidence_match: false,
|
|
165
|
+
differences: ["Test execution failed: #{result&.error&.message || 'No result'}"],
|
|
166
|
+
actual: { decision: nil, confidence: nil },
|
|
167
|
+
expected: { decision: scenario.expected_decision, confidence: scenario.expected_confidence }
|
|
168
|
+
)
|
|
169
|
+
end
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
def compare_decision(scenario, result, differences)
|
|
172
|
+
expected = scenario.expected_decision&.to_s
|
|
173
|
+
actual = result.decision&.to_s
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
match = if expected.nil?
|
|
176
|
+
true
|
|
177
|
+
elsif @options[:fuzzy_match]
|
|
178
|
+
fuzzy_decision_match?(expected, actual)
|
|
179
|
+
else
|
|
180
|
+
expected == actual
|
|
181
|
+
end
|
|
175
182
|
|
|
176
|
-
differences << "Decision mismatch: expected '#{
|
|
183
|
+
differences << "Decision mismatch: expected '#{expected}', got '#{actual}'" unless match
|
|
184
|
+
match
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def compare_confidence(scenario, result, differences)
|
|
188
|
+
expected = scenario.expected_confidence
|
|
189
|
+
actual = result.confidence
|
|
177
190
|
|
|
178
|
-
|
|
179
|
-
expected_confidence = scenario.expected_confidence
|
|
180
|
-
actual_confidence = result.confidence
|
|
191
|
+
return true if expected.nil?
|
|
181
192
|
|
|
182
|
-
if
|
|
183
|
-
confidence_match = true # No expectation, so it matches
|
|
184
|
-
elsif actual_confidence.nil?
|
|
185
|
-
confidence_match = false
|
|
193
|
+
if actual.nil?
|
|
186
194
|
differences << "Confidence missing in actual result"
|
|
187
|
-
|
|
188
|
-
tolerance = @options[:confidence_tolerance]
|
|
189
|
-
confidence_match = (expected_confidence - actual_confidence).abs <= tolerance
|
|
190
|
-
unless confidence_match
|
|
191
|
-
diff = (expected_confidence - actual_confidence).abs.round(4)
|
|
192
|
-
differences << "Confidence mismatch: expected #{expected_confidence}, got #{actual_confidence} (diff: #{diff})"
|
|
193
|
-
end
|
|
195
|
+
return false
|
|
194
196
|
end
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
differences: differences,
|
|
204
|
-
actual: {
|
|
205
|
-
decision: actual_decision,
|
|
206
|
-
confidence: actual_confidence
|
|
207
|
-
},
|
|
208
|
-
expected: {
|
|
209
|
-
decision: expected_decision,
|
|
210
|
-
confidence: expected_confidence
|
|
211
|
-
}
|
|
212
|
-
)
|
|
198
|
+
tolerance = @options[:confidence_tolerance]
|
|
199
|
+
match = (expected - actual).abs <= tolerance
|
|
200
|
+
unless match
|
|
201
|
+
diff = (expected - actual).abs.round(4)
|
|
202
|
+
differences << "Confidence mismatch: expected #{expected}, got #{actual} (diff: #{diff})"
|
|
203
|
+
end
|
|
204
|
+
match
|
|
213
205
|
end
|
|
214
|
-
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
215
206
|
|
|
216
207
|
def fuzzy_decision_match?(expected, actual)
|
|
217
208
|
return true if expected == actual
|
|
File without changes
|
|
@@ -5,7 +5,7 @@ module DecisionAgent
|
|
|
5
5
|
# MAJOR: Incremented for incompatible API changes
|
|
6
6
|
# MINOR: Incremented for backward-compatible functionality additions
|
|
7
7
|
# PATCH: Incremented for backward-compatible bug fixes
|
|
8
|
-
VERSION = "1.
|
|
8
|
+
VERSION = "1.2.0"
|
|
9
9
|
|
|
10
10
|
# Validate version format (semantic versioning)
|
|
11
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/)
|
|
@@ -131,6 +131,47 @@ module DecisionAgent
|
|
|
131
131
|
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Create (or update) a named tag pointing to a specific version.
|
|
135
|
+
# Tags are unique per model; calling this with an existing name re-points the tag.
|
|
136
|
+
def create_tag(model_id:, version_id:, name:)
|
|
137
|
+
raise DecisionAgent::ValidationError, "Tag name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
|
138
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless get_version(version_id: version_id)
|
|
139
|
+
|
|
140
|
+
retry_with_backoff(max_retries: 10) do
|
|
141
|
+
tag = nil
|
|
142
|
+
rule_version_tag_class.transaction do
|
|
143
|
+
existing = rule_version_tag_class.find_by(model_id: model_id, name: name)
|
|
144
|
+
if existing
|
|
145
|
+
existing.update!(version_id: version_id)
|
|
146
|
+
tag = existing.reload
|
|
147
|
+
else
|
|
148
|
+
tag = rule_version_tag_class.create!(model_id: model_id, name: name, version_id: version_id)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
serialize_tag(tag)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Retrieve a tag by name for a given model.
|
|
156
|
+
def get_tag(model_id:, name:)
|
|
157
|
+
tag = rule_version_tag_class.find_by(model_id: model_id, name: name)
|
|
158
|
+
tag ? serialize_tag(tag) : nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# List all tags for a given model, sorted by name.
|
|
162
|
+
def list_tags(model_id:)
|
|
163
|
+
rule_version_tag_class.where(model_id: model_id).order(name: :asc).map { |t| serialize_tag(t) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Delete a tag by name. Returns true if deleted, false if the tag did not exist.
|
|
167
|
+
def delete_tag(model_id:, name:)
|
|
168
|
+
tag = rule_version_tag_class.find_by(model_id: model_id, name: name)
|
|
169
|
+
return false unless tag
|
|
170
|
+
|
|
171
|
+
tag.destroy
|
|
172
|
+
true
|
|
173
|
+
end
|
|
174
|
+
|
|
134
175
|
private
|
|
135
176
|
|
|
136
177
|
def rule_version_class
|
|
@@ -143,6 +184,15 @@ module DecisionAgent
|
|
|
143
184
|
end
|
|
144
185
|
end
|
|
145
186
|
|
|
187
|
+
def rule_version_tag_class
|
|
188
|
+
if defined?(::RuleVersionTag)
|
|
189
|
+
::RuleVersionTag
|
|
190
|
+
else
|
|
191
|
+
raise DecisionAgent::ConfigurationError,
|
|
192
|
+
"RuleVersionTag model not found. Please run the versioning generator to create it."
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
146
196
|
# Retry database operations that may encounter SQLite busy exceptions
|
|
147
197
|
# This is especially important for concurrent operations on different rules
|
|
148
198
|
def retry_with_backoff(max_retries: 10, base_delay: 0.01)
|
|
@@ -157,7 +207,9 @@ module DecisionAgent
|
|
|
157
207
|
cause = e.cause
|
|
158
208
|
if cause
|
|
159
209
|
cause.class.name.include?("BusyException") ||
|
|
160
|
-
cause.class.name.include?("SQLite3::BusyException")
|
|
210
|
+
cause.class.name.include?("SQLite3::BusyException") ||
|
|
211
|
+
cause.class.name.include?("LockedException") ||
|
|
212
|
+
cause.class.name.include?("SQLite3::LockedException")
|
|
161
213
|
else
|
|
162
214
|
false
|
|
163
215
|
end
|
|
@@ -165,8 +217,10 @@ module DecisionAgent
|
|
|
165
217
|
warn "[DecisionAgent] Error checking busy exception cause: #{cause_check_error.message}"
|
|
166
218
|
false
|
|
167
219
|
end || e.message.include?("database is locked") ||
|
|
220
|
+
e.message.include?("database table is locked") ||
|
|
168
221
|
e.message.include?("SQLite3::BusyException") ||
|
|
169
|
-
e.message.include?("BusyException")
|
|
222
|
+
e.message.include?("BusyException") ||
|
|
223
|
+
e.message.include?("LockedException")
|
|
170
224
|
|
|
171
225
|
raise unless is_busy && retries < max_retries
|
|
172
226
|
|
|
@@ -201,6 +255,14 @@ module DecisionAgent
|
|
|
201
255
|
status: version.status
|
|
202
256
|
}
|
|
203
257
|
end
|
|
258
|
+
|
|
259
|
+
def serialize_tag(tag)
|
|
260
|
+
{
|
|
261
|
+
name: tag.name,
|
|
262
|
+
version_id: tag.version_id,
|
|
263
|
+
created_at: tag.updated_at || tag.created_at
|
|
264
|
+
}
|
|
265
|
+
end
|
|
204
266
|
end
|
|
205
267
|
end
|
|
206
268
|
end
|
|
@@ -82,6 +82,39 @@ module DecisionAgent
|
|
|
82
82
|
raise NotImplementedError, "#{self.class} must implement #delete_version"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# Create (or update) a named tag pointing to a specific version.
|
|
86
|
+
# Tags are unique per model; calling this with an existing name re-points the tag.
|
|
87
|
+
# @param model_id [String] The rule/model identifier
|
|
88
|
+
# @param version_id [String] The version to tag
|
|
89
|
+
# @param name [String] The tag name (e.g. "release-candidate")
|
|
90
|
+
# @return [Hash] The created/updated tag ({ name:, version_id:, created_at: })
|
|
91
|
+
def create_tag(model_id:, version_id:, name:)
|
|
92
|
+
raise NotImplementedError, "#{self.class} must implement #create_tag"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Retrieve a tag by name for a given model.
|
|
96
|
+
# @param model_id [String] The rule/model identifier
|
|
97
|
+
# @param name [String] The tag name
|
|
98
|
+
# @return [Hash, nil] The tag hash or nil if not found
|
|
99
|
+
def get_tag(model_id:, name:)
|
|
100
|
+
raise NotImplementedError, "#{self.class} must implement #get_tag"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# List all tags for a given model.
|
|
104
|
+
# @param model_id [String] The rule/model identifier
|
|
105
|
+
# @return [Array<Hash>] Array of tag hashes, sorted by name
|
|
106
|
+
def list_tags(model_id:)
|
|
107
|
+
raise NotImplementedError, "#{self.class} must implement #list_tags"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Delete a tag by name.
|
|
111
|
+
# @param model_id [String] The rule/model identifier
|
|
112
|
+
# @param name [String] The tag name
|
|
113
|
+
# @return [Boolean] True if deleted, false if tag did not exist
|
|
114
|
+
def delete_tag(model_id:, name:)
|
|
115
|
+
raise NotImplementedError, "#{self.class} must implement #delete_tag"
|
|
116
|
+
end
|
|
117
|
+
|
|
85
118
|
private
|
|
86
119
|
|
|
87
120
|
# Calculate differences between two content hashes
|
|
@@ -159,6 +159,45 @@ module DecisionAgent
|
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
def create_tag(model_id:, version_id:, name:)
|
|
163
|
+
raise DecisionAgent::ValidationError, "Tag name cannot be blank" if name.nil? || name.to_s.strip.empty?
|
|
164
|
+
|
|
165
|
+
# Validate the version exists
|
|
166
|
+
version = get_version(version_id: version_id)
|
|
167
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
168
|
+
|
|
169
|
+
with_rule_lock(model_id) do
|
|
170
|
+
tags = read_tags_unsafe(model_id)
|
|
171
|
+
tag = { name: name, version_id: version_id, created_at: Time.now.utc.iso8601 }
|
|
172
|
+
tags[name] = tag
|
|
173
|
+
write_tags_unsafe(model_id, tags)
|
|
174
|
+
tag
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def get_tag(model_id:, name:)
|
|
179
|
+
with_rule_lock(model_id) do
|
|
180
|
+
read_tags_unsafe(model_id)[name]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def list_tags(model_id:)
|
|
185
|
+
with_rule_lock(model_id) do
|
|
186
|
+
read_tags_unsafe(model_id).values.sort_by { |t| t[:name] }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def delete_tag(model_id:, name:)
|
|
191
|
+
with_rule_lock(model_id) do
|
|
192
|
+
tags = read_tags_unsafe(model_id)
|
|
193
|
+
return false unless tags.key?(name)
|
|
194
|
+
|
|
195
|
+
tags.delete(name)
|
|
196
|
+
write_tags_unsafe(model_id, tags)
|
|
197
|
+
true
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
162
201
|
def delete_version(version_id:)
|
|
163
202
|
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
164
203
|
begin
|
|
@@ -253,11 +292,13 @@ module DecisionAgent
|
|
|
253
292
|
|
|
254
293
|
return versions unless Dir.exist?(rule_dir)
|
|
255
294
|
|
|
256
|
-
Dir.glob(File.join(rule_dir, "*.json"))
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
295
|
+
Dir.glob(File.join(rule_dir, "*.json"))
|
|
296
|
+
.reject { |f| File.basename(f).start_with?("_") }
|
|
297
|
+
.each do |file|
|
|
298
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
299
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
300
|
+
# Skip corrupted or deleted files
|
|
301
|
+
next
|
|
261
302
|
end
|
|
262
303
|
|
|
263
304
|
versions.sort_by! { |v| -v[:version_number] }
|
|
@@ -268,8 +309,10 @@ module DecisionAgent
|
|
|
268
309
|
versions = []
|
|
269
310
|
return versions unless Dir.exist?(@storage_path)
|
|
270
311
|
|
|
271
|
-
Dir.glob(File.join(@storage_path, "*", "*.json"))
|
|
272
|
-
|
|
312
|
+
Dir.glob(File.join(@storage_path, "*", "*.json"))
|
|
313
|
+
.reject { |f| File.basename(f).start_with?("_") }
|
|
314
|
+
.each do |file|
|
|
315
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
273
316
|
end
|
|
274
317
|
|
|
275
318
|
versions
|
|
@@ -366,6 +409,33 @@ module DecisionAgent
|
|
|
366
409
|
@version_index.delete(version_id)
|
|
367
410
|
end
|
|
368
411
|
end
|
|
412
|
+
|
|
413
|
+
# Tags helpers — called while the rule lock is already held (unsafe = no extra lock)
|
|
414
|
+
|
|
415
|
+
def tags_filepath(model_id)
|
|
416
|
+
rule_dir = File.join(@storage_path, sanitize_filename(model_id))
|
|
417
|
+
FileUtils.mkdir_p(rule_dir)
|
|
418
|
+
File.join(rule_dir, "_tags.json")
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def read_tags_unsafe(model_id)
|
|
422
|
+
path = tags_filepath(model_id)
|
|
423
|
+
return {} unless File.exist?(path)
|
|
424
|
+
|
|
425
|
+
JSON.parse(File.read(path), symbolize_names: false)
|
|
426
|
+
.transform_values { |t| t.transform_keys(&:to_sym) }
|
|
427
|
+
rescue JSON::ParserError
|
|
428
|
+
{}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def write_tags_unsafe(model_id, tags)
|
|
432
|
+
path = tags_filepath(model_id)
|
|
433
|
+
temp = "#{path}.tmp.#{Process.pid}.#{Thread.current.object_id}"
|
|
434
|
+
File.write(temp, JSON.pretty_generate(tags))
|
|
435
|
+
File.rename(temp, path)
|
|
436
|
+
ensure
|
|
437
|
+
FileUtils.rm_f(temp)
|
|
438
|
+
end
|
|
369
439
|
end
|
|
370
440
|
end
|
|
371
441
|
end
|
|
@@ -18,8 +18,9 @@ module DecisionAgent
|
|
|
18
18
|
# @param rule_content [Hash] The rule definition
|
|
19
19
|
# @param created_by [String] User who created this version
|
|
20
20
|
# @param changelog [String] Description of changes
|
|
21
|
+
# @param tag [String, nil] Optional tag name to apply to the new version at creation time
|
|
21
22
|
# @return [Hash] The created version
|
|
22
|
-
def save_version(rule_id:, rule_content:, created_by: "system", changelog: nil)
|
|
23
|
+
def save_version(rule_id:, rule_content:, created_by: "system", changelog: nil, tag: nil)
|
|
23
24
|
validate_rule_content!(rule_content)
|
|
24
25
|
|
|
25
26
|
metadata = {
|
|
@@ -27,11 +28,15 @@ module DecisionAgent
|
|
|
27
28
|
changelog: changelog || generate_default_changelog(rule_id)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
@adapter.create_version(
|
|
31
|
+
version = @adapter.create_version(
|
|
31
32
|
rule_id: rule_id,
|
|
32
33
|
content: rule_content,
|
|
33
34
|
metadata: metadata
|
|
34
35
|
)
|
|
36
|
+
|
|
37
|
+
tag!(rule_id, version[:id], tag) if tag
|
|
38
|
+
|
|
39
|
+
version
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
# Get all versions for a rule
|
|
@@ -107,6 +112,39 @@ module DecisionAgent
|
|
|
107
112
|
@adapter.delete_version(version_id: version_id)
|
|
108
113
|
end
|
|
109
114
|
|
|
115
|
+
# Tag a specific version after the fact.
|
|
116
|
+
# Creates the tag if it does not exist; re-points it if the name is already used.
|
|
117
|
+
# @param model_id [String] The rule/model identifier
|
|
118
|
+
# @param version_id [String] The version to tag
|
|
119
|
+
# @param name [String] The tag name
|
|
120
|
+
# @return [Hash] The tag ({ name:, version_id:, created_at: })
|
|
121
|
+
def tag!(model_id, version_id, name)
|
|
122
|
+
@adapter.create_tag(model_id: model_id, version_id: version_id, name: name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Resolve a tag to its version hash, or nil if the tag does not exist.
|
|
126
|
+
# @param model_id [String] The rule/model identifier
|
|
127
|
+
# @param name [String] The tag name
|
|
128
|
+
# @return [Hash, nil] Tag hash or nil
|
|
129
|
+
def get_tag(model_id:, name:)
|
|
130
|
+
@adapter.get_tag(model_id: model_id, name: name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# List all tags for a model.
|
|
134
|
+
# @param model_id [String] The rule/model identifier
|
|
135
|
+
# @return [Array<Hash>] Tag hashes sorted by name
|
|
136
|
+
def list_tags(model_id:)
|
|
137
|
+
@adapter.list_tags(model_id: model_id)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Delete a tag by name.
|
|
141
|
+
# @param model_id [String] The rule/model identifier
|
|
142
|
+
# @param name [String] The tag name
|
|
143
|
+
# @return [Boolean] True if deleted, false if the tag did not exist
|
|
144
|
+
def delete_tag(model_id:, name:)
|
|
145
|
+
@adapter.delete_tag(model_id: model_id, name: name)
|
|
146
|
+
end
|
|
147
|
+
|
|
110
148
|
private
|
|
111
149
|
|
|
112
150
|
def default_adapter
|
|
File without changes
|
|
File without changes
|
|
@@ -70,14 +70,12 @@ module DecisionAgent
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Delete a DMN model
|
|
73
|
-
# rubocop:disable Naming/PredicateMethod
|
|
74
73
|
def delete_model(model_id)
|
|
75
74
|
@storage_mutex.synchronize do
|
|
76
75
|
@storage.delete(model_id)
|
|
77
76
|
end
|
|
78
77
|
true
|
|
79
78
|
end
|
|
80
|
-
# rubocop:enable Naming/PredicateMethod
|
|
81
79
|
|
|
82
80
|
# Add a decision to a model
|
|
83
81
|
def add_decision(model_id:, decision_id:, name:, type: "decision_table")
|
|
@@ -128,7 +126,6 @@ module DecisionAgent
|
|
|
128
126
|
end
|
|
129
127
|
|
|
130
128
|
# Delete a decision
|
|
131
|
-
# rubocop:disable Naming/PredicateMethod
|
|
132
129
|
def delete_decision(model_id:, decision_id:)
|
|
133
130
|
model = retrieve_model(model_id)
|
|
134
131
|
return false unless model
|
|
@@ -137,7 +134,6 @@ module DecisionAgent
|
|
|
137
134
|
store_model(model_id, model)
|
|
138
135
|
true
|
|
139
136
|
end
|
|
140
|
-
# rubocop:enable Naming/PredicateMethod
|
|
141
137
|
|
|
142
138
|
# Add input to decision table
|
|
143
139
|
def add_input(model_id:, decision_id:, input_id:, label:, type_ref: nil, expression: nil)
|
|
@@ -220,7 +216,6 @@ module DecisionAgent
|
|
|
220
216
|
end
|
|
221
217
|
|
|
222
218
|
# Delete rule
|
|
223
|
-
# rubocop:disable Naming/PredicateMethod
|
|
224
219
|
def delete_rule(model_id:, decision_id:, rule_id:)
|
|
225
220
|
model = retrieve_model(model_id)
|
|
226
221
|
return false unless model
|
|
@@ -232,7 +227,6 @@ module DecisionAgent
|
|
|
232
227
|
store_model(model_id, model)
|
|
233
228
|
true
|
|
234
229
|
end
|
|
235
|
-
# rubocop:enable Naming/PredicateMethod
|
|
236
230
|
|
|
237
231
|
# Validate a DMN model
|
|
238
232
|
def validate_model(model_id)
|
|
File without changes
|