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,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles date arithmetic operators: add_days, subtract_days, add_hours, subtract_hours, add_minutes, subtract_minutes
7
+ module DateArithmeticOperators
8
+ def self.handle(op, actual_value, expected_value, context_hash, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ when "add_days"
11
+ # Adds days to a date and compares
12
+ return false unless actual_value
13
+
14
+ start_date = ConditionEvaluator.parse_date(actual_value)
15
+ return false unless start_date
16
+
17
+ params = parse_date_arithmetic_params(expected_value, :days, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
18
+ return false unless params
19
+
20
+ result_date = start_date + (params[:days] * 86_400)
21
+ target_date = if params[:target] == "now"
22
+ Time.now
23
+ else
24
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
25
+ end
26
+ return false unless target_date
27
+
28
+ compare_date_result?(result_date, target_date, params)
29
+
30
+ when "subtract_days"
31
+ # Subtracts days from a date and compares
32
+ return false unless actual_value
33
+
34
+ start_date = ConditionEvaluator.parse_date(actual_value)
35
+ return false unless start_date
36
+
37
+ params = parse_date_arithmetic_params(expected_value, :days, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
38
+ return false unless params
39
+
40
+ result_date = start_date - (params[:days] * 86_400)
41
+ target_date = if params[:target] == "now"
42
+ Time.now
43
+ else
44
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
45
+ end
46
+ return false unless target_date
47
+
48
+ compare_date_result?(result_date, target_date, params)
49
+
50
+ when "add_hours"
51
+ # Adds hours to a date and compares
52
+ return false unless actual_value
53
+
54
+ start_date = ConditionEvaluator.parse_date(actual_value)
55
+ return false unless start_date
56
+
57
+ params = parse_date_arithmetic_params(expected_value, :hours, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
58
+ return false unless params
59
+
60
+ result_date = start_date + (params[:hours] * 3600)
61
+ target_date = if params[:target] == "now"
62
+ Time.now
63
+ else
64
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
65
+ end
66
+ return false unless target_date
67
+
68
+ compare_date_result?(result_date, target_date, params)
69
+
70
+ when "subtract_hours"
71
+ # Subtracts hours from a date and compares
72
+ return false unless actual_value
73
+
74
+ start_date = ConditionEvaluator.parse_date(actual_value)
75
+ return false unless start_date
76
+
77
+ params = parse_date_arithmetic_params(expected_value, :hours, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
78
+ return false unless params
79
+
80
+ result_date = start_date - (params[:hours] * 3600)
81
+ target_date = if params[:target] == "now"
82
+ Time.now
83
+ else
84
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
85
+ end
86
+ return false unless target_date
87
+
88
+ compare_date_result?(result_date, target_date, params)
89
+
90
+ when "add_minutes"
91
+ # Adds minutes to a date and compares
92
+ return false unless actual_value
93
+
94
+ start_date = ConditionEvaluator.parse_date(actual_value)
95
+ return false unless start_date
96
+
97
+ params = parse_date_arithmetic_params(expected_value, :minutes, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
98
+ return false unless params
99
+
100
+ result_date = start_date + (params[:minutes] * 60)
101
+ target_date = if params[:target] == "now"
102
+ Time.now
103
+ else
104
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
105
+ end
106
+ return false unless target_date
107
+
108
+ compare_date_result?(result_date, target_date, params)
109
+
110
+ when "subtract_minutes"
111
+ # Subtracts minutes from a date and compares
112
+ return false unless actual_value
113
+
114
+ start_date = ConditionEvaluator.parse_date(actual_value)
115
+ return false unless start_date
116
+
117
+ params = parse_date_arithmetic_params(expected_value, :minutes, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
118
+ return false unless params
119
+
120
+ result_date = start_date - (params[:minutes] * 60)
121
+ target_date = if params[:target] == "now"
122
+ Time.now
123
+ else
124
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
125
+ end
126
+ return false unless target_date
127
+
128
+ compare_date_result?(result_date, target_date, params)
129
+ end
130
+ # Returns nil if not handled by this module
131
+ end
132
+
133
+ # Parse date arithmetic parameters
134
+ def self.parse_date_arithmetic_params(value, unit = :days, param_cache: nil, param_cache_mutex: nil)
135
+ return nil unless value.is_a?(Hash)
136
+
137
+ # Normalize to hash (already a hash, but normalize keys)
138
+ normalized = Base.normalize_params_to_hash(value, [])
139
+
140
+ cache = param_cache
141
+ mutex = param_cache_mutex
142
+ if cache.nil? || mutex.nil?
143
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
144
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
145
+ end
146
+
147
+ cache_key = Base.normalize_param_cache_key(normalized, "date_arithmetic_#{unit}")
148
+ cached = cache[cache_key]
149
+ return cached if cached
150
+
151
+ mutex.synchronize do
152
+ cache[cache_key] ||= parse_date_arithmetic_params_impl(normalized, unit)
153
+ end
154
+ end
155
+
156
+ def self.parse_date_arithmetic_params_impl(value, unit)
157
+ unit_value = value[unit.to_s] || value[unit]
158
+ return nil unless unit_value.is_a?(Numeric)
159
+
160
+ {
161
+ unit => unit_value.to_f,
162
+ target: value[:target] || value["target"] || "now",
163
+ compare: value[:compare] || value["compare"],
164
+ eq: value[:eq] || value["eq"],
165
+ gt: value[:gt] || value["gt"],
166
+ lt: value[:lt] || value["lt"],
167
+ gte: value[:gte] || value["gte"],
168
+ lte: value[:lte] || value["lte"]
169
+ }
170
+ end
171
+
172
+ # Compare date result
173
+ def self.compare_date_result?(actual, target, params)
174
+ if params[:compare]
175
+ case params[:compare].to_s
176
+ when "eq", "=="
177
+ (actual - target).abs < 1
178
+ when "gt", ">"
179
+ actual > target
180
+ when "lt", "<"
181
+ actual < target
182
+ when "gte", ">="
183
+ actual >= target
184
+ when "lte", "<="
185
+ actual <= target
186
+ else
187
+ false
188
+ end
189
+ elsif params[:eq]
190
+ (actual - target).abs < 1
191
+ elsif params[:gt]
192
+ actual > target
193
+ elsif params[:lt]
194
+ actual < target
195
+ elsif params[:gte]
196
+ actual >= target
197
+ elsif params[:lte]
198
+ actual <= target
199
+ else
200
+ false
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles date/time operators: before_date, after_date, within_days, day_of_week
7
+ module DateTimeOperators
8
+ def self.handle(op, actual_value, expected_value, date_cache: nil, date_cache_mutex: nil) # rubocop:disable Lint/UnusedMethodArgument
9
+ case op
10
+ when "before_date"
11
+ # Checks if date is before specified date
12
+ ConditionEvaluator.compare_dates(actual_value, expected_value, :<)
13
+
14
+ when "after_date"
15
+ # Checks if date is after specified date
16
+ ConditionEvaluator.compare_dates(actual_value, expected_value, :>)
17
+
18
+ when "within_days"
19
+ # Checks if date is within N days from now (past or future)
20
+ return false unless actual_value
21
+ return false unless expected_value.is_a?(Numeric)
22
+
23
+ date = ConditionEvaluator.parse_date(actual_value)
24
+ return false unless date
25
+
26
+ now = Time.now
27
+ diff_days = ((date - now) / 86_400).abs # 86400 seconds in a day
28
+ diff_days <= expected_value
29
+
30
+ when "day_of_week"
31
+ # Checks if date falls on specified day of week
32
+ return false unless actual_value
33
+
34
+ date = ConditionEvaluator.parse_date(actual_value)
35
+ return false unless date
36
+
37
+ expected_day = ConditionEvaluator.normalize_day_of_week(expected_value)
38
+ return false unless expected_day
39
+
40
+ date.wday == expected_day
41
+ end
42
+ # Returns nil if not handled by this module
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles duration calculation operators: duration_seconds, duration_minutes, duration_hours, duration_days
7
+ module DurationOperators
8
+ def self.handle(op, actual_value, expected_value, context_hash, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ when "duration_seconds"
11
+ # Calculates duration between two dates in seconds
12
+ return false unless actual_value
13
+
14
+ start_date = ConditionEvaluator.parse_date(actual_value)
15
+ return false unless start_date
16
+
17
+ params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
18
+ return false unless params
19
+
20
+ end_date = if params[:end] == "now"
21
+ Time.now
22
+ else
23
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
24
+ params[:end]))
25
+ end
26
+ return false unless end_date
27
+
28
+ duration = (end_date - start_date).abs
29
+ compare_duration_result(duration, params)
30
+
31
+ when "duration_minutes"
32
+ # Calculates duration between two dates in minutes
33
+ return false unless actual_value
34
+
35
+ start_date = ConditionEvaluator.parse_date(actual_value)
36
+ return false unless start_date
37
+
38
+ params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
39
+ return false unless params
40
+
41
+ end_date = if params[:end] == "now"
42
+ Time.now
43
+ else
44
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
45
+ params[:end]))
46
+ end
47
+ return false unless end_date
48
+
49
+ duration = ((end_date - start_date).abs / 60.0)
50
+ compare_duration_result(duration, params)
51
+
52
+ when "duration_hours"
53
+ # Calculates duration between two dates in hours
54
+ return false unless actual_value
55
+
56
+ start_date = ConditionEvaluator.parse_date(actual_value)
57
+ return false unless start_date
58
+
59
+ params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
60
+ return false unless params
61
+
62
+ end_date = if params[:end] == "now"
63
+ Time.now
64
+ else
65
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
66
+ params[:end]))
67
+ end
68
+ return false unless end_date
69
+
70
+ duration = ((end_date - start_date).abs / 3600.0)
71
+ compare_duration_result(duration, params)
72
+
73
+ when "duration_days"
74
+ # Calculates duration between two dates in days
75
+ return false unless actual_value
76
+
77
+ start_date = ConditionEvaluator.parse_date(actual_value)
78
+ return false unless start_date
79
+
80
+ params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
81
+ return false unless params
82
+
83
+ end_date = if params[:end] == "now"
84
+ Time.now
85
+ else
86
+ ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
87
+ params[:end]))
88
+ end
89
+ return false unless end_date
90
+
91
+ duration = ((end_date - start_date).abs / 86_400.0)
92
+ compare_duration_result(duration, params)
93
+ end
94
+ # Returns nil if not handled by this module
95
+ end
96
+
97
+ # Parse duration parameters
98
+ def self.parse_duration_params(value, param_cache: nil, param_cache_mutex: nil)
99
+ return nil unless value.is_a?(Hash)
100
+
101
+ # Normalize to hash (already a hash, but normalize keys)
102
+ normalized = Base.normalize_params_to_hash(value, [])
103
+
104
+ cache = param_cache
105
+ mutex = param_cache_mutex
106
+ if cache.nil? || mutex.nil?
107
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
108
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
109
+ end
110
+
111
+ cache_key = Base.normalize_param_cache_key(normalized, "duration")
112
+ cached = cache[cache_key]
113
+ return cached if cached
114
+
115
+ mutex.synchronize do
116
+ cache[cache_key] ||= parse_duration_params_impl(normalized)
117
+ end
118
+ end
119
+
120
+ def self.parse_duration_params_impl(value)
121
+ end_field = value[:end] || value["end"]
122
+ return nil unless end_field
123
+
124
+ {
125
+ end: end_field.to_s,
126
+ min: value[:min] || value["min"],
127
+ max: value[:max] || value["max"],
128
+ gt: value[:gt] || value["gt"],
129
+ lt: value[:lt] || value["lt"],
130
+ gte: value[:gte] || value["gte"],
131
+ lte: value[:lte] || value["lte"]
132
+ }
133
+ end
134
+
135
+ # Compare duration result
136
+ def self.compare_duration_result(actual, params)
137
+ result = true
138
+ result &&= (actual >= params[:min]) if params[:min]
139
+ result &&= (actual <= params[:max]) if params[:max]
140
+ result &&= (actual > params[:gt]) if params[:gt]
141
+ result &&= (actual < params[:lt]) if params[:lt]
142
+ result &&= (actual >= params[:gte]) if params[:gte]
143
+ result &&= (actual <= params[:lte]) if params[:lte]
144
+ result
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Dsl
5
+ module Operators
6
+ # Handles financial calculation operators: compound_interest, present_value, future_value, payment
7
+ module FinancialOperators
8
+ def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
9
+ case op
10
+ when "compound_interest"
11
+ # Calculates compound interest: A = P(1 + r/n)^(nt)
12
+ return false unless actual_value.is_a?(Numeric)
13
+
14
+ params = parse_compound_interest_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
15
+ return false unless params
16
+
17
+ principal = actual_value
18
+ rate = params[:rate]
19
+ periods = params[:periods]
20
+ result = principal * ((1 + (rate / periods))**periods)
21
+
22
+ if params[:result]
23
+ (result.round(2) == params[:result].round(2))
24
+ else
25
+ compare_financial_result(result, params)
26
+ end
27
+
28
+ when "present_value"
29
+ # Calculates present value: PV = FV / (1 + r)^n
30
+ return false unless actual_value.is_a?(Numeric)
31
+
32
+ params = parse_present_value_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
33
+ return false unless params
34
+
35
+ future_value = actual_value
36
+ rate = params[:rate]
37
+ periods = params[:periods]
38
+ present_value = future_value / ((1 + rate)**periods)
39
+
40
+ if params[:result]
41
+ (present_value.round(2) == params[:result].round(2))
42
+ else
43
+ compare_financial_result(present_value, params)
44
+ end
45
+
46
+ when "future_value"
47
+ # Calculates future value: FV = PV * (1 + r)^n
48
+ return false unless actual_value.is_a?(Numeric)
49
+
50
+ params = parse_future_value_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
51
+ return false unless params
52
+
53
+ present_value = actual_value
54
+ rate = params[:rate]
55
+ periods = params[:periods]
56
+ future_value = present_value * ((1 + rate)**periods)
57
+
58
+ if params[:result]
59
+ (future_value.round(2) == params[:result].round(2))
60
+ else
61
+ compare_financial_result(future_value, params)
62
+ end
63
+
64
+ when "payment"
65
+ # Calculates loan payment: PMT = P * [r(1+r)^n] / [(1+r)^n - 1]
66
+ return false unless actual_value.is_a?(Numeric)
67
+
68
+ params = parse_payment_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
69
+ return false unless params
70
+
71
+ principal = actual_value
72
+ rate = params[:rate]
73
+ periods = params[:periods]
74
+
75
+ return false if rate <= 0 || periods <= 0
76
+
77
+ payment = if rate.zero?
78
+ principal / periods
79
+ else
80
+ principal * (rate * ((1 + rate)**periods)) / (((1 + rate)**periods) - 1)
81
+ end
82
+
83
+ if params[:result]
84
+ (payment.round(2) == params[:result].round(2))
85
+ else
86
+ compare_financial_result(payment, params)
87
+ end
88
+ end
89
+ # Returns nil if not handled by this module
90
+ end
91
+
92
+ # Parse compound interest parameters
93
+ def self.parse_compound_interest_params(value, param_cache: nil, param_cache_mutex: nil)
94
+ return nil unless value.is_a?(Hash)
95
+
96
+ normalized = Base.normalize_params_to_hash(value, [])
97
+
98
+ cache = param_cache
99
+ mutex = param_cache_mutex
100
+ if cache.nil? || mutex.nil?
101
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
102
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
103
+ end
104
+
105
+ cache_key = Base.normalize_param_cache_key(normalized, "compound_interest")
106
+ cached = cache[cache_key]
107
+ return cached if cached
108
+
109
+ mutex.synchronize do
110
+ cache[cache_key] ||= parse_compound_interest_params_impl(normalized)
111
+ end
112
+ end
113
+
114
+ def self.parse_compound_interest_params_impl(value)
115
+ rate = value[:rate] || value["rate"]
116
+ periods = value[:periods] || value["periods"]
117
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
118
+
119
+ {
120
+ rate: rate.to_f,
121
+ periods: periods.to_f,
122
+ result: value[:result] || value["result"],
123
+ compare: value[:compare] || value["compare"],
124
+ threshold: value[:threshold] || value["threshold"]
125
+ }
126
+ end
127
+
128
+ # Parse present value parameters
129
+ def self.parse_present_value_params(value, param_cache: nil, param_cache_mutex: nil)
130
+ return nil unless value.is_a?(Hash)
131
+
132
+ normalized = Base.normalize_params_to_hash(value, [])
133
+
134
+ cache = param_cache
135
+ mutex = param_cache_mutex
136
+ if cache.nil? || mutex.nil?
137
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
138
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
139
+ end
140
+
141
+ cache_key = Base.normalize_param_cache_key(normalized, "present_value")
142
+ cached = cache[cache_key]
143
+ return cached if cached
144
+
145
+ mutex.synchronize do
146
+ cache[cache_key] ||= parse_present_value_params_impl(normalized)
147
+ end
148
+ end
149
+
150
+ def self.parse_present_value_params_impl(value)
151
+ rate = value[:rate] || value["rate"]
152
+ periods = value[:periods] || value["periods"]
153
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
154
+
155
+ {
156
+ rate: rate.to_f,
157
+ periods: periods.to_f,
158
+ result: value[:result] || value["result"]
159
+ }
160
+ end
161
+
162
+ # Parse future value parameters
163
+ def self.parse_future_value_params(value, param_cache: nil, param_cache_mutex: nil)
164
+ return nil unless value.is_a?(Hash)
165
+
166
+ normalized = Base.normalize_params_to_hash(value, [])
167
+
168
+ cache = param_cache
169
+ mutex = param_cache_mutex
170
+ if cache.nil? || mutex.nil?
171
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
172
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
173
+ end
174
+
175
+ cache_key = Base.normalize_param_cache_key(normalized, "future_value")
176
+ cached = cache[cache_key]
177
+ return cached if cached
178
+
179
+ mutex.synchronize do
180
+ cache[cache_key] ||= parse_future_value_params_impl(normalized)
181
+ end
182
+ end
183
+
184
+ def self.parse_future_value_params_impl(value)
185
+ rate = value[:rate] || value["rate"]
186
+ periods = value[:periods] || value["periods"]
187
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
188
+
189
+ {
190
+ rate: rate.to_f,
191
+ periods: periods.to_f,
192
+ result: value[:result] || value["result"]
193
+ }
194
+ end
195
+
196
+ # Parse payment parameters
197
+ def self.parse_payment_params(value, param_cache: nil, param_cache_mutex: nil)
198
+ return nil unless value.is_a?(Hash)
199
+
200
+ normalized = Base.normalize_params_to_hash(value, [])
201
+
202
+ cache = param_cache
203
+ mutex = param_cache_mutex
204
+ if cache.nil? || mutex.nil?
205
+ cache = ConditionEvaluator.instance_variable_get(:@param_cache)
206
+ mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
207
+ end
208
+
209
+ cache_key = Base.normalize_param_cache_key(normalized, "payment")
210
+ cached = cache[cache_key]
211
+ return cached if cached
212
+
213
+ mutex.synchronize do
214
+ cache[cache_key] ||= parse_payment_params_impl(normalized)
215
+ end
216
+ end
217
+
218
+ def self.parse_payment_params_impl(value)
219
+ rate = value[:rate] || value["rate"]
220
+ periods = value[:periods] || value["periods"]
221
+ return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
222
+
223
+ {
224
+ rate: rate.to_f,
225
+ periods: periods.to_f,
226
+ result: value[:result] || value["result"]
227
+ }
228
+ end
229
+
230
+ # Compare financial result
231
+ def self.compare_financial_result(actual, params)
232
+ ConditionEvaluator.compare_financial_result(actual, params)
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end