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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93d5c128215a2a4a834f609111870adafa6f74bebdc05ada319399e5945f13de
|
|
4
|
+
data.tar.gz: 61133113389fd9da5b52833d4c2d640f3a7b4eb866979da9d4c783d4a2f95468
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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** -
|
|
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
|
-
- **
|
|
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
|
data/lib/decision_agent/agent.rb
CHANGED
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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?(
|
|
65
|
-
(
|
|
64
|
+
def self.epsilon_equal?(lhs, rhs, epsilon = 1e-10)
|
|
65
|
+
(lhs - rhs).abs < epsilon
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
87
|
+
rule_traces << Explainability::RuleTrace.new(
|
|
81
88
|
rule_id: rule_id,
|
|
82
|
-
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
|
|
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
|