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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../errors"
2
4
  require_relative "types"
3
5
 
@@ -60,7 +62,6 @@ module DecisionAgent
60
62
  private
61
63
 
62
64
  # Tokenize the expression
63
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
64
65
  def tokenize(expr)
65
66
  tokens = []
66
67
  i = 0
@@ -68,95 +69,115 @@ module DecisionAgent
68
69
  while i < expr.length
69
70
  char = expr[i]
70
71
 
71
- # Skip whitespace
72
72
  if char.match?(/\s/)
73
73
  i += 1
74
74
  next
75
75
  end
76
76
 
77
- # Check for multi-character operators
78
- if i + 1 < expr.length
79
- two_char = expr[i, 2]
80
- if %w[>= <= != ** or].include?(two_char)
81
- tokens << { type: :operator, value: two_char }
82
- i += 2
83
- next
84
- elsif two_char == "an" && i + 2 < expr.length && expr[i, 3] == "and"
85
- tokens << { type: :operator, value: "and" }
86
- i += 3
87
- next
88
- end
89
- end
77
+ token, consumed = tokenize_char(expr, i, char, tokens)
78
+ raise DecisionAgent::Dmn::FeelParseError, "Unexpected character: #{char} at position #{i}" unless token
90
79
 
91
- # Numbers (integer or float) - check BEFORE single char operators to handle negative numbers
92
- if char.match?(/\d/) ||
93
- (char == "-" && i + 1 < expr.length && expr[i + 1].match?(/\d/) &&
94
- (tokens.empty? || tokens.last[:type] == :operator || tokens.last[:type] == :paren))
95
- num_str = ""
96
- num_str << char if char == "-"
97
- i += 1 if char == "-"
98
-
99
- while i < expr.length && expr[i].match?(/[\d.]/)
100
- num_str << expr[i]
101
- i += 1
102
- end
103
-
104
- value = num_str.include?(".") ? num_str.to_f : num_str.to_i
105
- tokens << { type: :number, value: value }
106
- next
107
- end
80
+ tokens << token
81
+ i += consumed
82
+ end
108
83
 
109
- # Single character operators
110
- if "+-*/%><()=".include?(char)
111
- type = %w[( )].include?(char) ? :paren : :operator
112
- tokens << { type: type, value: char }
113
- i += 1
114
- next
115
- end
84
+ tokens
85
+ end
116
86
 
117
- # Quoted strings
118
- if char == '"'
119
- str = ""
120
- i += 1
121
- while i < expr.length && expr[i] != '"'
122
- str << expr[i]
123
- i += 1
124
- end
125
- i += 1 # Skip closing quote
126
- tokens << { type: :string, value: str }
127
- next
128
- end
87
+ # Dispatch tokenization for a single character position
88
+ # Returns [token, chars_consumed] or [nil, 0] if unrecognized
89
+ def tokenize_char(expr, pos, char, tokens)
90
+ tokenize_multi_char_op(expr, pos) ||
91
+ tokenize_number(expr, pos, char, tokens) ||
92
+ tokenize_single_char_op(char) ||
93
+ tokenize_string(expr, pos, char) ||
94
+ tokenize_keyword(expr, pos, char) ||
95
+ [nil, 0]
96
+ end
129
97
 
130
- # Booleans and keywords
131
- if char.match?(/[a-zA-Z]/)
132
- word = ""
133
- while i < expr.length && expr[i].match?(/[a-zA-Z_]/)
134
- word << expr[i]
135
- i += 1
136
- end
137
-
138
- tokens << case word.downcase
139
- when "true"
140
- { type: :boolean, value: true }
141
- when "false"
142
- { type: :boolean, value: false }
143
- when "not"
144
- { type: :operator, value: "not" }
145
- when "and", "or"
146
- { type: :operator, value: word.downcase }
147
- else
148
- # Field reference
149
- { type: :field, value: word }
150
- end
151
- next
152
- end
98
+ # Try to match multi-character operators (>=, <=, !=, **, and, or)
99
+ def tokenize_multi_char_op(expr, pos)
100
+ return nil unless pos + 1 < expr.length
153
101
 
154
- raise DecisionAgent::Dmn::FeelParseError, "Unexpected character: #{char} at position #{i}"
102
+ two_char = expr[pos, 2]
103
+ return [{ type: :operator, value: two_char }, 2] if %w[>= <= != ** or].include?(two_char)
104
+ return nil unless two_char == "an" && pos + 2 < expr.length && expr[pos, 3] == "and"
105
+
106
+ [{ type: :operator, value: "and" }, 3]
107
+ end
108
+
109
+ # Try to tokenize a number (integer or float, including negative)
110
+ def tokenize_number(expr, pos, char, tokens)
111
+ return nil unless number_start?(char, expr, pos, tokens)
112
+
113
+ num_str = String.new
114
+ if char == "-"
115
+ num_str << "-"
116
+ pos += 1
155
117
  end
