decision_agent 0.1.7 → 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 (56) 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 +1132 -12
  24. data/lib/decision_agent/dsl/schema_validator.rb +12 -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/app.js +119 -1
  29. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  30. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  31. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  32. data/lib/decision_agent/web/public/index.html +71 -0
  33. data/lib/decision_agent/web/public/styles.css +21 -0
  34. data/lib/decision_agent/web/server.rb +465 -0
  35. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  36. data/spec/advanced_operators_spec.rb +2147 -0
  37. data/spec/auth/rbac_adapter_spec.rb +228 -0
  38. data/spec/dmn/decision_graph_spec.rb +282 -0
  39. data/spec/dmn/decision_tree_spec.rb +203 -0
  40. data/spec/dmn/feel/errors_spec.rb +18 -0
  41. data/spec/dmn/feel/functions_spec.rb +400 -0
  42. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  43. data/spec/dmn/feel/types_spec.rb +176 -0
  44. data/spec/dmn/feel_parser_spec.rb +489 -0
  45. data/spec/dmn/hit_policy_spec.rb +202 -0
  46. data/spec/dmn/integration_spec.rb +226 -0
  47. data/spec/examples.txt +1909 -0
  48. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  49. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  50. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  51. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  52. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  53. data/spec/performance_optimizations_spec.rb +10 -3
  54. data/spec/thread_safety_spec.rb +10 -2
  55. data/spec/web_ui_rack_spec.rb +294 -0
  56. metadata +66 -1
