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,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles geospatial operators: within_radius, in_polygon
7
+ module GeospatialOperators
8
+ def self.handle(op, actual_value, expected_value, geospatial_cache: nil, geospatial_cache_mutex: nil)
9
+ case op
10
+ when "within_radius"
11
+ # Checks if point is within radius of center point
12
+ point = parse_coordinates(actual_value)
13
+ return false unless point
14
+
15
+ params = parse_radius_params(expected_value)
16
+ return false unless params
17
+
18
+ # Cache geospatial distance calculations
19
+ distance = get_cached_distance(point, params[:center], geospatial_cache: geospatial_cache,
20
+ geospatial_cache_mutex: geospatial_cache_mutex)
21
+ distance <= params[:radius]
22
+
23
+ when "in_polygon"
24
+ # Checks if point is inside a polygon using ray casting algorithm
25
+ point = parse_coordinates(actual_value)
26
+ return false unless point
27
+
28
+ polygon = parse_polygon(expected_value)
29
+ return false unless polygon
30
+ return false if polygon.size < 3 # Need at least 3 vertices
31
+
32
+ point_in_polygon?(point, polygon)
33
+ end
34
+ # Returns nil if not handled by this module
35
+ end
36
+
37
+ # Parse coordinates from various formats
38
+ def self.parse_coordinates(value)
39
+ case value
40
+ when Hash
41
+ lat = value[:lat] || value["lat"]
42
+ lon = value[:lon] || value["lon"]
43
+ return nil unless lat && lon
44
+
45
+ { lat: lat.to_f, lon: lon.to_f }
46
+ when Array
47
+ return nil unless value.size >= 2
48
+
49
+ { lat: value[0].to_f, lon: value[1].to_f }
50
+ end
51
+ end
52
+
53
+ # Parse radius parameters
54
+ def self.parse_radius_params(value)
55
+ return nil unless value.is_a?(Hash)
56
+
57
+ center = value[:center] || value["center"]
58
+ radius = value[:radius] || value["radius"]
59
+ return nil unless center && radius
60
+
61
+ center_coords = parse_coordinates(center)
62
+ return nil unless center_coords
63
+
64
+ { center: center_coords, radius: radius.to_f }
65
+ end
66
+
67
+ # Parse polygon from various formats
68
+ def self.parse_polygon(value)
69
+ return nil unless value.is_a?(Array)
70
+ return nil if value.empty?
71
+
72
+ value.map { |v| parse_coordinates(v) }.compact
73
+ end
74
+
75
+ # Get cached distance between two points
76
+ def self.get_cached_distance(point1, point2, geospatial_cache: nil, geospatial_cache_mutex: nil)
77
+ cache = geospatial_cache
78
+ mutex = geospatial_cache_mutex
79
+ if cache.nil? || mutex.nil?
80
+ cache = ConditionEvaluator.instance_variable_get(:@geospatial_cache)
81
+ mutex = ConditionEvaluator.instance_variable_get(:@geospatial_cache_mutex)
82
+ end
83
+
84
+ # Create cache key from sorted coordinates
85
+ key = [[point1[:lat], point1[:lon]], [point2[:lat], point2[:lon]]].sort.hash
86
+ cached = cache[key]
87
+ return cached if cached
88
+
89
+ mutex.synchronize do
90
+ cache[key] ||= calculate_distance(point1, point2)
91
+ end
92
+ end
93
+
94
+ # Calculate distance between two points using Haversine formula
95
+ def self.calculate_distance(point1, point2)
96
+ Helpers::GeospatialHelpers.haversine_distance(point1, point2)
97
+ end
98
+
99
+ # Check if point is inside polygon using ray casting algorithm
100
+ def self.point_in_polygon?(point, polygon)
101
+ Helpers::GeospatialHelpers.point_in_polygon?(point, polygon)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles mathematical operators: trigonometric, exponential, logarithmic, rounding, etc.
7
+ module MathematicalOperators
8
+ def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ # Trigonometric functions
11
+ when "sin"
12
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
13
+
14
+ Base.epsilon_equal?(Math.sin(actual_value), expected_value)
15
+ when "cos"
16
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
17
+
18
+ Base.epsilon_equal?(Math.cos(actual_value), expected_value)
19
+ when "tan"
20
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
21
+
22
+ Base.epsilon_equal?(Math.tan(actual_value), expected_value)
23
+ when "asin"
24
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
25
+ return false if actual_value < -1 || actual_value > 1
26
+
27
+ Base.epsilon_equal?(Math.asin(actual_value), expected_value)
28
+ when "acos"
29
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
30
+ return false if actual_value < -1 || actual_value > 1
31
+
32
+ Base.epsilon_equal?(Math.acos(actual_value), expected_value)
33
+ when "atan"
34
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
35
+
36
+ Base.epsilon_equal?(Math.atan(actual_value), expected_value)
37
+ when "atan2"
38
+ return false unless actual_value.is_a?(Numeric)
39
+
40
+ params = parse_atan2_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
41
+ return false unless params
42
+
43
+ Base.epsilon_equal?(Math.atan2(actual_value, params[:y]), params[:result])
44
+ when "sinh"
45
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
46
+
47
+ Base.epsilon_equal?(Math.sinh(actual_value), expected_value)
48
+ when "cosh"
49
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
50
+
51
+ Base.epsilon_equal?(Math.cosh(actual_value), expected_value)
52
+ when "tanh"
53
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
54
+
55
+ Base.epsilon_equal?(Math.tanh(actual_value), expected_value)
56
+
57
+ # Exponential and logarithmic functions
58
+ when "sqrt"
59
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
60
+ return false if actual_value.negative?
61
+
62
+ Base.epsilon_equal?(Math.sqrt(actual_value), expected_value)
63
+ when "cbrt"
64
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
65
+
66
+ result = if actual_value.negative?
67
+ -((-actual_value)**(1.0 / 3))
68
+ else
69
+ actual_value**(1.0 / 3)
70
+ end
71
+ Base.epsilon_equal?(result, expected_value)
72
+ when "power"
73
+ return false unless actual_value.is_a?(Numeric)
74
+
75
+ params = parse_power_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
76
+ return false unless params
77
+
78
+ Base.epsilon_equal?(actual_value**params[:exponent], params[:result])
79
+ when "exp"
80
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
81
+
82
+ Base.epsilon_equal?(Math.exp(actual_value), expected_value)
83
+ when "log"
84
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
85
+ return false if actual_value <= 0
86
+
87
+ Base.epsilon_equal?(Math.log(actual_value), expected_value)
88
+ when "log10"
89
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
90
+ return false if actual_value <= 0
91
+
92
+ Base.epsilon_equal?(Math.log10(actual_value), expected_value)
93
+ when "log2"
94
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
95
+ return false if actual_value <= 0
96
+
97
+ Base.epsilon_equal?(Math.log(actual_value) / Math.log(2), expected_value)
98
+
99
+ # Rounding and absolute value functions
100
+ when "round"
101
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
102
+
103
+ actual_value.round == expected_value
104
+ when "floor"
105
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
106
+
107
+ actual_value.floor == expected_value
108
+ when "ceil"
109
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
110
+
111
+ actual_value.ceil == expected_value
112
+ when "abs"
113
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
114
+
115
+ actual_value.abs == expected_value
116
+ when "truncate"
117
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
118
+
119
+ actual_value.truncate == expected_value
120
+
121
+ # Advanced mathematical functions
122
+ when "factorial"
123
+ return false unless actual_value.is_a?(Numeric) && expected_value.is_a?(Numeric)
124
+ return false if actual_value.negative? || !actual_value.integer?
125
+
126
+ (1..actual_value.to_i).reduce(1, :*) == expected_value
127
+ when "gcd"
128
+ return false unless actual_value.is_a?(Numeric) && actual_value.integer?
129
+
130
+ params = parse_gcd_lcm_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
131
+ return false unless params && params[:other].integer?
132
+
133
+ actual_value.to_i.gcd(params[:other].to_i) == params[:result]
134
+ when "lcm"
135
+ return false unless actual_value.is_a?(Numeric) && actual_value.integer?
136
+
137
+ params = parse_gcd_lcm_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
138
+ return false unless params && params[:other].integer?
139
+
140
+ actual_value.to_i.lcm(params[:other].to_i) == params[:result]
141
+ end
142
+ # Returns nil if not handled by this module
143
+ end
144
+
145
+ # Parse atan2 parameters
146
+ def self.parse_atan2_params(value, param_cache: nil, param_cache_mutex: nil)
147
+ normalized = Base.normalize_params_to_hash(value, %i[y result])
148
+ return nil unless normalized.is_a?(Hash)
149
+
150
+ cache = param_cache
151
+ mutex = param_cache_mutex
152
+ if cache.nil? || mutex.nil?
153
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
154
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
155
+ end
156
+
157
+ cache_key = Base.normalize_param_cache_key(normalized, "atan2")
158
+ cached = cache[cache_key]
159
+ return cached if cached
160
+
161
+ mutex.synchronize do
162
+ cache[cache_key] ||= parse_atan2_params_impl(normalized)
163
+ end
164
+ end
165
+
166
+ def self.parse_atan2_params_impl(value)
167
+ y = value[:y] || value["y"]
168
+ result = value[:result] || value["result"]
169
+ return nil unless y && !result.nil?
170
+
171
+ { y: y, result: result }
172
+ end
173
+
174
+ # Parse power parameters
175
+ def self.parse_power_params(value, param_cache: nil, param_cache_mutex: nil)
176
+ normalized = Base.normalize_params_to_hash(value, %i[exponent result])
177
+ return nil unless normalized.is_a?(Hash)
178
+
179
+ cache = param_cache
180
+ mutex = param_cache_mutex
181
+ if cache.nil? || mutex.nil?
182
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
183
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
184
+ end
185
+
186
+ cache_key = Base.normalize_param_cache_key(normalized, "power")
187
+ cached = cache[cache_key]
188
+ return cached if cached
189
+
190
+ mutex.synchronize do
191
+ cache[cache_key] ||= parse_power_params_impl(normalized)
192
+ end
193
+ end
194
+
195
+ def self.parse_power_params_impl(value)
196
+ exponent = value[:exponent] || value["exponent"]
197
+ result = value[:result] || value["result"]
198
+ return nil unless exponent && !result.nil?
199
+
200
+ { exponent: exponent, result: result }
201
+ end
202
+
203
+ # Parse gcd/lcm parameters
204
+ def self.parse_gcd_lcm_params(value, param_cache: nil, param_cache_mutex: nil)
205
+ normalized = Base.normalize_params_to_hash(value, %i[other result])
206
+ return nil unless normalized.is_a?(Hash)
207
+
208
+ cache = param_cache
209
+ mutex = param_cache_mutex
210
+ if cache.nil? || mutex.nil?
211
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
212
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
213
+ end
214
+
215
+ cache_key = Base.normalize_param_cache_key(normalized, "gcd_lcm")
216
+ cached = cache[cache_key]
217
+ return cached if cached
218
+
219
+ mutex.synchronize do
220
+ cache[cache_key] ||= parse_gcd_lcm_params_impl(normalized)
221
+ end
222
+ end
223
+
224
+ def self.parse_gcd_lcm_params_impl(value)
225
+ other = value[:other] || value["other"]
226
+ result = value[:result] || value["result"]
227
+ return nil unless other && !result.nil?
228
+
229
+ { other: other, result: result }
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles moving window calculation operators: moving_average, moving_sum, moving_max, moving_min
7
+ module MovingWindowOperators
8
+ def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ when "moving_average"
11
+ # Calculates moving average over window
12
+ return false unless actual_value.is_a?(Array)
13
+ return false if actual_value.empty?
14
+
15
+ numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
16
+ return false if numeric_array.empty?
17
+
18
+ params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
19
+ return false unless params
20
+
21
+ window = [params[:window], numeric_array.size].min
22
+ return false if window < 1
23
+
24
+ window_array = numeric_array.slice(-window, window)
25
+ moving_avg = window_array.sum.to_f / window
26
+ compare_moving_window_result(moving_avg, params)
27
+
28
+ when "moving_sum"
29
+ # Calculates moving sum over window
30
+ return false unless actual_value.is_a?(Array)
31
+ return false if actual_value.empty?
32
+
33
+ numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
34
+ return false if numeric_array.empty?
35
+
36
+ params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
37
+ return false unless params
38
+
39
+ window = [params[:window], numeric_array.size].min
40
+ return false if window < 1
41
+
42
+ window_array = numeric_array.slice(-window, window)
43
+ moving_sum = window_array.sum
44
+ compare_moving_window_result(moving_sum, params)
45
+
46
+ when "moving_max"
47
+ # Calculates moving max over window
48
+ return false unless actual_value.is_a?(Array)
49
+ return false if actual_value.empty?
50
+
51
+ numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
52
+ return false if numeric_array.empty?
53
+
54
+ params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
55
+ return false unless params
56
+
57
+ window = [params[:window], numeric_array.size].min
58
+ return false if window < 1
59
+
60
+ window_array = numeric_array.slice(-window, window)
61
+ moving_max = window_array.max
62
+ compare_moving_window_result(moving_max, params)
63
+
64
+ when "moving_min"
65
+ # Calculates moving min over window
66
+ return false unless actual_value.is_a?(Array)
67
+ return false if actual_value.empty?
68
+
69
+ numeric_array = actual_value.select { |v| v.is_a?(Numeric) }
70
+ return false if numeric_array.empty?
71
+
72
+ params = parse_moving_window_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
73
+ return false unless params
74
+
75
+ window = [params[:window], numeric_array.size].min
76
+ return false if window < 1
77
+
78
+ window_array = numeric_array.slice(-window, window)
79
+ moving_min = window_array.min
80
+ compare_moving_window_result(moving_min, params)
81
+ end
82
+ # Returns nil if not handled by this module
83
+ end
84
+
85
+ # Parse moving window parameters
86
+ def self.parse_moving_window_params(value, param_cache: nil, param_cache_mutex: nil)
87
+ return nil unless value.is_a?(Hash)
88
+
89
+ # Normalize to hash (already a hash, but normalize keys)
90
+ normalized = Base.normalize_params_to_hash(value, [])
91
+
92
+ cache = param_cache
93
+ mutex = param_cache_mutex
94
+ if cache.nil? || mutex.nil?
95
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
96
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
97
+ end
98
+
99
+ cache_key = Base.normalize_param_cache_key(normalized, "moving_window")
100
+ cached = cache[cache_key]
101
+ return cached if cached
102
+
103
+ mutex.synchronize do
104
+ cache[cache_key] ||= parse_moving_window_params_impl(normalized)
105
+ end
106
+ end
107
+
108
+ def self.parse_moving_window_params_impl(value)
109
+ window = value[:window] || value["window"]
110
+ return nil unless window.is_a?(Numeric) && window.positive?
111
+
112
+ {
113
+ window: window.to_i,
114
+ threshold: value[:threshold] || value["threshold"],
115
+ gt: value[:gt] || value["gt"],
116
+ lt: value[:lt] || value["lt"],
117
+ gte: value[:gte] || value["gte"],
118
+ lte: value[:lte] || value["lte"]
119
+ }
120
+ end
121
+
122
+ # Compare moving window result
123
+ def self.compare_moving_window_result(actual, params)
124
+ result = true
125
+ result &&= (actual >= params[:threshold]) if params[:threshold]
126
+ result &&= (actual > params[:gt]) if params[:gt]
127
+ result &&= (actual < params[:lt]) if params[:lt]
128
+ result &&= (actual >= params[:gte]) if params[:gte]
129
+ result &&= (actual <= params[:lte]) if params[:lte]
130
+ result
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles numeric operators: between, modulo
7
+ module NumericOperators
8
+ def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ when "between"
11
+ # Checks if numeric value is between min and max (inclusive)
12
+ # expected_value should be [min, max] or {min: x, max: y}
13
+ if actual_value.is_a?(Numeric)
14
+ range = parse_range(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
15
+ range ? actual_value.between?(range[:min], range[:max]) : false
16
+ else
17
+ false
18
+ end
19
+
20
+ when "modulo"
21
+ # Checks if value modulo divisor equals remainder
22
+ # expected_value should be [divisor, remainder] or {divisor: x, remainder: y}
23
+ if actual_value.is_a?(Numeric)
24
+ params = parse_modulo_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
25
+ params ? (actual_value % params[:divisor]) == params[:remainder] : false
26
+ else
27
+ false
28
+ end
29
+ end
30
+ # Returns nil if not handled by this module
31
+ end
32
+
33
+ # Parse range for 'between' operator
34
+ # Accepts [min, max] or {min: x, max: y}
35
+ # Converts arrays to hash for consistency and better performance
36
+ def self.parse_range(value, param_cache: nil, param_cache_mutex: nil)
37
+ # Normalize to hash if array (for large params, hash is more efficient)
38
+ normalized_value = normalize_params_to_hash(value, %i[min max])
39
+
40
+ # Use provided caches or access ConditionEvaluator class variables
41
+ cache = param_cache
42
+ mutex = param_cache_mutex
43
+
44
+ if cache.nil? || mutex.nil?
45
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
46
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
47
+ end
48
+
49
+ cache_key = normalize_param_cache_key(normalized_value, "range")
50
+
51
+ # Fast path: check cache without lock
52
+ cached = cache[cache_key]
53
+ return cached if cached
54
+
55
+ # Slow path: parse and cache
56
+ mutex.synchronize do
57
+ cache[cache_key] ||= parse_range_impl(normalized_value)
58
+ end
59
+ end
60
+
61
+ def self.parse_range_impl(value)
62
+ return nil unless value.is_a?(Hash)
63
+
64
+ min = value[:min] || value["min"]
65
+ max = value[:max] || value["max"]
66
+ return nil unless min && max
67
+
68
+ { min: min, max: max }
69
+ end
70
+
71
+ # Parse modulo parameters
72
+ # Accepts [divisor, remainder] or {divisor: x, remainder: y}
73
+ # Converts arrays to hash for consistency and better performance
74
+ def self.parse_modulo_params(value, param_cache: nil, param_cache_mutex: nil)
75
+ # Normalize to hash if array (for large params, hash is more efficient)
76
+ normalized_value = normalize_params_to_hash(value, %i[divisor remainder])
77
+
78
+ # Use provided caches or access ConditionEvaluator class variables
79
+ cache = param_cache
80
+ mutex = param_cache_mutex
81
+
82
+ if cache.nil? || mutex.nil?
83
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
84
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
85
+ end
86
+
87
+ cache_key = normalize_param_cache_key(normalized_value, "modulo")
88
+
89
+ # Fast path: check cache without lock
90
+ cached = cache[cache_key]
91
+ return cached if cached
92
+
93
+ # Slow path: parse and cache
94
+ mutex.synchronize do
95
+ cache[cache_key] ||= parse_modulo_params_impl(normalized_value)
96
+ end
97
+ end
98
+
99
+ def self.parse_modulo_params_impl(value)
100
+ return nil unless value.is_a?(Hash)
101
+
102
+ divisor = value[:divisor] || value["divisor"]
103
+ remainder = value[:remainder] || value["remainder"]
104
+ return nil unless divisor && !remainder.nil?
105
+
106
+ { divisor: divisor, remainder: remainder }
107
+ end
108
+
109
+ # Use Base utilities
110
+ def self.normalize_params_to_hash(value, keys)
111
+ Base.normalize_params_to_hash(value, keys)
112
+ end
113
+
114
+ def self.normalize_param_cache_key(value, prefix)
115
+ Base.normalize_param_cache_key(value, prefix)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles rate calculation operators: rate_per_second, rate_per_minute, rate_per_hour
7
+ module RateOperators
8
+ def self.handle(op, actual_value, expected_value)
9
+ case op
10
+ when "rate_per_second"
11
+ # Calculates rate per second from array of timestamps
12
+ return false unless actual_value.is_a?(Array)
13
+ return false if actual_value.empty?
14
+
15
+ timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
16
+ return false if timestamps.size < 2
17
+
18
+ sorted_timestamps = timestamps.sort
19
+ time_span = sorted_timestamps.last - sorted_timestamps.first
20
+ return false if time_span <= 0
21
+
22
+ rate = timestamps.size.to_f / time_span
23
+ compare_rate_result(rate, expected_value)
24
+
25
+ when "rate_per_minute"
26
+ # Calculates rate per minute from array of timestamps
27
+ return false unless actual_value.is_a?(Array)
28
+ return false if actual_value.empty?
29
+
30
+ timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
31
+ return false if timestamps.size < 2
32
+
33
+ sorted_timestamps = timestamps.sort
34
+ time_span = sorted_timestamps.last - sorted_timestamps.first
35
+ return false if time_span <= 0
36
+
37
+ rate = (timestamps.size.to_f / time_span) * 60.0
38
+ compare_rate_result(rate, expected_value)
39
+
40
+ when "rate_per_hour"
41
+ # Calculates rate per hour from array of timestamps
42
+ return false unless actual_value.is_a?(Array)
43
+ return false if actual_value.empty?
44
+
45
+ timestamps = actual_value.map { |ts| ConditionEvaluator.parse_date(ts) }.compact
46
+ return false if timestamps.size < 2
47
+
48
+ sorted_timestamps = timestamps.sort
49
+ time_span = sorted_timestamps.last - sorted_timestamps.first
50
+ return false if time_span <= 0
51
+
52
+ rate = (timestamps.size.to_f / time_span) * 3600.0
53
+ compare_rate_result(rate, expected_value)
54
+ end
55
+ # Returns nil if not handled by this module
56
+ end
57
+
58
+ # Compare rate result
59
+ def self.compare_rate_result(actual, expected)
60
+ ConditionEvaluator.compare_rate_result(actual, expected)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end