156
118
 
157
- tokens
119
+ while pos < expr.length && expr[pos].match?(/[\d.]/)
120
+ num_str << expr[pos]
121
+ pos += 1
122
+ end
123
+
124
+ value = num_str.include?(".") ? num_str.to_f : num_str.to_i
125
+ [{ type: :number, value: value }, (char == "-" ? 1 : 0) + num_str.delete("-").length]
126
+ end
127
+
128
+ def number_start?(char, expr, pos, tokens)
129
+ return true if char.match?(/\d/)
130
+
131
+ char == "-" && pos + 1 < expr.length && expr[pos + 1].match?(/\d/) &&
132
+ (tokens.empty? || tokens.last[:type] == :operator || tokens.last[:type] == :paren)
133
+ end
134
+
135
+ # Try to tokenize a single-character operator or parenthesis
136
+ def tokenize_single_char_op(char)
137
+ return nil unless "+-*/%><()=".include?(char)
138
+
139
+ type = %w[( )].include?(char) ? :paren : :operator
140
+ [{ type: type, value: char }, 1]
141
+ end
142
+
143
+ # Try to tokenize a quoted string
144
+ def tokenize_string(expr, pos, char)
145
+ return nil unless char == '"'
146
+
147
+ str = String.new
148
+ idx = pos + 1
149
+ while idx < expr.length && expr[idx] != '"'
150
+ str << expr[idx]
151
+ idx += 1
152
+ end
153
+ idx += 1 # Skip closing quote
154
+
155
+ [{ type: :string, value: str }, idx - pos]
156
+ end
157
+
158
+ # Try to tokenize a keyword (boolean, operator, or field reference)
159
+ def tokenize_keyword(expr, pos, char)
160
+ return nil unless char.match?(/[a-zA-Z]/)
161
+
162
+ word = String.new
163
+ idx = pos
164
+ while idx < expr.length && expr[idx].match?(/[a-zA-Z_]/)
165
+ word << expr[idx]
166
+ idx += 1
167
+ end
168
+
169
+ [keyword_token(word), idx - pos]
170
+ end
171
+
172
+ def keyword_token(word)
173
+ case word.downcase
174
+ when "true" then { type: :boolean, value: true }
175
+ when "false" then { type: :boolean, value: false }
176
+ when "not" then { type: :operator, value: "not" }
177
+ when "and", "or" then { type: :operator, value: word.downcase }
178
+ else { type: :field, value: word }
179
+ end
158
180
  end
159
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
160
181
 
161
182
  # Parse expression with operator precedence
162
183
  def parse_expression(min_precedence = 0)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "parslet"
2
4
  require_relative "../errors"
3
5
 
@@ -6,6 +8,54 @@ module DecisionAgent
6
8
  module Feel
7
9
  # Transforms Parslet parse tree into AST
8
10
  class Transformer < Parslet::Transform
11
+ # Extract a context entry key from various node representations
12
+ def self.extract_entry_key(key_node)
13
+ return key_node.to_s if key_node.is_a?(Parslet::Slice)
14
+ return key_node.to_s unless key_node.is_a?(Hash)
15
+
16
+ case key_node[:type]
17
+ when :field then key_node[:name].to_s
18
+ when :string then key_node[:value].to_s
19
+ when :identifier then key_node[:name].to_s
20
+ else
21
+ key_node[:identifier]&.to_s || key_node[:string]&.to_s || key_node.to_s
22
+ end
23
+ end
24
+
25
+ # Extract a name string from a node that may be a Hash or raw value
26
+ def self.extract_name(name_node)
27
+ return name_node.to_s.strip unless name_node.is_a?(Hash)
28
+ return name_node[:name].to_s.strip if name_node[:type] == :field
29
+
30
+ name_node[:identifier]&.to_s&.strip || name_node.to_s
31
+ end
32
+
33
+ # Apply a single postfix operation to the current AST node
34
+ def self.apply_postfix_op(current, op)
35
+ return current unless op.is_a?(Hash)
36
+
37
+ if op[:property_access]
38
+ { type: :property_access, object: current, property: op[:property_access][:property][:identifier].to_s }
39
+ elsif op[:function_call]
40
+ { type: :function_call, name: current, arguments: op[:function_call][:arguments] || [] }
41
+ elsif op[:filter]
42
+ { type: :filter, list: current, condition: op[:filter][:filter] }
43
+ else
44
+ current
45
+ end
46
+ end
47
+
48
+ # Extract variable name from a potentially transformed node
49
+ def self.extract_variable_name(var_node)
50
+ if var_node.is_a?(Hash) && var_node[:type] == :field
51
+ var_node[:name]
52
+ elsif var_node.is_a?(Hash) && var_node[:identifier]
53
+ var_node[:identifier].to_s
54
+ else
55
+ var_node.to_s
56
+ end
57
+ end
58
+
9
59
  # Literals
