decision_agent 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Cache management helpers for ConditionEvaluator
|
|
7
|
+
module CacheHelpers
|
|
8
|
+
def self.get_cached_regex(pattern, regex_cache:, regex_cache_mutex:)
|
|
9
|
+
return pattern if pattern.is_a?(Regexp)
|
|
10
|
+
|
|
11
|
+
# Fast path: check cache without lock
|
|
12
|
+
cached = regex_cache[pattern]
|
|
13
|
+
return cached if cached
|
|
14
|
+
|
|
15
|
+
# Slow path: compile and cache
|
|
16
|
+
regex_cache_mutex.synchronize do
|
|
17
|
+
regex_cache[pattern] ||= Regexp.new(pattern.to_s)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.get_cached_path(key_path, path_cache:, path_cache_mutex:)
|
|
22
|
+
# Fast path: check cache without lock
|
|
23
|
+
cached = path_cache[key_path]
|
|
24
|
+
return cached if cached
|
|
25
|
+
|
|
26
|
+
# Slow path: split and cache
|
|
27
|
+
path_cache_mutex.synchronize do
|
|
28
|
+
path_cache[key_path] ||= key_path.to_s.split(".").freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.get_cached_date(date_string, date_cache:, date_cache_mutex:, parse_date_fast:)
|
|
33
|
+
# Fast path: check cache without lock
|
|
34
|
+
cached = date_cache[date_string]
|
|
35
|
+
return cached if cached
|
|
36
|
+
|
|
37
|
+
# Slow path: parse and cache
|
|
38
|
+
date_cache_mutex.synchronize do
|
|
39
|
+
date_cache[date_string] ||= parse_date_fast.call(date_string)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.get_cached_distance(point1, point2, geospatial_cache:, geospatial_cache_mutex:, haversine_distance:)
|
|
44
|
+
# Round coordinates to 4 decimal places (~11m precision) for cache key
|
|
45
|
+
key = [
|
|
46
|
+
point1[:lat].round(4),
|
|
47
|
+
point1[:lon].round(4),
|
|
48
|
+
point2[:lat].round(4),
|
|
49
|
+
point2[:lon].round(4)
|
|
50
|
+
].join(",")
|
|
51
|
+
|
|
52
|
+
# Fast path: check cache without lock
|
|
53
|
+
cached = geospatial_cache[key]
|
|
54
|
+
return cached if cached
|
|
55
|
+
|
|
56
|
+
# Slow path: calculate and cache
|
|
57
|
+
geospatial_cache_mutex.synchronize do
|
|
58
|
+
geospatial_cache[key] ||= haversine_distance.call(point1, point2)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.clear_caches!(regex_cache:, path_cache:, date_cache:, geospatial_cache:, param_cache:)
|
|
63
|
+
regex_cache.clear
|
|
64
|
+
path_cache.clear
|
|
65
|
+
date_cache.clear
|
|
66
|
+
geospatial_cache.clear
|
|
67
|
+
param_cache.clear
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.cache_stats(regex_cache:, path_cache:, date_cache:, geospatial_cache:, param_cache:)
|
|
71
|
+
{
|
|
72
|
+
regex: regex_cache.size,
|
|
73
|
+
path: path_cache.size,
|
|
74
|
+
date: date_cache.size,
|
|
75
|
+
geospatial: geospatial_cache.size,
|
|
76
|
+
param: param_cache.size
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Comparison helpers for ConditionEvaluator
|
|
7
|
+
module ComparisonHelpers
|
|
8
|
+
def self.compare_percentile_result(actual, params)
|
|
9
|
+
result = true
|
|
10
|
+
result &&= (actual >= params[:threshold]) if params[:threshold]
|
|
11
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
12
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
13
|
+
result &&= (actual >= params[:gte]) if params[:gte]
|
|
14
|
+
result &&= (actual <= params[:lte]) if params[:lte]
|
|
15
|
+
result &&= (actual == params[:eq]) if params[:eq]
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.compare_duration_result(actual, params)
|
|
20
|
+
result = true
|
|
21
|
+
result &&= (actual >= params[:min]) if params[:min]
|
|
22
|
+
result &&= (actual <= params[:max]) if params[:max]
|
|
23
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
24
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
25
|
+
result &&= (actual >= params[:gte]) if params[:gte]
|
|
26
|
+
result &&= (actual <= params[:lte]) if params[:lte]
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.compare_date_result?(actual, target, params)
|
|
31
|
+
if params[:compare]
|
|
32
|
+
case params[:compare].to_s
|
|
33
|
+
when "eq", "=="
|
|
34
|
+
(actual - target).abs < 1
|
|
35
|
+
when "gt", ">"
|
|
36
|
+
actual > target
|
|
37
|
+
when "lt", "<"
|
|
38
|
+
actual < target
|
|
39
|
+
when "gte", ">="
|
|
40
|
+
actual >= target
|
|
41
|
+
when "lte", "<="
|
|
42
|
+
actual <= target
|
|
43
|
+
else
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
elsif params[:eq]
|
|
47
|
+
(actual - target).abs < 1
|
|
48
|
+
elsif params[:gt]
|
|
49
|
+
actual > target
|
|
50
|
+
elsif params[:lt]
|
|
51
|
+
actual < target
|
|
52
|
+
elsif params[:gte]
|
|
53
|
+
actual >= target
|
|
54
|
+
elsif params[:lte]
|
|
55
|
+
actual <= target
|
|
56
|
+
else
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.compare_moving_window_result(actual, params)
|
|
62
|
+
result = true
|
|
63
|
+
result &&= (actual >= params[:threshold]) if params[:threshold]
|
|
64
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
65
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
66
|
+
result &&= (actual >= params[:gte]) if params[:gte]
|
|
67
|
+
result &&= (actual <= params[:lte]) if params[:lte]
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.compare_financial_result(actual, params)
|
|
72
|
+
result = true
|
|
73
|
+
result &&= (actual >= params[:threshold]) if params[:threshold]
|
|
74
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
75
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.compare_numeric_with_hash(actual, expected)
|
|
80
|
+
comparisons = [
|
|
81
|
+
[:min, ->(val, threshold) { val >= threshold }],
|
|
82
|
+
[:max, ->(val, threshold) { val <= threshold }],
|
|
83
|
+
[:gt, ->(val, threshold) { val > threshold }],
|
|
84
|
+
[:lt, ->(val, threshold) { val < threshold }],
|
|
85
|
+
[:gte, ->(val, threshold) { val >= threshold }],
|
|
86
|
+
[:lte, ->(val, threshold) { val <= threshold }],
|
|
87
|
+
[:eq, ->(val, threshold) { val == threshold }]
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
comparisons.all? do |key, comparison|
|
|
91
|
+
threshold = expected[key] || expected[key.to_s]
|
|
92
|
+
threshold.nil? || comparison.call(actual, threshold)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Date/time helper methods for ConditionEvaluator
|
|
7
|
+
module DateHelpers
|
|
8
|
+
def self.parse_date_fast(date_string)
|
|
9
|
+
return nil unless date_string.is_a?(String)
|
|
10
|
+
|
|
11
|
+
# Fast-path: ISO8601 date format (YYYY-MM-DD)
|
|
12
|
+
if date_string.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
13
|
+
year, month, day = date_string.split("-").map(&:to_i)
|
|
14
|
+
begin
|
|
15
|
+
return Time.new(year, month, day)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
warn "[DecisionAgent] Failed to parse date '#{date_string}': #{e.message}"
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fast-path: ISO8601 datetime format (YYYY-MM-DDTHH:MM:SS or YYYY-MM-DDTHH:MM:SSZ)
|
|
23
|
+
if date_string.match?(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
|
24
|
+
begin
|
|
25
|
+
# Try ISO8601 parsing first (faster than Time.parse for this format)
|
|
26
|
+
return Time.iso8601(date_string)
|
|
27
|
+
rescue ArgumentError
|
|
28
|
+
# Fall through to Time.parse
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fallback to Time.parse for other formats
|
|
33
|
+
Time.parse(date_string)
|
|
34
|
+
rescue ArgumentError, TypeError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse_date(value, get_cached_date:)
|
|
39
|
+
case value
|
|
40
|
+
when Time, Date, DateTime
|
|
41
|
+
value
|
|
42
|
+
when String
|
|
43
|
+
get_cached_date.call(value)
|
|
44
|
+
end
|
|
45
|
+
rescue ArgumentError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
ALLOWED_COMPARISON_OPERATORS = %i[< > <= >= ==].freeze
|
|
50
|
+
|
|
51
|
+
def self.compare_dates(actual_value, expected_value, operator, parse_date:)
|
|
52
|
+
return false unless actual_value && expected_value
|
|
53
|
+
|
|
54
|
+
op = operator.to_sym
|
|
55
|
+
raise ArgumentError, "Unsupported comparison operator: #{operator}" unless ALLOWED_COMPARISON_OPERATORS.include?(op)
|
|
56
|
+
|
|
57
|
+
# Fast path: Both are already Time/Date objects (no parsing needed)
|
|
58
|
+
actual_is_date = actual_value.is_a?(Time) || actual_value.is_a?(Date) || actual_value.is_a?(DateTime)
|
|
59
|
+
expected_is_date = expected_value.is_a?(Time) || expected_value.is_a?(Date) || expected_value.is_a?(DateTime)
|
|
60
|
+
return actual_value.public_send(op, expected_value) if actual_is_date && expected_is_date
|
|
61
|
+
|
|
62
|
+
# Slow path: Parse dates (with caching)
|
|
63
|
+
actual_date = parse_date.call(actual_value)
|
|
64
|
+
expected_date = parse_date.call(expected_value)
|
|
65
|
+
|
|
66
|
+
return false unless actual_date && expected_date
|
|
67
|
+
|
|
68
|
+
actual_date.public_send(op, expected_date)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.normalize_day_of_week(value)
|
|
72
|
+
case value
|
|
73
|
+
when Numeric
|
|
74
|
+
value.to_i % 7
|
|
75
|
+
when String
|
|
76
|
+
day_map = {
|
|
77
|
+
"sunday" => 0, "sun" => 0,
|
|
78
|
+
"monday" => 1, "mon" => 1,
|
|
79
|
+
"tuesday" => 2, "tue" => 2,
|
|
80
|
+
"wednesday" => 3, "wed" => 3,
|
|
81
|
+
"thursday" => 4, "thu" => 4,
|
|
82
|
+
"friday" => 5, "fri" => 5,
|
|
83
|
+
"saturday" => 6, "sat" => 6
|
|
84
|
+
}
|
|
85
|
+
day_map[value.downcase]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Geospatial helper methods for ConditionEvaluator
|
|
7
|
+
module GeospatialHelpers
|
|
8
|
+
def self.parse_coordinates(value)
|
|
9
|
+
case value
|
|
10
|
+
when Hash
|
|
11
|
+
lat = value["lat"] || value[:lat] || value["latitude"] || value[:latitude]
|
|
12
|
+
lon = value["lon"] || value[:lon] || value["lng"] || value[:lng] ||
|
|
13
|
+
value["longitude"] || value[:longitude]
|
|
14
|
+
return nil unless lat && lon
|
|
15
|
+
|
|
16
|
+
{ lat: lat.to_f, lon: lon.to_f }
|
|
17
|
+
when Array
|
|
18
|
+
return nil unless value.size == 2
|
|
19
|
+
|
|
20
|
+
{ lat: value[0].to_f, lon: value[1].to_f }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.parse_radius_params(value, parse_coordinates:)
|
|
25
|
+
return nil unless value.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
center_data = value["center"] || value[:center]
|
|
28
|
+
radius = value["radius"] || value[:radius]
|
|
29
|
+
|
|
30
|
+
return nil unless center_data && radius
|
|
31
|
+
|
|
32
|
+
center = parse_coordinates.call(center_data)
|
|
33
|
+
return nil unless center
|
|
34
|
+
|
|
35
|
+
{ center: center, radius: radius.to_f }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse_polygon(value, parse_coordinates:)
|
|
39
|
+
return nil unless value.is_a?(Array)
|
|
40
|
+
|
|
41
|
+
value.map { |vertex| parse_coordinates.call(vertex) }.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.haversine_distance(point1, point2)
|
|
45
|
+
earth_radius_km = 6371.0
|
|
46
|
+
|
|
47
|
+
lat1_rad = (point1[:lat] * Math::PI) / 180
|
|
48
|
+
lat2_rad = (point2[:lat] * Math::PI) / 180
|
|
49
|
+
delta_lat = ((point2[:lat] - point1[:lat]) * Math::PI) / 180
|
|
50
|
+
delta_lon = ((point2[:lon] - point1[:lon]) * Math::PI) / 180
|
|
51
|
+
|
|
52
|
+
haversine_a = (Math.sin(delta_lat / 2)**2) +
|
|
53
|
+
(Math.cos(lat1_rad) * Math.cos(lat2_rad) *
|
|
54
|
+
(Math.sin(delta_lon / 2)**2))
|
|
55
|
+
|
|
56
|
+
haversine_c = 2 * Math.atan2(Math.sqrt(haversine_a), Math.sqrt(1 - haversine_a))
|
|
57
|
+
|
|
58
|
+
earth_radius_km * haversine_c
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.point_in_polygon?(point, polygon)
|
|
62
|
+
return false if polygon.size < 3
|
|
63
|
+
|
|
64
|
+
inside = false
|
|
65
|
+
j = polygon.size - 1
|
|
66
|
+
|
|
67
|
+
(0...polygon.size).each do |i|
|
|
68
|
+
xi = polygon[i][:lat]
|
|
69
|
+
yi = polygon[i][:lon]
|
|
70
|
+
xj = polygon[j][:lat]
|
|
71
|
+
yj = polygon[j][:lon]
|
|
72
|
+
|
|
73
|
+
intersect = ((yi > point[:lon]) != (yj > point[:lon])) &&
|
|
74
|
+
(point[:lat] < ((xj - xi) * (point[:lon] - yi) / (yj - yi)) + xi)
|
|
75
|
+
|
|
76
|
+
inside = !inside if intersect
|
|
77
|
+
j = i
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
inside
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Operator evaluation helpers for ConditionEvaluator
|
|
7
|
+
module OperatorEvaluationHelpers
|
|
8
|
+
# Evaluates operator using mixins (in order of most common to least common)
|
|
9
|
+
# Returns the result from the first mixin that handles the operator, or false if unknown
|
|
10
|
+
def self.evaluate_operator(op, actual_value, expected_value, context_hash, regex_cache:, regex_cache_mutex:, param_cache:,
|
|
11
|
+
param_cache_mutex:, geospatial_cache:, geospatial_cache_mutex:)
|
|
12
|
+
# Try basic operators first (most common)
|
|
13
|
+
result = try_basic_operators(
|
|
14
|
+
op, actual_value, expected_value,
|
|
15
|
+
regex_cache: regex_cache,
|
|
16
|
+
regex_cache_mutex: regex_cache_mutex,
|
|
17
|
+
param_cache: param_cache,
|
|
18
|
+
param_cache_mutex: param_cache_mutex
|
|
19
|
+
)
|
|
20
|
+
return result unless result.nil?
|
|
21
|
+
|
|
22
|
+
# Try mathematical and statistical operators
|
|
23
|
+
result = try_math_and_statistical_operators(
|
|
24
|
+
op, actual_value, expected_value,
|
|
25
|
+
param_cache: param_cache,
|
|
26
|
+
param_cache_mutex: param_cache_mutex
|
|
27
|
+
)
|
|
28
|
+
return result unless result.nil?
|
|
29
|
+
|
|
30
|
+
# Try date/time operators
|
|
31
|
+
result = try_datetime_operators(
|
|
32
|
+
op, actual_value, expected_value, context_hash,
|
|
33
|
+
param_cache: param_cache,
|
|
34
|
+
param_cache_mutex: param_cache_mutex
|
|
35
|
+
)
|
|
36
|
+
return result unless result.nil?
|
|
37
|
+
|
|
38
|
+
# Try advanced operators (rate, moving window, financial)
|
|
39
|
+
result = try_advanced_operators(
|
|
40
|
+
op, actual_value, expected_value,
|
|
41
|
+
param_cache: param_cache,
|
|
42
|
+
param_cache_mutex: param_cache_mutex
|
|
43
|
+
)
|
|
44
|
+
return result unless result.nil?
|
|
45
|
+
|
|
46
|
+
# Try collection and aggregation operators
|
|
47
|
+
result = try_collection_and_aggregation_operators(
|
|
48
|
+
op, actual_value, expected_value,
|
|
49
|
+
param_cache: param_cache,
|
|
50
|
+
param_cache_mutex: param_cache_mutex
|
|
51
|
+
)
|
|
52
|
+
return result unless result.nil?
|
|
53
|
+
|
|
54
|
+
# Try special operators (geospatial, data enrichment)
|
|
55
|
+
result = try_special_operators(
|
|
56
|
+
op, actual_value, expected_value, context_hash,
|
|
57
|
+
geospatial_cache: geospatial_cache,
|
|
58
|
+
geospatial_cache_mutex: geospatial_cache_mutex
|
|
59
|
+
)
|
|
60
|
+
return result unless result.nil?
|
|
61
|
+
|
|
62
|
+
# Unknown operator - returns false (fail-safe)
|
|
63
|
+
# Note: Validation should catch this earlier
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.try_basic_operators(op, actual_value, expected_value, regex_cache:, regex_cache_mutex:, param_cache:, param_cache_mutex:)
|
|
68
|
+
result = Operators::BasicComparisonOperators.handle(op, actual_value, expected_value)
|
|
69
|
+
return result unless result.nil?
|
|
70
|
+
|
|
71
|
+
result = Operators::StringOperators.handle(
|
|
72
|
+
op, actual_value, expected_value,
|
|
73
|
+
regex_cache: regex_cache,
|
|
74
|
+
regex_cache_mutex: regex_cache_mutex
|
|
75
|
+
)
|
|
76
|
+
return result unless result.nil?
|
|
77
|
+
|
|
78
|
+
Operators::NumericOperators.handle(
|
|
79
|
+
op, actual_value, expected_value,
|
|
80
|
+
param_cache: param_cache,
|
|
81
|
+
param_cache_mutex: param_cache_mutex
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.try_math_and_statistical_operators(op, actual_value, expected_value, param_cache:, param_cache_mutex:)
|
|
86
|
+
result = Operators::MathematicalOperators.handle(
|
|
87
|
+
op, actual_value, expected_value,
|
|
88
|
+
param_cache: param_cache,
|
|
89
|
+
param_cache_mutex: param_cache_mutex
|
|
90
|
+
)
|
|
91
|
+
return result unless result.nil?
|
|
92
|
+
|
|
93
|
+
Operators::StatisticalAggregations.handle(
|
|
94
|
+
op, actual_value, expected_value,
|
|
95
|
+
param_cache: param_cache,
|
|
96
|
+
param_cache_mutex: param_cache_mutex
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.try_datetime_operators(op, actual_value, expected_value, context_hash, param_cache:, param_cache_mutex:)
|
|
101
|
+
result = Operators::DateTimeOperators.handle(op, actual_value, expected_value)
|
|
102
|
+
return result unless result.nil?
|
|
103
|
+
|
|
104
|
+
result = Operators::DurationOperators.handle(
|
|
105
|
+
op, actual_value, expected_value, context_hash,
|
|
106
|
+
param_cache: param_cache,
|
|
107
|
+
param_cache_mutex: param_cache_mutex
|
|
108
|
+
)
|
|
109
|
+
return result unless result.nil?
|
|
110
|
+
|
|
111
|
+
result = Operators::DateArithmeticOperators.handle(
|
|
112
|
+
op, actual_value, expected_value, context_hash,
|
|
113
|
+
param_cache: param_cache,
|
|
114
|
+
param_cache_mutex: param_cache_mutex
|
|
115
|
+
)
|
|
116
|
+
return result unless result.nil?
|
|
117
|
+
|
|
118
|
+
Operators::TimeComponentOperators.handle(op, actual_value, expected_value)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.try_advanced_operators(op, actual_value, expected_value, param_cache:, param_cache_mutex:)
|
|
122
|
+
result = Operators::RateOperators.handle(op, actual_value, expected_value)
|
|
123
|
+
return result unless result.nil?
|
|
124
|
+
|
|
125
|
+
result = Operators::MovingWindowOperators.handle(
|
|
126
|
+
op, actual_value, expected_value,
|
|
127
|
+
param_cache: param_cache,
|
|
128
|
+
param_cache_mutex: param_cache_mutex
|
|
129
|
+
)
|
|
130
|
+
return result unless result.nil?
|
|
131
|
+
|
|
132
|
+
Operators::FinancialOperators.handle(
|
|
133
|
+
op, actual_value, expected_value,
|
|
134
|
+
param_cache: param_cache,
|
|
135
|
+
param_cache_mutex: param_cache_mutex
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.try_collection_and_aggregation_operators(op, actual_value, expected_value, param_cache:, param_cache_mutex:)
|
|
140
|
+
result = Operators::StringAggregations.handle(
|
|
141
|
+
op, actual_value, expected_value,
|
|
142
|
+
param_cache: param_cache,
|
|
143
|
+
param_cache_mutex: param_cache_mutex
|
|
144
|
+
)
|
|
145
|
+
return result unless result.nil?
|
|
146
|
+
|
|
147
|
+
Operators::CollectionOperators.handle(op, actual_value, expected_value)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.try_special_operators(op, actual_value, expected_value, _context_hash, geospatial_cache:, geospatial_cache_mutex:)
|
|
151
|
+
Operators::GeospatialOperators.handle(
|
|
152
|
+
op, actual_value, expected_value,
|
|
153
|
+
geospatial_cache: geospatial_cache,
|
|
154
|
+
geospatial_cache_mutex: geospatial_cache_mutex
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|