decision_agent 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -0,0 +1,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