10
60
  rule(null: simple(:_)) { { type: :null, value: nil } }
11
61
 
@@ -54,26 +104,7 @@ module DecisionAgent
54
104
  end
55
105
 
56
106
  pairs = entries_array.map do |entry|
57
- # Extract key - could be a transformed field node, string node, or raw value
58
- key = if entry[:key].is_a?(Hash)
59
- # Key is a structured node
60
- case entry[:key][:type]
61
- when :field
62
- entry[:key][:name].to_s
63
- when :string
64
- entry[:key][:value].to_s
65
- when :identifier
66
- entry[:key][:name].to_s
67
- else
68
- entry[:key][:identifier]&.to_s || entry[:key][:string]&.to_s || entry[:key].to_s
69
- end
70
- elsif entry[:key].is_a?(Parslet::Slice)
71
- entry[:key].to_s
72
- else
73
- entry[:key].to_s
74
- end
75
-
76
- [key, entry[:value]]
107
+ [Transformer.extract_entry_key(entry[:key]), entry[:value]]
77
108
  end
78
109
 
79
110
  { type: :context_literal, pairs: pairs }
@@ -110,36 +141,16 @@ module DecisionAgent
110
141
  else [args]
111
142
  end
112
143
 
113
- func_name = case name
114
- when Hash
115
- # Handle transformed field nodes or raw identifier hashes
116
- if name[:type] == :field
117
- name[:name].to_s.strip
118
- else
119
- name[:identifier]&.to_s&.strip || name.to_s
120
- end
121
- else
122
- name.to_s.strip
123
- end
124
-
125
144
  {
126
145
  type: :function_call,
127
- name: func_name,
146
+ name: Transformer.extract_name(name),
128
147
  arguments: args_array
129
148
  }
130
149
  end
131
150
 
132
151
  # Identifier or function call (just identifier, no arguments)
133
152
  rule(identifier_or_call: { name: subtree(:name) }) do
134
- # Just an identifier
135
- field_name = case name
136
- when Hash
137
- name[:identifier]&.to_s&.strip || name[:type] == :field ? name[:name] : name.to_s
138
- else
139
- name.to_s.strip
140
- end
141
-
142
- { type: :field, name: field_name }
153
+ { type: :field, name: Transformer.extract_name(name) }
143
154
  end
144
155
 
145
156
  # Comparison operations
@@ -260,35 +271,7 @@ module DecisionAgent
260
271
 
261
272
  # Postfix operations (property access, function calls, filters)
262
273
  rule(postfix: { base: subtree(:base), postfix_ops: subtree(:ops) }) do
263
- ops_array = Array(ops)
264
- ops_array.reduce(base) do |current, op|
265
- case op
266
- when Hash
267
- if op[:property_access]
268
- {
269
- type: :property_access,
270
- object: current,
271
- property: op[:property_access][:property][:identifier].to_s
272
- }
273
- elsif op[:function_call]
274
- {
275
- type: :function_call,
276
- name: current,
277
- arguments: op[:function_call][:arguments] || []
278
- }
279
- elsif op[:filter]
280
- {
281
- type: :filter,
282
- list: current,
283
- condition: op[:filter][:filter]
284
- }
285
- else
286
- current
287
- end
288
- else
289
- current
290
- end
291
- end
274
+ Array(ops).reduce(base) { |current, op| Transformer.apply_postfix_op(current, op) }
292
275
  end
293
276
 
294
277
  # If-then-else conditional
@@ -303,19 +286,10 @@ module DecisionAgent
303
286
 
304
287
  # Quantified expressions
305
288
  rule(quantifier: simple(:q), var: subtree(:v), list: subtree(:l), condition: subtree(:c)) do
306
- # Variable might be already transformed to a field node or still be an identifier hash
307
- var_name = if v.is_a?(Hash) && v[:type] == :field
308
- v[:name]
309
- elsif v.is_a?(Hash) && v[:identifier]
310
- v[:identifier].to_s
311
- else
312
- v.to_s
313
- end
314
-
315
289
  {
316
290
  type: :quantified,
317
291
  quantifier: q.to_s,
318
- variable: var_name,
292
+ variable: Transformer.extract_variable_name(v),
319
293
  list: l,
320
294
  condition: c
321
295
  }
