decision_agent 0.2.0 → 0.3.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
  24. data/lib/decision_agent/dsl/schema_validator.rb +2 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  29. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  30. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  31. data/lib/decision_agent/web/public/index.html +3 -0
  32. data/lib/decision_agent/web/public/styles.css +21 -0
  33. data/lib/decision_agent/web/server.rb +465 -0
  34. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  35. data/spec/auth/rbac_adapter_spec.rb +228 -0
  36. data/spec/dmn/decision_graph_spec.rb +282 -0
  37. data/spec/dmn/decision_tree_spec.rb +203 -0
  38. data/spec/dmn/feel/errors_spec.rb +18 -0
  39. data/spec/dmn/feel/functions_spec.rb +400 -0
  40. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  41. data/spec/dmn/feel/types_spec.rb +176 -0
  42. data/spec/dmn/feel_parser_spec.rb +489 -0
  43. data/spec/dmn/hit_policy_spec.rb +202 -0
  44. data/spec/dmn/integration_spec.rb +226 -0
  45. data/spec/examples.txt +1846 -1570
  46. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  47. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  48. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  49. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  50. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  51. data/spec/performance_optimizations_spec.rb +10 -3
  52. data/spec/thread_safety_spec.rb +10 -2
  53. data/spec/web_ui_rack_spec.rb +294 -0
  54. metadata +65 -1
