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.
- checksums.yaml +4 -4
- data/README.md +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- 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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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_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
|
|
@@ -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
|
-
#
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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,
|
|
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
|
|