@@ -323,18 +297,9 @@ module DecisionAgent
323
297
 
324
298
  # For expression
325
299
  rule(var: subtree(:v), list: subtree(:l), return_expr: subtree(:r)) do
326
- # Variable might be already transformed to a field node or still be an identifier hash
327
- var_name = if v.is_a?(Hash) && v[:type] == :field
328
- v[:name]
329
- elsif v.is_a?(Hash) && v[:identifier]
330
- v[:identifier].to_s
331
- else
332
- v.to_s
333
- end
334
-
335
300
  {
336
301
  type: :for,
337
- variable: var_name,
302
+ variable: Transformer.extract_variable_name(v),
338
303
  list: l,
339
304
  return_expr: r
340
305
  }
@@ -355,17 +320,6 @@ module DecisionAgent
355
320
  body: body
356
321
  }
357
322
  end
358
-
359
- # Helper to convert parse tree to AST
360
- def self.to_ast(parse_tree)
361
- new.apply(parse_tree)
362
- rescue StandardError => e
363
- raise FeelTransformError.new(
364
- "Failed to transform parse tree to AST: #{e.message}",
365
- parse_tree: parse_tree,
366
- error: e
367
- )
368
- end
369
323
  end
370
324
  end
371
325
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "time"
2
4
  require "date"
3
5
  require "bigdecimal"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "parser"
2
4
  require_relative "validator"
3
5
  require_relative "adapter"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "errors"
2
4
 
3
5
  module DecisionAgent
@@ -53,10 +55,6 @@ module DecisionAgent
53
55
  @decision_table = table
54
56
  end
55
57
 
56
- def add_information_requirement(requirement)
57
- @information_requirements << requirement
58
- end
59
-
60
58
  def freeze
61
59
  @id.freeze
62
60
  @name.freeze
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "nokogiri"
2
4
  require "securerandom"
3
5
  require_relative "model"
@@ -227,8 +227,9 @@ module DecisionAgent
227
227
  # This would need to be enhanced to track which rule matched
228
228
  evaluator.evaluate(context: context)
229
229
  # In a full implementation, we'd track the matched rule
230
- rescue StandardError
231
- # Ignore errors for coverage calculation
230
+ rescue StandardError => e
231
+ # Log errors during coverage calculation but continue
232
+ warn "[DecisionAgent] Coverage evaluation failed for test case: #{e.message}"
232
233
  end
233
234
  end
234
235
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "errors"
2
4
  require_relative "feel/evaluator"
3
5
 
@@ -210,9 +212,9 @@ module DecisionAgent
210
212
  when "PRIORITY"
211
213
  # Check that outputs have defined allowed values with priorities
212
214
  table.outputs.each do |output|
213
- unless output.instance_variable_get(:@allowed_values)
214
- @warnings << "#{path}: PRIORITY hit policy requires outputs to have defined allowed values"
215
- end
215
+ next if output.instance_variable_get(:@allowed_values)
216
+
217
+ @warnings << "#{path}: PRIORITY hit policy requires outputs to have defined allowed values"
216
218
  end
217
219
  end
218
220
  end
@@ -65,8 +65,8 @@ module DecisionAgent
65
65
  svg = [
66
66
  %(<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}" viewBox="0 0 #{width} #{height}">),
67
67
  "<defs>",
68
- ' <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">',
69
- ' <polygon points="0 0, 10 3, 0 6" fill="#666" />',
68
+ %( <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">),
69
+ %( <polygon points="0 0, 10 3, 0 6" fill="#666" />),
70
70
  " </marker>",
71
71
  "</defs>",
72
72
  "<g>"
@@ -264,8 +264,8 @@ module DecisionAgent
264
264
  svg = [
265
265
  %(<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}" viewBox="0 0 #{width} #{height}">),
266
266
  "<defs>",
267
- ' <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">',
268
- ' <polygon points="0 0, 10 3, 0 6" fill="#666" />',
267
+ %( <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">),
268
+ %( <polygon points="0 0, 10 3, 0 6" fill="#666" />),
269
269
  " </marker>",
270
270
  "</defs>",
271
271
  "<g>"
@@ -288,8 +288,9 @@ module DecisionAgent
288
288
  # Use topological sort to arrange nodes in layers
289
289
  begin
290
290
  order = @graph.topological_order
291
- rescue StandardError
292
- # If circular, just use the order as-is
291
+ rescue StandardError => e
292
+ # If circular dependency detected, fall back to unordered keys
293
+ warn "[DecisionAgent] Topological sort failed (possible circular dependency): #{e.message}"
293
294
  order = @graph.decisions.keys
294
295
  end
295
296