@@ -0,0 +1,797 @@
1
+ require_relative "../errors"
2
+ require_relative "simple_parser"
3
+ require_relative "parser"
4
+ require_relative "transformer"
5
+ require_relative "functions"
6
+ require_relative "types"
7
+ require_relative "../../dsl/condition_evaluator"
8
+
9
+ module DecisionAgent
10
+ module Dmn
11
+ module Feel
12
+ # FEEL expression evaluator with hybrid parsing strategy
13
+ # Phase 2A: Basic comparisons, ranges, list membership (regex-based)
14
+ # Phase 2B: Arithmetic, logical operators, functions (enhanced parser)
15
+ # Maps FEEL expressions to DecisionAgent ConditionEvaluator
16
+ # rubocop:disable Metrics/ClassLength
17
+ class Evaluator
18
+ def initialize
19
+ @simple_parser = SimpleParser.new
20
+ @parslet_parser = Parser.new
21
+ @transformer = Transformer.new
22
+ @cache = {}
23
+ @cache_mutex = Mutex.new
24
+ @ast_cache = {} # Cache for Parslet AST nodes
25
+ @use_parslet = true # Enable full Parslet parser
26
+ end
27
+
28
+ # Evaluate a FEEL expression against a context
29
+ # @param expression [String] FEEL expression (e.g., ">= 18", "in [1,2,3]", "age + 5")
30
+ # @param field_name [String] The field name being evaluated
31
+ # @param context [Hash] Evaluation context
32
+ # @return [Boolean] Evaluation result
33
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
34
+ def evaluate(expression, field_name, context)
35
+ return true if expression == "-" # DMN "don't care" marker
36
+
37
+ # Try Parslet parser first (Phase 2B)
38
+ if @use_parslet
39
+ begin
40
+ expr_key = expression.to_s.strip
41
+
42
+ # Check AST cache first
43
+ ast = @cache_mutex.synchronize do
44
+ @ast_cache[expr_key]
45
+ end
46
+
47
+ if ast.nil?
48
+ parse_tree = @parslet_parser.parse(expr_key)
49
+ ast = @transformer.apply(parse_tree)
50
+ @cache_mutex.synchronize do
51
+ @ast_cache[expr_key] = ast
52
+ end
53
+ end
54
+
55
+ result = evaluate_ast_node(ast, context)
56
+ # If result is nil and AST is a simple field reference that doesn't exist in context,
57
+ # fall back to Phase 2A approach to return condition structure
58
+ unless result.nil? && ast.is_a?(Hash) && ast[:type] == :field &&
59
+ !context.key?(ast[:name]) && !context.key?(ast[:name].to_s) &&
60
+ !context.key?(ast[:name].to_sym)
61
+ return result
62
+ end
63
+ # Fall through to Phase 2A
64
+ rescue Parslet::ParseFailed, FeelParseError, FeelTransformError => e
65
+ # Fall back to Phase 2A approach
66
+ warn "Parslet parse failed: #{e.message}, falling back" if ENV["DEBUG_FEEL"]
67
+ end
68
+ end
69
+
70
+ # Phase 2A approach: use condition structures
71
+ # Check cache first (thread-safe)
72
+ cache_key = "#{expression}::#{field_name}"
73
+ condition = @cache_mutex.synchronize do
74
+ @cache[cache_key]
75
+ end
76
+
77
+ return Dsl::ConditionEvaluator.evaluate(condition, context) if condition
78
+
79
+ # Parse and translate expression to condition structure
80
+ expr_str = expression.to_s.strip
81
+
82
+ # Check if expression matches any known pattern that can be successfully parsed
83
+ is_supported = literal?(expr_str) ||
84
+ comparison_expression?(expr_str) ||
85
+ list_expression?(expr_str) ||
86
+ range_expression?(expr_str)
87
+
88
+ # For SimpleParser, check if it can actually parse successfully
89
+ if SimpleParser.can_parse?(expr_str)
90
+ begin
91
+ @simple_parser.parse(expr_str)
92
+ is_supported = true
93
+ rescue FeelParseError
94
+ # SimpleParser says it can parse, but actually can't - not supported
95
+ is_supported = false
96
+ end
97
+ end
98
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
99
+
100
+ condition = parse_expression_to_condition(expression, field_name, context)
101
+
102
+ # If parse_expression_to_condition returned nil, create default condition structure
103
+ unless condition.is_a?(Hash)
104
+ condition = {
105
+ "field" => field_name,
106
+ "op" => "eq",
107
+ "value" => parse_value(expr_str)
108
+ }
109
+ end
110
+
111
+ # Store in cache (thread-safe)
112
+ @cache_mutex.synchronize do
113
+ @cache[cache_key] = condition
114
+ end
115
+
116
+ # For completely unsupported expressions (no patterns matched), return condition structure
117
+ # This allows fallback to literal equality for unknown syntax
118
+ return condition unless is_supported
119
+
120
+ # Delegate to existing ConditionEvaluator for supported expressions
121
+ evaluation_result = Dsl::ConditionEvaluator.evaluate(condition, context)
122
+
123
+ # If evaluation returns false for a simple equality check and the field doesn't exist in context,
124
+ # treat as unsupported expression and return condition structure (fallback behavior)
125
+ if evaluation_result == false && condition["op"] == "eq"
126
+ field_key = condition["field"]
127
+ field_exists = context.key?(field_key) || context.key?(field_key.to_s) || context.key?(field_key.to_sym)
128
+ return condition unless field_exists
129
+ end
130
+
131
+ # If evaluation returns nil, return condition structure as fallback
132
+ return condition if evaluation_result.nil?
133
+
134
+ evaluation_result
135
+ end
136
+
137
+ # Parse FEEL expression into operator and value (for internal use by Adapter)
138
+ # This maintains backward compatibility with Phase 2A
139
+ def parse_expression(expr)
140
+ expr = expr.to_s.strip
141
+
142
+ # Handle literal values (quoted strings, numbers, booleans)
143
+ return parse_literal(expr) if literal?(expr)
144
+
145
+ # Handle comparison operators
146
+ return parse_comparison(expr) if comparison_expression?(expr)
147
+
148
+ # Handle list membership
149
+ return parse_list_membership(expr) if list_expression?(expr)
150
+
151
+ # Handle range expressions
152
+ return parse_range(expr) if range_expression?(expr)
153
+
154
+ # Check if it's a simple parsable expression (arithmetic/logical)
155
+ if SimpleParser.can_parse?(expr)
156
+ begin
157
+ ast = @simple_parser.parse(expr)
158
+ return translate_ast(ast, nil)
159
+ rescue FeelParseError
160
+ # Fall back to literal equality
161
+ end
162
+ end
163
+
164
+ # Default: equality
165
+ { operator: "eq", value: parse_value(expr) }
166
+ end
167
+
168
+ private
169
+
170
+ def literal?(expr)
171
+ # Quoted string
172
+ return true if expr.start_with?('"') && expr.end_with?('"')
173
+ # Number
174
+ return true if expr.match?(/^-?\d+(\.\d+)?$/)
175
+ # Boolean
176
+ return true if %w[true false].include?(expr.downcase)
177
+
178
+ false
179
+ end
180
+
181
+ def parse_literal(expr)
182
+ if expr.start_with?('"') && expr.end_with?('"')
183
+ # String literal
184
+ { operator: "eq", value: expr[1..-2] }
185
+ elsif expr.match?(/^-?\d+\.\d+$/)
186
+ # Float
187
+ { operator: "eq", value: expr.to_f }
188
+ elsif expr.match?(/^-?\d+$/)
189
+ # Integer
190
+ { operator: "eq", value: expr.to_i }
191
+ elsif expr.downcase == "true"
192
+ { operator: "eq", value: true }
193
+ elsif expr.downcase == "false"
194
+ { operator: "eq", value: false }
195
+ else
196
+ { operator: "eq", value: expr }
197
+ end
198
+ end
199
+
200
+ def comparison_expression?(expr)
201
+ expr.match?(/^(>=|<=|>|<|!=|=)/)
202
+ end
203
+
204
+ def parse_comparison(expr)
205
+ # Extract operator
206
+ operator_match = expr.match(/^(>=|<=|>|<|!=|=)\s*(.+)/)
207
+ return { operator: "eq", value: expr } unless operator_match
208
+
209
+ feel_op = operator_match[1]
210
+ value_str = operator_match[2]
211
+
212
+ # Map FEEL operator to ConditionEvaluator operator
213
+ condition_op = case feel_op
214
+ when ">=" then "gte"
215
+ when "<=" then "lte"
216
+ when ">" then "gt"
217
+ when "<" then "lt"
218
+ when "!=" then "neq"
219
+ when "=" then "eq"
220
+ else "eq"
221
+ end
222
+
223
+ { operator: condition_op, value: parse_value(value_str) }
224
+ end
225
+
226
+ def list_expression?(expr)
227
+ expr.match?(/\[.+\]/)
228
+ end
229
+
230
+ def parse_list_membership(expr)
231
+ # Handle "in [1, 2, 3]" or just "[1, 2, 3]"
232
+ list_match = expr.match(/(?:in\s+)?\[(.+)\]/)
233
+ return { operator: "eq", value: expr } unless list_match
234
+
235
+ items_str = list_match[1]
236
+ items = items_str.split(",").map { |item| parse_value(item.strip) }
237
+
238
+ { operator: "in", value: items }
239
+ end
240
+
241
+ def range_expression?(expr)
242
+ # FEEL ranges like "[10..20]", "(10..20)", etc.
243
+ expr.match?(/[\[(]\d+(\.\d+)?\.\.\d+(\.\d+)?[\])]/)
244
+ end
245
+
246
+ def parse_range(expr)
247
+ # Parse FEEL range syntax: [min..max], (min..max), [min..max), (min..max]
248
+ range_match = expr.match(/([\[(])(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)([\])])/)
249
+ return { operator: "eq", value: expr } unless range_match
250
+
251
+ inclusive_start = range_match[1] == "["
252
+ min_val = parse_value(range_match[2])
253
+ max_val = parse_value(range_match[3])
254
+ inclusive_end = range_match[4] == "]"
255
+
256
+ # For Phase 2A, we only support fully inclusive ranges
257
+ # Map to 'between' operator
258
+ if inclusive_start && inclusive_end
259
+ { operator: "between", value: [min_val, max_val] }
260
+ else
261
+ # Fall back to complex condition (Phase 2B)
262
+ raise FeelParseError,
263
+ "Half-open ranges not yet supported: #{expr}. " \
264
+ "Use [min..max] for inclusive ranges."
265
+ end
266
+ end
267
+
268
+ def parse_value(str)
269
+ str = str.to_s.strip
270
+
271
+ # Remove quotes
272
+ return str[1..-2] if str.start_with?('"') && str.end_with?('"')
273
+
274
+ # Try to parse as number
275
+ if str.match?(/^-?\d+\.\d+$/)
276
+ return str.to_f
277
+ elsif str.match?(/^-?\d+$/)
278
+ return str.to_i
279
+ end
280
+
281
+ # Boolean
282
+ return true if str.downcase == "true"
283
+ return false if str.downcase == "false"
284
+
285
+ # Return as string
286
+ str
287
+ end
288
+
289
+ # Parse expression to condition structure (Phase 2A backward compatibility)
290
+ def parse_expression_to_condition(expression, field_name, context)
291
+ expr = expression.to_s.strip
292
+
293
+ # Try Phase 2A patterns
294
+ if literal?(expr) || comparison_expression?(expr) || list_expression?(expr) || range_expression?(expr)
295
+ parsed = parse_expression(expr)
296
+ return {
297
+ "field" => field_name,
298
+ "op" => parsed[:operator],
299
+ "value" => parsed[:value]
300
+ }
301
+ end
302
+
303
+ # Try simple parser for arithmetic/logical expressions
304
+ if SimpleParser.can_parse?(expr)
305
+ begin
306
+ ast = @simple_parser.parse(expr)
307
+ translated = translate_ast(ast, field_name, context)
308
+ # If translate_ast returns a valid Hash, return it
309
+ # Otherwise, fall through to default literal equality
310
+ return translated if translated.is_a?(Hash)
311
+ rescue FeelParseError, StandardError => e
312
+ warn "FEEL parse warning: #{e.message}, falling back to literal match" if ENV["DEBUG_FEEL"]
313
+ # Fall through to default
314
+ end
315
+ end
316
+
317
+ # Default: literal equality - always return a Hash
318
+ {
319
+ "field" => field_name,
320
+ "op" => "eq",
321
+ "value" => parse_value(expr)
322
+ }
323
+ end
324
+
325
+ # Translate AST to ConditionEvaluator format
326
+ def translate_ast(node, field_name, context = {})
327
+ case node[:type]
328
+ when :literal
329
+ # Just a value
330
+ return node[:value] if field_name.nil?
331
+
332
+ { "field" => field_name, "op" => "eq", "value" => node[:value] }
333
+
334
+ when :field
335
+ # Field reference - get value from context
336
+ context.to_h[node[:name].to_sym] || context.to_h[node[:name]]
337
+
338
+ when :arithmetic
339
+ translate_arithmetic(node, field_name, context)
340
+
341
+ when :logical
342
+ translate_logical(node, field_name, context)
343
+
344
+ when :comparison
345
+ translate_comparison(node, field_name, context)
346
+
347
+ else
348
+ raise FeelEvaluationError.new("Unknown AST node type: #{node[:type]}", expression: node.inspect)
349
+ end
350
+ end
351
+
352
+ # Translate arithmetic operations
353
+ def translate_arithmetic(node, _field_name, context)
354
+ op = node[:operator]
355
+
356
+ if op == "negate"
357
+ # Unary negation
358
+ operand_val = evaluate_ast_value(node[:operand], context)
359
+ return -operand_val
360
+ end
361
+
362
+ # Binary arithmetic
363
+ left_val = evaluate_ast_value(node[:left], context)
364
+ right_val = evaluate_ast_value(node[:right], context)
365
+
366
+ case op
367
+ when "+" then left_val + right_val
368
+ when "-" then left_val - right_val
369
+ when "*" then left_val * right_val
370
+ when "/" then left_val / right_val.to_f
371
+ when "**" then left_val**right_val
372
+ when "%" then left_val % right_val
373
+ else
374
+ raise FeelEvaluationError, "Unknown arithmetic operator: #{op}"
375
+ end
376
+ end
377
+
378
+ # Translate logical operations
379
+ def translate_logical(node, field_name, context)
380
+ op = node[:operator]
381
+
382
+ if op == "not"
383
+ # Unary NOT
384
+ operand = translate_ast(node[:operand], nil, context)
385
+ return { "all" => [{ "field" => field_name, "op" => "eq", "value" => false }] } if operand == true
386
+ return { "all" => [{ "field" => field_name, "op" => "eq", "value" => true }] } if operand == false
387
+
388
+ return !operand
389
+ end
390
+
391
+ # Binary logical (and, or)
392
+ left_condition = ast_to_condition(node[:left], field_name, context)
393
+ right_condition = ast_to_condition(node[:right], field_name, context)
394
+
395
+ case op
396
+ when "and"
397
+ { "all" => [left_condition, right_condition] }
398
+ when "or"
399
+ { "any" => [left_condition, right_condition] }
400
+ else
401
+ raise FeelEvaluationError, "Unknown logical operator: #{op}"
402
+ end
403
+ end
404
+
405
+ # Translate comparison operations
406
+ def translate_comparison(node, _field_name, context)
407
+ left_val = evaluate_ast_value(node[:left], context)
408
+ right_val = evaluate_ast_value(node[:right], context)
409
+ op = node[:operator]
410
+
411
+ # Map FEEL comparison to ConditionEvaluator operator
412
+ condition_op = case op
413
+ when ">=" then "gte"
414
+ when "<=" then "lte"
415
+ when ">" then "gt"
416
+ when "<" then "lt"
417
+ when "!=" then "neq"
418
+ when "=" then "eq"
419
+ else "eq"
420
+ end
421
+
422
+ # If left side is a field reference, use it as the field
423
+ if node[:left][:type] == :field
424
+ return {
425
+ "field" => node[:left][:name],
426
+ "op" => condition_op,
427
+ "value" => right_val
428
+ }
429
+ end
430
+
431
+ # Otherwise, evaluate both sides and return boolean result
432
+ case op
433
+ when ">=" then left_val >= right_val
434
+ when "<=" then left_val <= right_val
435
+ when ">" then left_val > right_val
436
+ when "<" then left_val < right_val
437
+ when "!=" then left_val != right_val
438
+ when "=" then left_val == right_val
439
+ else left_val == right_val
440
+ end
441
+ end
442
+
443
+ # Convert AST node to condition structure
444
+ def ast_to_condition(node, field_name, context)
445
+ case node[:type]
446
+ when :comparison
447
+ translate_comparison(node, field_name, context)
448
+ when :logical
449
+ translate_logical(node, field_name, context)
450
+ when :field
451
+ # Field reference in boolean context
452
+ { "field" => node[:name], "op" => "eq", "value" => true }
453
+ when :literal
454
+ # Literal in boolean context
455
+ { "field" => field_name, "op" => "eq", "value" => node[:value] }
456
+ else
457
+ # Evaluate and create condition
458
+ val = translate_ast(node, nil, context)
459
+ { "field" => field_name, "op" => "eq", "value" => val }
460
+ end
461
+ end
462
+
463
+ # Evaluate AST node to get a value (not a condition)
464
+ def evaluate_ast_value(node, context)
465
+ case node[:type]
466
+ when :literal
467
+ node[:value]
468
+ when :field
469
+ context.to_h[node[:name].to_sym] || context.to_h[node[:name]] || 0
470
+ when :arithmetic
471
+ translate_arithmetic(node, nil, context)
472
+ else
473
+ translate_ast(node, nil, context)
474
+ end
475
+ end
476
+
477
+ # Evaluate Parslet AST node (Phase 2B - full FEEL support)
478
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
479
+ def evaluate_ast_node(node, context)
480
+ return node unless node.is_a?(Hash)
481
+
482
+ # Handle nodes without type - might be raw Parslet output
483
+ if node[:type].nil? || node[:type].to_s.empty?
484
+ # Try to extract value from common Parslet structures
485
+ return node[:value] if node.key?(:value)
486
+ return node[:number] if node.key?(:number)
487
+ return node[:string] if node.key?(:string)
488
+ return node[:boolean] if node.key?(:boolean)
489
+
490
+ return node
491
+ end
492
+
493
+ case node[:type]
494
+ when :number, :string, :boolean
495
+ node[:value]
496
+ when :null
497
+ nil
498
+ when :field
499
+ get_field_value(node[:name], context)
500
+ when :list, :list_literal
501
+ evaluate_list(node, context)
502
+ when :context, :context_literal
503
+ evaluate_context(node, context)
504
+ when :range
505
+ evaluate_range(node, context)
506
+ when :function_call
507
+ evaluate_function_call(node, context)
508
+ when :property_access
509
+ evaluate_property_access(node, context)
510
+ when :comparison
511
+ evaluate_comparison_node?(node, context)
512
+ when :arithmetic
513
+ evaluate_arithmetic_node(node, context)
514
+ when :logical
515
+ evaluate_logical_node(node, context)
516
+ when :conditional
517
+ evaluate_conditional(node, context)
518
+ when :quantified
519
+ evaluate_quantified(node, context)
520
+ when :for
521
+ evaluate_for(node, context)
522
+ when :filter
523
+ evaluate_filter(node, context)
524
+ when :between
525
+ evaluate_between?(node, context)
526
+ when :in
527
+ evaluate_in_node?(node, context)
528
+ when :instance_of
529
+ evaluate_instance_of?(node, context)
530
+ else
531
+ raise FeelEvaluationError, "Unknown AST node type: #{node[:type]}"
532
+ end
533
+ end
534
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
535
+
536
+ # Get field value from context
537
+ def get_field_value(field_name, context)
538
+ ctx = context.to_h
539
+ ctx[field_name.to_sym] || ctx[field_name] || ctx[field_name.to_s]
540
+ end
541
+
542
+ # Evaluate list literal
543
+ def evaluate_list(node, context)
544
+ return [] if node[:elements].nil? || node[:elements].empty?
545
+
546
+ Array(node[:elements]).map { |elem| evaluate_ast_node(elem, context) }
547
+ end
548
+
549
+ # Evaluate context literal
550
+ def evaluate_context(node, context)
551
+ result = {}
552
+ return result if node[:pairs].nil? || node[:pairs].empty?
553
+
554
+ node[:pairs].each do |key, value|
555
+ result[key.to_sym] = evaluate_ast_node(value, context)
556
+ end
557
+ result
558
+ end
559
+
560
+ # Evaluate range
561
+ def evaluate_range(node, context)
562
+ start_val = evaluate_ast_node(node[:start], context)
563
+ end_val = evaluate_ast_node(node[:end], context)
564
+
565
+ {
566
+ type: :range,
567
+ start: start_val,
568
+ end: end_val,
569
+ start_inclusive: node[:start_inclusive],
570
+ end_inclusive: node[:end_inclusive]
571
+ }
572
+ end
573
+
574
+ # Evaluate function call
575
+ def evaluate_function_call(node, context)
576
+ # Extract function name - could be a string or a field node
577
+ function_name = if node[:name].is_a?(Hash)
578
+ if node[:name][:type] == :field
579
+ node[:name][:name]
580
+ else
581
+ node[:name][:name] || node[:name][:identifier] || node[:name].to_s
582
+ end
583
+ else
584
+ node[:name]
585
+ end
586
+
587
+ args = Array(node[:arguments]).map { |arg| evaluate_ast_node(arg, context) }
588
+
589
+ Functions.execute(function_name.to_s, args, context)
590
+ end
591
+
592
+ # Evaluate property access
593
+ def evaluate_property_access(node, context)
594
+ object = evaluate_ast_node(node[:object], context)
595
+ property = node[:property]
596
+
597
+ case object
598
+ when Hash
599
+ object[property.to_sym] || object[property.to_s] || object[property]
600
+ when Types::Context
601
+ object[property.to_sym]
602
+ else
603
+ object.respond_to?(property) ? object.send(property) : nil
604
+ end
605
+ end
606
+
607
+ # Evaluate comparison node
608
+ def evaluate_comparison_node?(node, context)
609
+ left_val = evaluate_ast_node(node[:left], context)
610
+ right_val = evaluate_ast_node(node[:right], context)
611
+
612
+ case node[:operator]
613
+ when "=" then left_val == right_val
614
+ when "!=" then left_val != right_val
615
+ when "<" then left_val < right_val
616
+ when ">" then left_val > right_val
617
+ when "<=" then left_val <= right_val
618
+ when ">=" then left_val >= right_val
619
+ else false
620
+ end
621
+ end
622
+
623
+ # Evaluate arithmetic node
624
+ def evaluate_arithmetic_node(node, context)
625
+ if node[:operand]
626
+ # Unary operation
627
+ operand_val = evaluate_ast_node(node[:operand], context)
628
+ return node[:operator] == "negate" ? -operand_val : operand_val
629
+ end
630
+
631
+ # Binary operation
632
+ left_val = evaluate_ast_node(node[:left], context)
633
+ right_val = evaluate_ast_node(node[:right], context)
634
+
635
+ case node[:operator]
636
+ when "+" then left_val + right_val
637
+ when "-" then left_val - right_val
638
+ when "*" then left_val * right_val
639
+ when "/" then left_val / right_val.to_f
640
+ when "**" then left_val**right_val
641
+ when "%" then left_val % right_val
642
+ else 0
643
+ end
644
+ end
645
+
646
+ # Evaluate logical node
647
+ def evaluate_logical_node(node, context)
648
+ if node[:operand]
649
+ # Unary NOT
650
+ operand_val = evaluate_ast_node(node[:operand], context)
651
+ return !operand_val
652
+ end
653
+
654
+ # Binary operation
655
+ left_val = evaluate_ast_node(node[:left], context)
656
+
657
+ case node[:operator]
658
+ when "and"
659
+ return false unless left_val
660
+
661
+ right_val = evaluate_ast_node(node[:right], context)
662
+ left_val && right_val
663
+ when "or"
664
+ return true if left_val
665
+
666
+ right_val = evaluate_ast_node(node[:right], context)
667
+ left_val || right_val
668
+ else
669
+ false
670
+ end
671
+ end
672
+
673
+ # Evaluate if/then/else conditional
674
+ def evaluate_conditional(node, context)
675
+ condition_val = evaluate_ast_node(node[:condition], context)
676
+
677
+ if condition_val
678
+ evaluate_ast_node(node[:then_expr], context)
679
+ else
680
+ evaluate_ast_node(node[:else_expr], context)
681
+ end
682
+ end
683
+
684
+ # Evaluate quantified expression (some/every)
685
+ def evaluate_quantified(node, context)
686
+ list_val = evaluate_ast_node(node[:list], context)
687
+ return false unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
688
+
689
+ variable = node[:variable]
690
+
691
+ case node[:quantifier]
692
+ when "some"
693
+ Array(list_val).any? do |item|
694
+ item_context = context.to_h.merge(variable.to_sym => item)
695
+ evaluate_ast_node(node[:condition], item_context)
696
+ end
697
+ when "every"
698
+ Array(list_val).all? do |item|
699
+ item_context = context.to_h.merge(variable.to_sym => item)
700
+ evaluate_ast_node(node[:condition], item_context)
701
+ end
702
+ else
703
+ false
704
+ end
705
+ end
706
+
707
+ # Evaluate for expression
708
+ def evaluate_for(node, context)
709
+ list_val = evaluate_ast_node(node[:list], context)
710
+ return [] unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
711
+
712
+ variable = node[:variable]
713
+
714
+ Array(list_val).map do |item|
715
+ item_context = context.to_h.merge(variable.to_sym => item)
716
+ evaluate_ast_node(node[:return_expr], item_context)
717
+ end
718
+ end
719
+
720
+ # Evaluate filter expression
721
+ def evaluate_filter(node, context)
722
+ list_val = evaluate_ast_node(node[:list], context)
723
+ return [] unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
724
+
725
+ Array(list_val).select do |item|
726
+ # For filter, use 'item' as the implicit variable
727
+ item_context = if item.is_a?(Hash)
728
+ context.to_h.merge(item)
729
+ else
730
+ context.to_h.merge(item: item)
731
+ end
732
+ evaluate_ast_node(node[:condition], item_context)
733
+ end
734
+ end
735
+
736
+ # Evaluate between expression
737
+ def evaluate_between?(node, context)
738
+ value = evaluate_ast_node(node[:value], context)
739
+ min_val = evaluate_ast_node(node[:min], context)
740
+ max_val = evaluate_ast_node(node[:max], context)
741
+
742
+ value.between?(min_val, max_val)
743
+ end
744
+
745
+ # Evaluate in expression
746
+ def evaluate_in_node?(node, context)
747
+ value = evaluate_ast_node(node[:value], context)
748
+ list_val = evaluate_ast_node(node[:list], context)
749
+
750
+ if list_val.is_a?(Array) || list_val.is_a?(Types::List)
751
+ Array(list_val).include?(value)
752
+ elsif list_val.is_a?(Hash) && list_val[:type] == :range
753
+ # Check if value is in range
754
+ in_range?(value, list_val)
755
+ else
756
+ false
757
+ end
758
+ end
759
+
760
+ # Evaluate instance of expression
761
+ def evaluate_instance_of?(node, context)
762
+ value = evaluate_ast_node(node[:value], context)
763
+ type_name = node[:type_name]
764
+
765
+ case type_name
766
+ when "number"
767
+ value.is_a?(Numeric)
768
+ when "string"
769
+ value.is_a?(String)
770
+ when "boolean"
771
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
772
+ when "date"
773
+ value.is_a?(Types::Date) || value.is_a?(Date) || value.is_a?(Time)
774
+ when "time"
775
+ value.is_a?(Types::Time) || value.is_a?(Time)
776
+ when "duration"
777
+ value.is_a?(Types::Duration)
778
+ when "list"
779
+ value.is_a?(Array) || value.is_a?(Types::List)
780
+ when "context"
781
+ value.is_a?(Hash) || value.is_a?(Types::Context)
782
+ else
783
+ false
784
+ end
785
+ end
786
+
787
+ # Check if value is in range
788
+ def in_range?(value, range)
789
+ start_check = range[:start_inclusive] ? value >= range[:start] : value > range[:start]
790
+ end_check = range[:end_inclusive] ? value <= range[:end] : value < range[:end]
791
+ start_check && end_check
792
+ end
793
+ end
794
+ # rubocop:enable Metrics/ClassLength
795
+ end
796
+ end
797
+ end