decision_agent 0.2.0 → 1.0.1

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -0,0 +1,217 @@
1
+ require "nokogiri"
2
+ require "set"
3
+ require_relative "errors"
4
+ require_relative "../versioning/version_manager"
5
+
6
+ module DecisionAgent
7
+ module Dmn
8
+ # Exports DecisionAgent rules to DMN XML format
9
+ class Exporter
10
+ def initialize(version_manager: nil)
11
+ @version_manager = version_manager || Versioning::VersionManager.new
12
+ end
13
+
14
+ # Export ruleset to DMN XML
15
+ # @param rule_id [String] Rule ID to export
16
+ # @param output_path [String, nil] Optional file path to write
17
+ # @return [String] DMN XML content
18
+ def export(rule_id, output_path: nil)
19
+ # Get active version
20
+ version = @version_manager.get_active_version(rule_id: rule_id)
21
+ raise InvalidDmnModelError, "No active version found for '#{rule_id}'" unless version
22
+
23
+ # Convert JSON rules to DMN
24
+ dmn_xml = convert_to_dmn(version[:content], rule_id)
25
+
26
+ # Write to file if path provided
27
+ File.write(output_path, dmn_xml) if output_path
28
+
29
+ dmn_xml
30
+ end
31
+
32
+ private
33
+
34
+ # Helper to get hash value with both string and symbol key support
35
+ def hash_get(hash, key)
36
+ hash[key.to_s] || hash[key.to_sym]
37
+ end
38
+
39
+ # rubocop:disable Metrics/MethodLength
40
+ def convert_to_dmn(rules_json, rule_id)
41
+ # Handle both string and symbol keys
42
+ ruleset_name = rules_json["ruleset"] || rules_json[:ruleset] || rule_id
43
+ rules = rules_json["rules"] || rules_json[:rules] || []
44
+
45
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
46
+ xml.definitions(
47
+ "xmlns" => "https://www.omg.org/spec/DMN/20191111/MODEL/",
48
+ "xmlns:dmndi" => "https://www.omg.org/spec/DMN/20191111/DMNDI/",
49
+ "xmlns:dc" => "http://www.omg.org/spec/DMN/20180521/DC/",
50
+ "id" => "definitions_#{rule_id}",
51
+ "name" => ruleset_name,
52
+ "namespace" => "http://decision_agent.local"
53
+ ) do
54
+ xml.decision(id: rule_id, name: ruleset_name) do
55
+ xml.decisionTable(
56
+ id: "#{rule_id}_table",
57
+ hitPolicy: "FIRST",
58
+ outputLabel: "decision"
59
+ ) do
60
+ # Extract unique inputs from rules
61
+ inputs = extract_inputs(rules)
62
+ inputs.each_with_index do |input, idx|
63
+ xml.input(id: "input_#{idx + 1}", label: input) do
64
+ xml.inputExpression(typeRef: "string") do
65
+ text_node = Nokogiri::XML::Node.new("text", xml.doc)
66
+ text_node.content = input
67
+ xml.parent.add_child(text_node)
68
+ end
69
+ end
70
+ end
71
+
72
+ # Single output for decision
73
+ xml.output(id: "output_1", label: "decision", name: "decision", typeRef: "string")
74
+
75
+ # Convert rules
76
+ rules.each_with_index do |rule, idx|
77
+ convert_rule_to_xml(xml, rule, inputs, idx)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ builder.to_xml
85
+ end
86
+ # rubocop:enable Metrics/MethodLength
87
+
88
+ def extract_inputs(rules)
89
+ # Extract all unique field names used in conditions
90
+ inputs = Set.new
91
+
92
+ return [] unless rules.is_a?(Array)
93
+
94
+ rules.each do |rule|
95
+ condition = hash_get(rule, "if")
96
+ extract_fields_from_condition(condition, inputs)
97
+ end
98
+
99
+ inputs.to_a.sort
100
+ end
101
+
102
+ def extract_fields_from_condition(condition, inputs)
103
+ return unless condition.is_a?(Hash)
104
+
105
+ if hash_get(condition, "field")
106
+ inputs << hash_get(condition, "field")
107
+ elsif hash_get(condition, "all")
108
+ hash_get(condition, "all").each { |c| extract_fields_from_condition(c, inputs) }
109
+ elsif hash_get(condition, "any")
110
+ hash_get(condition, "any").each { |c| extract_fields_from_condition(c, inputs) }
111
+ end
112
+ end
113
+
114
+ def convert_rule_to_xml(xml, rule, inputs, idx)
115
+ rule_id = hash_get(rule, "id") || "rule_#{idx + 1}"
116
+ xml.rule(id: rule_id) do
117
+ # Input entries (in order of inputs array)
118
+ inputs.each do |input_name|
119
+ feel_expr = condition_to_feel(hash_get(rule, "if"), input_name)
120
+ xml.inputEntry(id: "entry_#{idx + 1}_#{input_name}") do
121
+ text_node = Nokogiri::XML::Node.new("text", xml.doc)
122
+ text_node.content = feel_expr
123
+ xml.parent.add_child(text_node)
124
+ end
125
+ end
126
+
127
+ # Output entry
128
+ then_clause = hash_get(rule, "then")
129
+ decision_value = hash_get(then_clause, "decision") if then_clause
130
+ xml.outputEntry(id: "output_#{idx + 1}") do
131
+ text_node = Nokogiri::XML::Node.new("text", xml.doc)
132
+ text_node.content = format_feel_value(decision_value)
133
+ xml.parent.add_child(text_node)
134
+ end
135
+
136
+ # Description
137
+ reason = hash_get(then_clause, "reason") if then_clause
138
+ description = hash_get(rule, "description")
139
+ if reason || description
140
+ xml.description do
141
+ xml.text reason || description
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ def condition_to_feel(condition, target_field)
148
+ return "-" unless condition.is_a?(Hash)
149
+
150
+ # Find condition for this field
151
+ field_condition = find_field_condition(condition, target_field)
152
+ return "-" unless field_condition
153
+
154
+ # Convert operator and value to FEEL
155
+ op = hash_get(field_condition, "op")
156
+ value = hash_get(field_condition, "value")
157
+
158
+ convert_operator_to_feel(op, value)
159
+ end
160
+
161
+ def find_field_condition(condition, target_field)
162
+ if hash_get(condition, "field") == target_field
163
+ condition
164
+ elsif hash_get(condition, "all")
165
+ hash_get(condition, "all").each do |c|
166
+ result = find_field_condition(c, target_field)
167
+ return result if result
168
+ end
169
+ nil
170
+ elsif hash_get(condition, "any")
171
+ # For export, we pick first matching (Phase 2A limitation)
172
+ hash_get(condition, "any").each do |c|
173
+ result = find_field_condition(c, target_field)
174
+ return result if result
175
+ end
176
+ nil
177
+ end
178
+ end
179
+
180
+ def convert_operator_to_feel(op, value)
181
+ case op
182
+ when "eq"
183
+ format_feel_value(value)
184
+ when "neq"
185
+ "!= #{format_feel_value(value)}"
186
+ when "gt"
187
+ "> #{format_feel_value(value)}"
188
+ when "gte"
189
+ ">= #{format_feel_value(value)}"
190
+ when "lt"
191
+ "< #{format_feel_value(value)}"
192
+ when "lte"
193
+ "<= #{format_feel_value(value)}"
194
+ when "in"
195
+ "[#{value.map { |v| format_feel_value(v) }.join(', ')}]"
196
+ when "between"
197
+ "[#{value[0]}..#{value[1]}]"
198
+ else
199
+ format_feel_value(value)
200
+ end
201
+ end
202
+
203
+ def format_feel_value(value)
204
+ case value
205
+ when String
206
+ "\"#{value}\""
207
+ when Numeric
208
+ value.to_s
209
+ when TrueClass, FalseClass
210
+ value.to_s
211
+ else
212
+ value.to_s
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end