@@ -0,0 +1,349 @@
1
+ require "parslet"
2
+ require_relative "../errors"
3
+
4
+ module DecisionAgent
5
+ module Dmn
6
+ module Feel
7
+ # Parslet-based FEEL 1.3 expression parser
8
+ # Implements full FEEL grammar including:
9
+ # - Literals (numbers, strings, booleans, null)
10
+ # - Lists and contexts
11
+ # - Arithmetic, comparison, and logical operators
12
+ # - Function calls
13
+ # - Property access and path expressions
14
+ # - Quantified expressions (some/every)
15
+ # - For expressions
16
+ # - If/then/else conditionals
17
+ # - Ranges and intervals
18
+ class Parser < Parslet::Parser
19
+ # Root rule - entry point for parsing
20
+ rule(:expression) { boxed_expression }
21
+
22
+ # Boxed expressions - top-level expression types
23
+ rule(:boxed_expression) do
24
+ if_expression |
25
+ quantified_expression |
26
+ for_expression |
27
+ disjunction
28
+ end
29
+
30
+ # If-then-else conditional
31
+ rule(:if_expression) do
32
+ str("if") >> space >> expression.as(:condition) >>
33
+ space >> str("then") >> space >> expression.as(:then_expr) >>
34
+ space >> str("else") >> space >> expression.as(:else_expr)
35
+ end
36
+
37
+ # Quantified expressions: some/every
38
+ rule(:quantified_expression) do
39
+ quantifier.as(:quantifier) >> space >>
40
+ identifier.as(:var) >> space >>
41
+ str("in") >> space >>
42
+ expression.as(:list) >> space >>
43
+ str("satisfies") >> space >>
44
+ expression.as(:condition)
45
+ end
46
+
47
+ rule(:quantifier) { str("some") | str("every") }
48
+
49
+ # For expressions
50
+ rule(:for_expression) do
51
+ str("for") >> space >>
52
+ identifier.as(:var) >> space >>
53
+ str("in") >> space >>
54
+ expression.as(:list) >> space >>
55
+ str("return") >> space >>
56
+ expression.as(:return_expr)
57
+ end
58
+
59
+ # Logical OR (disjunction)
60
+ rule(:disjunction) do
61
+ (conjunction.as(:left) >>
62
+ (space >> str("or") >> space >>
63
+ conjunction.as(:right)).repeat(1).as(:or_ops)).as(:or) |
64
+ conjunction
65
+ end
66
+
67
+ # Logical AND (conjunction)
68
+ rule(:conjunction) do
69
+ (comparison.as(:left) >>
70
+ (space >> str("and") >> space >>
71
+ comparison.as(:right)).repeat(1).as(:and_ops)).as(:and) |
72
+ comparison
73
+ end
74
+
75
+ # Comparison operators
76
+ rule(:comparison) do
77
+ (arithmetic.as(:left) >> space? >>
78
+ comparison_op.as(:op) >> space? >>
79
+ arithmetic.as(:right)).as(:comparison) |
80
+ between_expression |
81
+ in_expression |
82
+ instance_of_expression |
83
+ arithmetic
84
+ end
85
+
86
+ rule(:comparison_op) do
87
+ str("!=") | str("<=") | str(">=") |
88
+ str("=") | str("<") | str(">")
89
+ end
90
+
91
+ # Between expression
92
+ rule(:between_expression) do
93
+ (arithmetic.as(:value) >> space >>
94
+ str("between") >> space >>
95
+ arithmetic.as(:min) >> space >>
96
+ str("and") >> space >>
97
+ arithmetic.as(:max)).as(:between)
98
+ end
99
+
100
+ # In expression (list membership)
101
+ rule(:in_expression) do
102
+ (arithmetic.as(:value) >> space >>
103
+ str("in") >> space >>
104
+ (positive_unary_test | simple_positive_unary_tests).as(:list)).as(:in)
105
+ end
106
+
107
+ # Instance of type checking
108
+ rule(:instance_of_expression) do
109
+ (arithmetic.as(:value) >> space >>
110
+ str("instance") >> space >> str("of") >> space >>
111
+ type_name.as(:type)).as(:instance_of)
112
+ end
113
+
114
+ rule(:type_name) do
115
+ str("number") | str("string") | str("boolean") |
116
+ str("date") | str("time") | str("duration") |
117
+ str("list") | str("context")
118
+ end
119
+
120
+ # Arithmetic expressions (addition, subtraction)
121
+ rule(:arithmetic) do
122
+ (term.as(:left) >> space? >>
123
+ arithmetic_op.as(:op) >> space? >>
124
+ arithmetic.as(:right)).as(:arithmetic) |
125
+ term
126
+ end
127
+
128
+ rule(:arithmetic_op) { str("+") | str("-") }
129
+
130
+ # Term (multiplication, division, modulo)
131
+ rule(:term) do
132
+ (exponentiation.as(:left) >> space? >>
133
+ term_op.as(:op) >> space? >>
134
+ term.as(:right)).as(:term) |
135
+ exponentiation
136
+ end
137
+
138
+ rule(:term_op) { str("*") | str("/") | str("%") }
139
+
140
+ # Exponentiation
141
+ rule(:exponentiation) do
142
+ (unary.as(:left) >> space? >>
143
+ str("**").as(:op) >> space? >>
144
+ exponentiation.as(:right)).as(:exponentiation) |
145
+ unary
146
+ end
147
+
148
+ # Unary expressions (not, negation)
149
+ rule(:unary) do
150
+ (str("not").as(:op) >> space >> unary.as(:operand)).as(:unary) |
151
+ (str("-").as(:op) >> unary.as(:operand)).as(:unary) |
152
+ postfix
153
+ end
154
+
155
+ # Postfix expressions (property access, function calls, filtering)
156
+ rule(:postfix) do
157
+ (primary.as(:base) >>
158
+ (property_access | function_call | filter_expression).repeat(1).as(:postfix_ops)).as(:postfix) |
159
+ primary
160
+ end
161
+
162
+ # Property access: .property
163
+ rule(:property_access) do
164
+ (str(".") >> identifier.as(:property)).as(:property_access)
165
+ end
166
+
167
+ # Function call: (args...)
168
+ rule(:function_call) do
169
+ (str("(") >> space? >>
170
+ arguments.maybe.as(:arguments) >>
171
+ space? >> str(")")).as(:function_call)
172
+ end
173
+
174
+ # Filter expression: [condition]
175
+ rule(:filter_expression) do
176
+ (str("[") >> space? >>
177
+ expression.as(:filter) >>
178
+ space? >> str("]")).as(:filter)
179
+ end
180
+
181
+ # Function arguments
182
+ rule(:arguments) do
183
+ named_arguments | positional_arguments
184
+ end
185
+
186
+ rule(:positional_arguments) do
187
+ expression.as(:arg) >>
188
+ (space? >> str(",") >> space? >> expression.as(:arg)).repeat
189
+ end
190
+
191
+ rule(:named_arguments) do
192
+ named_argument >>
193
+ (space? >> str(",") >> space? >> named_argument).repeat
194
+ end
195
+
196
+ rule(:named_argument) do
197
+ (identifier.as(:name) >> space? >>
198
+ str(":") >> space? >>
199
+ expression.as(:value)).as(:named_arg)
200
+ end
201
+
202
+ # Primary expressions
203
+ rule(:primary) do
204
+ null_literal |
205
+ boolean_literal |
206
+ number_literal |
207
+ string_literal |
208
+ list_literal |
209
+ context_literal |
210
+ range_literal |
211
+ function_definition |
212
+ identifier_or_function |
213
+ parenthesized
214
+ end
215
+
216
+ # Null literal
217
+ rule(:null_literal) { str("null").as(:null) }
218
+
219
+ # Boolean literals
220
+ rule(:boolean_literal) do
221
+ (str("true") | str("false")).as(:boolean)
222
+ end
223
+
224
+ # Number literal
225
+ rule(:number_literal) do
226
+ (str("-").maybe >> match["\\d"] >> match["\\d"].repeat >>
227
+ (str(".") >> match["\\d"].repeat(1)).maybe).as(:number)
228
+ end
229
+
230
+ # String literal
231
+ rule(:string_literal) do
232
+ str('"') >>
233
+ ((str("\\") >> any) | (str('"').absent? >> any)).repeat.as(:string) >>
234
+ str('"')
235
+ end
236
+
237
+ # List literal: [1, 2, 3]
238
+ rule(:list_literal) do
239
+ (str("[") >> space? >>
240
+ (expression >> (space? >> str(",") >> space? >> expression).repeat).maybe.as(:list) >>
241
+ space? >> str("]")).as(:list_literal)
242
+ end
243
+
244
+ # Context literal: {a: 1, b: 2}
245
+ rule(:context_literal) do
246
+ (str("{") >> space? >>
247
+ context_entries.maybe.as(:context) >>
248
+ space? >> str("}")).as(:context_literal)
249
+ end
250
+
251
+ rule(:context_entries) do
252
+ context_entry >> (space? >> str(",") >> space? >> context_entry).repeat
253
+ end
254
+
255
+ rule(:context_entry) do
256
+ (context_key.as(:key) >> space? >>
257
+ str(":") >> space? >>
258
+ expression.as(:value)).as(:entry)
259
+ end
260
+
261
+ rule(:context_key) do
262
+ identifier | string_literal
263
+ end
264
+
265
+ # Range literal: [1..10], (1..10), etc.
266
+ rule(:range_literal) do
267
+ ((str("[") | str("(")).as(:start_bracket) >>
268
+ space? >>
269
+ expression.as(:start) >>
270
+ space? >> str("..") >> space? >>
271
+ expression.as(:end) >>
272
+ space? >>
273
+ (str("]") | str(")")).as(:end_bracket)).as(:range)
274
+ end
275
+
276
+ # Function definition: function(x, y) x + y
277
+ rule(:function_definition) do
278
+ (str("function") >> space? >>
279
+ str("(") >> space? >>
280
+ parameters.maybe.as(:params) >>
281
+ space? >> str(")") >> space? >>
282
+ (str("external") | expression.as(:body))).as(:function_def)
283
+ end
284
+
285
+ rule(:parameters) do
286
+ identifier.as(:param) >>
287
+ (space? >> str(",") >> space? >> identifier.as(:param)).repeat
288
+ end
289
+
290
+ # Identifier or function name
291
+ rule(:identifier_or_function) do
292
+ (identifier.as(:name) >>
293
+ (str("(") >> space? >>
294
+ arguments.maybe.as(:arguments) >>
295
+ space? >> str(")")).maybe).as(:identifier_or_call)
296
+ end
297
+
298
+ # Identifier (variable/field name)
299
+ rule(:identifier) do
300
+ (match["a-zA-Z_"] >> match["a-zA-Z0-9_"].repeat).as(:identifier)
301
+ end
302
+
303
+ # Parenthesized expression
304
+ rule(:parenthesized) do
305
+ str("(") >> space? >> expression >> space? >> str(")")
306
+ end
307
+
308
+ # Unary tests (for DMN decision tables)
309
+ rule(:simple_positive_unary_tests) do
310
+ (positive_unary_test >>
311
+ (space? >> str(",") >> space? >> positive_unary_test).repeat).as(:unary_tests)
312
+ end
313
+
314
+ rule(:positive_unary_test) do
315
+ range_literal | comparison | expression
316
+ end
317
+
318
+ # Whitespace
319
+ rule(:space) { match["\\s"].repeat(1) }
320
+ rule(:space?) { match["\\s"].repeat }
321
+
322
+ # Root parsing method
323
+ root(:expression)
324
+
325
+ # Instance method for parsing with error handling
326
+ def parse(input)
327
+ super
328
+ rescue Parslet::ParseFailed => e
329
+ error_msg = "Failed to parse FEEL expression: #{e.parse_failure_cause.ascii_tree}"
330
+ error = FeelParseError.new(error_msg)
331
+ error.instance_variable_set(:@expression, input)
332
+ error.instance_variable_set(:@position, 0)
333
+ raise error
334
+ end
335
+
336
+ # Parse with error handling (class method for backward compatibility)
337
+ def self.parse_expression(input)
338
+ new.parse(input)
339
+ rescue Parslet::ParseFailed => e
340
+ error_msg = "Failed to parse FEEL expression: #{e.parse_failure_cause.ascii_tree}"
341
+ error = FeelParseError.new(error_msg)
342
+ error.instance_variable_set(:@expression, input)
343
+ error.instance_variable_set(:@position, 0)
344
+ raise error
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,276 @@
1
+ require_relative "../errors"
2
+ require_relative "types"
3
+
4
+ module DecisionAgent
5
+ module Dmn
6
+ module Feel
7
+ # Simple regex-based parser for common FEEL expressions
8
+ # Handles arithmetic, logical operators, and simple comparisons
9
+ # Uses operator precedence climbing for correct evaluation order
10
+ class SimpleParser
11
+ ARITHMETIC_OPS = %w[+ - * / ** %].freeze
12
+ LOGICAL_OPS = %w[and or].freeze
13
+ COMPARISON_OPS = %w[>= <= != > < =].freeze
14
+
15
+ # Operator precedence (higher number = higher precedence)
16
+ PRECEDENCE = {
17
+ "or" => 1,
18
+ "and" => 2,
19
+ "=" => 3,
20
+ "!=" => 3,
21
+ "<" => 4,
22
+ "<=" => 4,
23
+ ">" => 4,
24
+ ">=" => 4,
25
+ "+" => 5,
26
+ "-" => 5,
27
+ "*" => 6,
28
+ "/" => 6,
29
+ "%" => 6,
30
+ "**" => 7
31
+ }.freeze
32
+
33
+ def initialize
34
+ @tokens = []
35
+ @position = 0
36
+ end
37
+
38
+ # Check if expression can be handled by simple parser
39
+ def self.can_parse?(expression)
40
+ expr = expression.to_s.strip
41
+ # Can't handle: lists, contexts, functions, quantifiers, for expressions
42
+ return false if expr.match?(/[\[{]/) # Lists or contexts
43
+ return false if expr.match?(/\w+\s*\(/) # Function calls
44
+ return false if expr.match?(/\b(some|every|for|if)\b/) # Complex constructs
45
+
46
+ true
47
+ end
48
+
49
+ # Parse expression and return AST-like structure
50
+ def parse(expression)
51
+ expr = expression.to_s.strip
52
+ raise DecisionAgent::Dmn::FeelParseError, "Empty expression" if expr.empty?
53
+
54
+ @tokens = tokenize(expr)
55
+ @position = 0
56
+
57
+ parse_expression
58
+ end
59
+
60
+ private
61
+
62
+ # Tokenize the expression
63
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
64
+ def tokenize(expr)
65
+ tokens = []
66
+ i = 0
67
+
68
+ while i < expr.length
69
+ char = expr[i]
70
+
71
+ # Skip whitespace
72
+ if char.match?(/\s/)
73
+ i += 1
74
+ next
75
+ end
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
90
+
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
108
+
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
116
+
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
129
+
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
153
+
154
+ raise DecisionAgent::Dmn::FeelParseError, "Unexpected character: #{char} at position #{i}"
155
+ end
156
+
157
+ tokens
158
+ end
159
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
160
+
161
+ # Parse expression with operator precedence
162
+ def parse_expression(min_precedence = 0)
163
+ left = parse_unary
164
+
165
+ while @position < @tokens.length
166
+ token = current_token
167
+ break unless token && token[:type] == :operator
168
+
169
+ op = token[:value]
170
+ precedence = PRECEDENCE[op]
171
+ break if precedence.nil? || precedence < min_precedence
172
+
173
+ consume_token # Consume operator
174
+
175
+ right = parse_expression(precedence + 1)
176
+
177
+ left = {
178
+ type: operator_type(op),
179
+ operator: op,
180
+ left: left,
181
+ right: right
182
+ }
183
+ end
184
+
185
+ left
186
+ end
187
+
188
+ # Parse unary expressions (not, -, +)
189
+ def parse_unary
190
+ token = current_token
191
+
192
+ if token && token[:type] == :operator
193
+ case token[:value]
194
+ when "not"
195
+ consume_token
196
+ operand = parse_unary
197
+ return {
198
+ type: :logical,
199
+ operator: "not",
200
+ operand: operand
201
+ }
202
+ when "-"
203
+ consume_token
204
+ operand = parse_unary
205
+ return {
206
+ type: :arithmetic,
207
+ operator: "negate",
208
+ operand: operand
209
+ }
210
+ when "+"
211
+ consume_token # Skip unary plus
212
+ return parse_unary
213
+ end
214
+ end
215
+
216
+ parse_primary
217
+ end
218
+
219
+ # Parse primary expressions (numbers, strings, booleans, fields, parentheses)
220
+ def parse_primary
221
+ token = current_token
222
+
223
+ raise DecisionAgent::Dmn::FeelParseError, "Unexpected end of expression" unless token
224
+
225
+ case token[:type]
226
+ when :number
227
+ consume_token
228
+ { type: :literal, value: token[:value] }
229
+
230
+ when :string
231
+ consume_token
232
+ { type: :literal, value: token[:value] }
233
+
234
+ when :boolean
235
+ consume_token
236
+ { type: :boolean, value: token[:value] }
237
+
238
+ when :field
239
+ consume_token
240
+ { type: :field, name: token[:value] }
241
+
242
+ when :paren
243
+ raise DecisionAgent::Dmn::FeelParseError, "Unexpected closing parenthesis" unless token[:value] == "("
244
+
245
+ consume_token
246
+ expr = parse_expression
247
+ closing = current_token
248
+ raise DecisionAgent::Dmn::FeelParseError, "Expected closing parenthesis" unless closing && closing[:value] == ")"
249
+
250
+ consume_token
251
+ expr
252
+
253
+ else
254
+ raise DecisionAgent::Dmn::FeelParseError, "Unexpected token: #{token.inspect}"
255
+ end
256
+ end
257
+
258
+ def current_token
259
+ @tokens[@position]
260
+ end
261
+
262
+ def consume_token
263
+ @position += 1
264
+ end
265
+
266
+ def operator_type(op)
267
+ return :arithmetic if ARITHMETIC_OPS.include?(op)
268
+ return :logical if LOGICAL_OPS.include?(op) || op == "not"
269
+ return :comparison if COMPARISON_OPS.include?(op)
270
+
271
+ :unknown
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end