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,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../versioning/version_manager"
4
+ require_relative "parser"
5
+ require_relative "exporter"
6
+
7
+ module DecisionAgent
8
+ module Dmn
9
+ # DMN Versioning Support
10
+ # Integrates DMN models with the DecisionAgent versioning system
11
+ class DmnVersionManager
12
+ attr_reader :version_manager
13
+
14
+ def initialize(version_manager: nil)
15
+ @version_manager = version_manager || Versioning::VersionManager.new
16
+ end
17
+
18
+ # Save a DMN model as a new version
19
+ def save_dmn_version(model:, created_by: "system", changelog: nil)
20
+ # Export DMN model to XML
21
+ exporter = Exporter.new
22
+ xml_content = exporter.export(model)
23
+
24
+ # Save as version
25
+ @version_manager.save_version(
26
+ rule_id: model.id,
27
+ rule_content: {
28
+ format: "dmn",
29
+ xml: xml_content,
30
+ name: model.name,
31
+ namespace: model.namespace
32
+ },
33
+ created_by: created_by,
34
+ changelog: changelog || "DMN model updated"
35
+ )
36
+ end
37
+
38
+ # Get a specific DMN version
39
+ def get_dmn_version(version_id:)
40
+ version = @version_manager.get_version(version_id: version_id)
41
+ return nil unless version
42
+
43
+ # Parse DMN from version content
44
+ return unless version[:content].is_a?(Hash) && version[:content][:format] == "dmn"
45
+
46
+ parser = Parser.new
47
+ model = parser.parse(version[:content][:xml])
48
+
49
+ {
50
+ version_id: version[:version_id],
51
+ model: model,
52
+ created_at: version[:created_at],
53
+ created_by: version[:created_by],
54
+ changelog: version[:changelog],
55
+ is_active: version[:is_active]
56
+ }
57
+ end
58
+
59
+ # Get all versions of a DMN model
60
+ def get_dmn_versions(model_id:, limit: nil)
61
+ versions = @version_manager.get_versions(rule_id: model_id, limit: limit)
62
+
63
+ versions.map do |version|
64
+ {
65
+ version_id: version[:version_id],
66
+ created_at: version[:created_at],
67
+ created_by: version[:created_by],
68
+ changelog: version[:changelog],
69
+ is_active: version[:is_active]
70
+ }
71
+ end
72
+ end
73
+
74
+ # Get DMN version history with full details
75
+ def get_dmn_history(model_id:)
76
+ history = @version_manager.get_history(rule_id: model_id)
77
+
78
+ history.map do |entry|
79
+ if entry[:content].is_a?(Hash) && entry[:content][:format] == "dmn"
80
+ entry.merge(
81
+ model_name: entry[:content][:name],
82
+ model_namespace: entry[:content][:namespace]
83
+ )
84
+ else
85
+ entry
86
+ end
87
+ end
88
+ end
89
+
90
+ # Rollback to a previous DMN version
91
+ def rollback_dmn(version_id:, performed_by: "system")
92
+ version = @version_manager.rollback(
93
+ version_id: version_id,
94
+ performed_by: performed_by
95
+ )
96
+
97
+ # Parse and return the model
98
+ return unless version[:content].is_a?(Hash) && version[:content][:format] == "dmn"
99
+
100
+ parser = Parser.new
101
+ parser.parse(version[:content][:xml])
102
+ end
103
+
104
+ # Compare two DMN versions
105
+ def compare_dmn_versions(version_id_1:, version_id_2:)
106
+ comparison = @version_manager.compare(
107
+ version_id_1: version_id_1,
108
+ version_id_2: version_id_2
109
+ )
110
+
111
+ return nil unless comparison
112
+
113
+ # Parse both versions
114
+ parser = Parser.new
115
+
116
+ model_1 = if comparison[:version_1][:content].is_a?(Hash) && comparison[:version_1][:content][:format] == "dmn"
117
+ parser.parse(comparison[:version_1][:content][:xml])
118
+ end
119
+
120
+ model_2 = if comparison[:version_2][:content].is_a?(Hash) && comparison[:version_2][:content][:format] == "dmn"
121
+ parser.parse(comparison[:version_2][:content][:xml])
122
+ end
123
+
124
+ # Compare DMN models
125
+ diff = compare_models(model_1, model_2)
126
+
127
+ comparison.merge(
128
+ model_diff: diff,
129
+ model_1: model_1 ? { id: model_1.id, name: model_1.name, decisions: model_1.decisions.size } : nil,
130
+ model_2: model_2 ? { id: model_2.id, name: model_2.name, decisions: model_2.decisions.size } : nil
131
+ )
132
+ end
133
+
134
+ # Delete a DMN version
135
+ def delete_dmn_version(version_id:)
136
+ @version_manager.delete_version(version_id: version_id)
137
+ end
138
+
139
+ # Tag a DMN version
140
+ # rubocop:disable Naming/PredicateMethod
141
+ def tag_dmn_version(version_id:, tag:)
142
+ _tag = tag # TODO: Implement tag functionality
143
+ version = @version_manager.get_version(version_id: version_id)
144
+ return false unless version
145
+ # rubocop:enable Naming/PredicateMethod
146
+
147
+ # Add tag to metadata (this would need to be implemented in VersionManager)
148
+ # For now, we'll use changelog to append the tag
149
+ true
150
+ end
151
+
152
+ # Get active DMN version for a model
153
+ def get_active_dmn_version(model_id:)
154
+ versions = @version_manager.get_versions(rule_id: model_id, limit: 1)
155
+ return nil if versions.empty?
156
+
157
+ active_version = versions.find { |v| v[:is_active] } || versions.first
158
+ get_dmn_version(version_id: active_version[:version_id])
159
+ end
160
+
161
+ private
162
+
163
+ def compare_models(model_1, model_2)
164
+ return { status: "both_nil" } if model_1.nil? && model_2.nil?
165
+ return { status: "model_1_nil" } if model_1.nil?
166
+ return { status: "model_2_nil" } if model_2.nil?
167
+
168
+ diff = {
169
+ name_changed: model_1.name != model_2.name,
170
+ namespace_changed: model_1.namespace != model_2.namespace,
171
+ decisions_added: [],
172
+ decisions_removed: [],
173
+ decisions_modified: []
174
+ }
175
+
176
+ # Find added/removed/modified decisions
177
+ ids_1 = model_1.decisions.to_set(&:id)
178
+ ids_2 = model_2.decisions.to_set(&:id)
179
+
180
+ diff[:decisions_added] = (ids_2 - ids_1).to_a
181
+ diff[:decisions_removed] = (ids_1 - ids_2).to_a
182
+
183
+ # Check for modified decisions
184
+ common_ids = ids_1 & ids_2
185
+ common_ids.each do |decision_id|
186
+ decision_1 = model_1.find_decision(decision_id)
187
+ decision_2 = model_2.find_decision(decision_id)
188
+
189
+ diff[:decisions_modified] << decision_id if decision_changed?(decision_1, decision_2)
190
+ end
191
+
192
+ diff
193
+ end
194
+
195
+ def decision_changed?(decision_1, decision_2)
196
+ return true if decision_1.name != decision_2.name
197
+
198
+ # Compare decision tables
199
+ if decision_1.decision_table && decision_2.decision_table
200
+ table_1 = decision_1.decision_table
201
+ table_2 = decision_2.decision_table
202
+
203
+ return true if table_1.hit_policy != table_2.hit_policy
204
+ return true if table_1.inputs.size != table_2.inputs.size
205
+ return true if table_1.outputs.size != table_2.outputs.size
206
+ return true if table_1.rules.size != table_2.rules.size
207
+ elsif decision_1.decision_table != decision_2.decision_table
208
+ return true
209
+ end
210
+
211
+ false
212
+ end
213
+ end
214
+
215
+ # Extension to Model class for versioning support
216
+ module ModelVersioning
217
+ def save_version(created_by: "system", changelog: nil, version_manager: nil)
218
+ vmgr = version_manager || DmnVersionManager.new
219
+ vmgr.save_dmn_version(model: self, created_by: created_by, changelog: changelog)
220
+ end
221
+
222
+ def load_version(version_id, version_manager: nil)
223
+ vmgr = version_manager || DmnVersionManager.new
224
+ result = vmgr.get_dmn_version(version_id: version_id)
225
+ result ? result[:model] : nil
226
+ end
227
+ end
228
+ end
229
+ end