decision_agent 0.1.1 → 0.1.2
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 +138 -1000
- data/bin/decision_agent +5 -0
- data/lib/decision_agent/errors.rb +12 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +105 -0
- data/lib/decision_agent/versioning/adapter.rb +102 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +182 -0
- data/lib/decision_agent/versioning/version_manager.rb +135 -0
- data/lib/decision_agent/web/public/app.js +318 -0
- data/lib/decision_agent/web/public/index.html +55 -0
- data/lib/decision_agent/web/public/styles.css +219 -0
- data/lib/decision_agent/web/server.rb +166 -1
- data/lib/decision_agent.rb +4 -0
- data/lib/generators/decision_agent/install/install_generator.rb +40 -0
- data/lib/generators/decision_agent/install/templates/README +47 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +26 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +60 -0
- data/spec/versioning_spec.rb +673 -0
- metadata +17 -7
data/bin/decision_agent
CHANGED
|
@@ -29,6 +29,11 @@ def print_help
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def start_web_ui(port = 4567)
|
|
32
|
+
# Ruby 4.0 compatibility: Puma expects Bundler::ORIGINAL_ENV which was removed
|
|
33
|
+
if defined?(Bundler) && !Bundler.const_defined?(:ORIGINAL_ENV)
|
|
34
|
+
Bundler.const_set(:ORIGINAL_ENV, ENV.to_h.dup)
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
puts "🎯 Starting DecisionAgent Rule Builder..."
|
|
33
38
|
puts "📍 Server: http://localhost:#{port}"
|
|
34
39
|
puts "⚡️ Press Ctrl+C to stop"
|
|
@@ -59,4 +59,16 @@ module DecisionAgent
|
|
|
59
59
|
super("Weight must be between 0.0 and 1.0, got: #{weight}")
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
class NotFoundError < Error
|
|
64
|
+
def initialize(message = "Resource not found")
|
|
65
|
+
super(message)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class ValidationError < Error
|
|
70
|
+
def initialize(message = "Validation failed")
|
|
71
|
+
super(message)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
62
74
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative "adapter"
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Versioning
|
|
5
|
+
# ActiveRecord-based version storage adapter for Rails applications
|
|
6
|
+
# Requires ActiveRecord models to be set up in the Rails app
|
|
7
|
+
class ActiveRecordAdapter < Adapter
|
|
8
|
+
def initialize
|
|
9
|
+
unless defined?(ActiveRecord)
|
|
10
|
+
raise DecisionAgent::ConfigurationError,
|
|
11
|
+
"ActiveRecord is not available. Please ensure Rails/ActiveRecord is loaded."
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_version(rule_id:, content:, metadata: {})
|
|
16
|
+
# Get the next version number for this rule
|
|
17
|
+
last_version = rule_version_class.where(rule_id: rule_id)
|
|
18
|
+
.order(version_number: :desc)
|
|
19
|
+
.first
|
|
20
|
+
next_version_number = last_version ? last_version.version_number + 1 : 1
|
|
21
|
+
|
|
22
|
+
# Deactivate previous active versions
|
|
23
|
+
rule_version_class.where(rule_id: rule_id, status: "active")
|
|
24
|
+
.update_all(status: "archived")
|
|
25
|
+
|
|
26
|
+
# Create new version
|
|
27
|
+
version = rule_version_class.create!(
|
|
28
|
+
rule_id: rule_id,
|
|
29
|
+
version_number: next_version_number,
|
|
30
|
+
content: content.to_json,
|
|
31
|
+
created_by: metadata[:created_by] || "system",
|
|
32
|
+
changelog: metadata[:changelog] || "Version #{next_version_number}",
|
|
33
|
+
status: metadata[:status] || "active"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
serialize_version(version)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_versions(rule_id:, limit: nil)
|
|
40
|
+
query = rule_version_class.where(rule_id: rule_id)
|
|
41
|
+
.order(version_number: :desc)
|
|
42
|
+
query = query.limit(limit) if limit
|
|
43
|
+
|
|
44
|
+
query.map { |v| serialize_version(v) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_version(version_id:)
|
|
48
|
+
version = rule_version_class.find_by(id: version_id)
|
|
49
|
+
version ? serialize_version(version) : nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_version_by_number(rule_id:, version_number:)
|
|
53
|
+
version = rule_version_class.find_by(
|
|
54
|
+
rule_id: rule_id,
|
|
55
|
+
version_number: version_number
|
|
56
|
+
)
|
|
57
|
+
version ? serialize_version(version) : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_active_version(rule_id:)
|
|
61
|
+
version = rule_version_class.find_by(rule_id: rule_id, status: "active")
|
|
62
|
+
version ? serialize_version(version) : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def activate_version(version_id:)
|
|
66
|
+
version = rule_version_class.find(version_id)
|
|
67
|
+
|
|
68
|
+
# Deactivate all other versions for this rule
|
|
69
|
+
rule_version_class.where(rule_id: version.rule_id, status: "active")
|
|
70
|
+
.where.not(id: version_id)
|
|
71
|
+
.update_all(status: "archived")
|
|
72
|
+
|
|
73
|
+
# Activate this version
|
|
74
|
+
version.update!(status: "active")
|
|
75
|
+
|
|
76
|
+
serialize_version(version)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def rule_version_class
|
|
82
|
+
# Look for the RuleVersion model in the main app
|
|
83
|
+
if defined?(::RuleVersion)
|
|
84
|
+
::RuleVersion
|
|
85
|
+
else
|
|
86
|
+
raise DecisionAgent::ConfigurationError,
|
|
87
|
+
"RuleVersion model not found. Please run the generator to create it."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serialize_version(version)
|
|
92
|
+
{
|
|
93
|
+
id: version.id,
|
|
94
|
+
rule_id: version.rule_id,
|
|
95
|
+
version_number: version.version_number,
|
|
96
|
+
content: JSON.parse(version.content),
|
|
97
|
+
created_by: version.created_by,
|
|
98
|
+
created_at: version.created_at,
|
|
99
|
+
changelog: version.changelog,
|
|
100
|
+
status: version.status
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Versioning
|
|
3
|
+
# Abstract base class for version storage adapters
|
|
4
|
+
# Allows framework-agnostic versioning with pluggable storage backends
|
|
5
|
+
class Adapter
|
|
6
|
+
# Create a new version for a rule
|
|
7
|
+
# @param rule_id [String] Unique identifier for the rule
|
|
8
|
+
# @param content [Hash] Rule definition as a hash
|
|
9
|
+
# @param metadata [Hash] Additional metadata (created_by, changelog, etc.)
|
|
10
|
+
# @return [Hash] The created version
|
|
11
|
+
def create_version(rule_id:, content:, metadata: {})
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement #create_version"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List all versions for a specific rule
|
|
16
|
+
# @param rule_id [String] The rule identifier
|
|
17
|
+
# @param limit [Integer, nil] Optional limit for number of versions
|
|
18
|
+
# @return [Array<Hash>] Array of version hashes
|
|
19
|
+
def list_versions(rule_id:, limit: nil)
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #list_versions"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get a specific version by ID
|
|
24
|
+
# @param version_id [String, Integer] The version identifier
|
|
25
|
+
# @return [Hash, nil] The version hash or nil if not found
|
|
26
|
+
def get_version(version_id:)
|
|
27
|
+
raise NotImplementedError, "#{self.class} must implement #get_version"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get a specific version by rule_id and version_number
|
|
31
|
+
# @param rule_id [String] The rule identifier
|
|
32
|
+
# @param version_number [Integer] The version number
|
|
33
|
+
# @return [Hash, nil] The version hash or nil if not found
|
|
34
|
+
def get_version_by_number(rule_id:, version_number:)
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #get_version_by_number"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the active version for a rule
|
|
39
|
+
# @param rule_id [String] The rule identifier
|
|
40
|
+
# @return [Hash, nil] The active version or nil
|
|
41
|
+
def get_active_version(rule_id:)
|
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #get_active_version"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Activate a specific version
|
|
46
|
+
# @param version_id [String, Integer] The version to activate
|
|
47
|
+
# @return [Hash] The activated version
|
|
48
|
+
def activate_version(version_id:)
|
|
49
|
+
raise NotImplementedError, "#{self.class} must implement #activate_version"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Compare two versions
|
|
53
|
+
# @param version_id_1 [String, Integer] First version ID
|
|
54
|
+
# @param version_id_2 [String, Integer] Second version ID
|
|
55
|
+
# @return [Hash] Comparison result with differences
|
|
56
|
+
def compare_versions(version_id_1:, version_id_2:)
|
|
57
|
+
v1 = get_version(version_id: version_id_1)
|
|
58
|
+
v2 = get_version(version_id: version_id_2)
|
|
59
|
+
|
|
60
|
+
return nil if v1.nil? || v2.nil?
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
version_1: v1,
|
|
64
|
+
version_2: v2,
|
|
65
|
+
differences: calculate_diff(v1[:content], v2[:content])
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Delete a specific version
|
|
70
|
+
# @param version_id [String, Integer] The version to delete
|
|
71
|
+
# @return [Boolean] True if deleted successfully
|
|
72
|
+
def delete_version(version_id:)
|
|
73
|
+
raise NotImplementedError, "#{self.class} must implement #delete_version"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Calculate differences between two content hashes
|
|
79
|
+
# @param content1 [Hash] First content
|
|
80
|
+
# @param content2 [Hash] Second content
|
|
81
|
+
# @return [Hash] Differences
|
|
82
|
+
def calculate_diff(content1, content2)
|
|
83
|
+
{
|
|
84
|
+
added: content2.to_a - content1.to_a,
|
|
85
|
+
removed: content1.to_a - content2.to_a,
|
|
86
|
+
changed: detect_changes(content1, content2)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def detect_changes(hash1, hash2)
|
|
91
|
+
changes = {}
|
|
92
|
+
hash1.each do |key, value1|
|
|
93
|
+
value2 = hash2[key]
|
|
94
|
+
if value1 != value2 && !value2.nil?
|
|
95
|
+
changes[key] = { old: value1, new: value2 }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
changes
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
require_relative "adapter"
|
|
2
|
+
require "json"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module DecisionAgent
|
|
6
|
+
module Versioning
|
|
7
|
+
# File-based version storage adapter for non-Rails applications
|
|
8
|
+
# Stores versions as JSON files in a directory structure
|
|
9
|
+
class FileStorageAdapter < Adapter
|
|
10
|
+
attr_reader :storage_path
|
|
11
|
+
|
|
12
|
+
# Initialize with a storage directory
|
|
13
|
+
# @param storage_path [String] Path to store version files (default: ./versions)
|
|
14
|
+
def initialize(storage_path: "./versions")
|
|
15
|
+
@storage_path = storage_path
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
FileUtils.mkdir_p(@storage_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_version(rule_id:, content:, metadata: {})
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
create_version_unsafe(rule_id: rule_id, content: content, metadata: metadata)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def create_version_unsafe(rule_id:, content:, metadata: {})
|
|
29
|
+
# Get the next version number
|
|
30
|
+
versions = list_versions(rule_id: rule_id)
|
|
31
|
+
next_version_number = versions.empty? ? 1 : versions.first[:version_number] + 1
|
|
32
|
+
|
|
33
|
+
# Deactivate previous active versions
|
|
34
|
+
versions.each do |v|
|
|
35
|
+
if v[:status] == "active"
|
|
36
|
+
update_version_status(v[:id], "archived")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create version data
|
|
41
|
+
version_id = generate_version_id(rule_id, next_version_number)
|
|
42
|
+
version = {
|
|
43
|
+
id: version_id,
|
|
44
|
+
rule_id: rule_id,
|
|
45
|
+
version_number: next_version_number,
|
|
46
|
+
content: content,
|
|
47
|
+
created_by: metadata[:created_by] || "system",
|
|
48
|
+
created_at: Time.now.utc.iso8601,
|
|
49
|
+
changelog: metadata[:changelog] || "Version #{next_version_number}",
|
|
50
|
+
status: metadata[:status] || "active"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Write to file
|
|
54
|
+
write_version_file(version)
|
|
55
|
+
|
|
56
|
+
version
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
public
|
|
60
|
+
|
|
61
|
+
def list_versions(rule_id:, limit: nil)
|
|
62
|
+
versions = []
|
|
63
|
+
rule_dir = File.join(@storage_path, sanitize_filename(rule_id))
|
|
64
|
+
|
|
65
|
+
return versions unless Dir.exist?(rule_dir)
|
|
66
|
+
|
|
67
|
+
Dir.glob(File.join(rule_dir, "*.json")).each do |file|
|
|
68
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
versions.sort_by! { |v| -v[:version_number] }
|
|
72
|
+
limit ? versions.take(limit) : versions
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get_version(version_id:)
|
|
76
|
+
all_versions.find { |v| v[:id] == version_id }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_version_by_number(rule_id:, version_number:)
|
|
80
|
+
versions = list_versions(rule_id: rule_id)
|
|
81
|
+
versions.find { |v| v[:version_number] == version_number }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def get_active_version(rule_id:)
|
|
85
|
+
versions = list_versions(rule_id: rule_id)
|
|
86
|
+
versions.find { |v| v[:status] == "active" }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def activate_version(version_id:)
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
version = get_version(version_id: version_id)
|
|
92
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
93
|
+
|
|
94
|
+
# Deactivate all other versions for this rule
|
|
95
|
+
list_versions(rule_id: version[:rule_id]).each do |v|
|
|
96
|
+
if v[:id] != version_id && v[:status] == "active"
|
|
97
|
+
update_version_status(v[:id], "archived")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Activate this version
|
|
102
|
+
version[:status] = "active"
|
|
103
|
+
write_version_file(version)
|
|
104
|
+
|
|
105
|
+
version
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def delete_version(version_id:)
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
version = get_version(version_id: version_id)
|
|
112
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
113
|
+
|
|
114
|
+
# Prevent deletion of active versions
|
|
115
|
+
if version[:status] == "active"
|
|
116
|
+
raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Delete the file
|
|
120
|
+
rule_dir = File.join(@storage_path, sanitize_filename(version[:rule_id]))
|
|
121
|
+
filename = "#{version[:version_number]}.json"
|
|
122
|
+
filepath = File.join(rule_dir, filename)
|
|
123
|
+
|
|
124
|
+
if File.exist?(filepath)
|
|
125
|
+
File.delete(filepath)
|
|
126
|
+
true
|
|
127
|
+
else
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def all_versions
|
|
136
|
+
versions = []
|
|
137
|
+
return versions unless Dir.exist?(@storage_path)
|
|
138
|
+
|
|
139
|
+
Dir.glob(File.join(@storage_path, "*", "*.json")).each do |file|
|
|
140
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
versions
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def update_version_status(version_id, status)
|
|
147
|
+
version = get_version(version_id: version_id)
|
|
148
|
+
return unless version
|
|
149
|
+
|
|
150
|
+
version[:status] = status
|
|
151
|
+
write_version_file(version)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def write_version_file(version)
|
|
155
|
+
rule_dir = File.join(@storage_path, sanitize_filename(version[:rule_id]))
|
|
156
|
+
FileUtils.mkdir_p(rule_dir)
|
|
157
|
+
|
|
158
|
+
filename = "#{version[:version_number]}.json"
|
|
159
|
+
filepath = File.join(rule_dir, filename)
|
|
160
|
+
|
|
161
|
+
# Use atomic write to prevent race conditions during concurrent access
|
|
162
|
+
# Write to temp file first, then atomically rename
|
|
163
|
+
temp_file = "#{filepath}.tmp.#{Process.pid}.#{Thread.current.object_id}"
|
|
164
|
+
begin
|
|
165
|
+
File.write(temp_file, JSON.pretty_generate(version))
|
|
166
|
+
File.rename(temp_file, filepath)
|
|
167
|
+
ensure
|
|
168
|
+
# Clean up temp file if rename failed
|
|
169
|
+
File.delete(temp_file) if File.exist?(temp_file)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def generate_version_id(rule_id, version_number)
|
|
174
|
+
"#{rule_id}_v#{version_number}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def sanitize_filename(name)
|
|
178
|
+
name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Versioning
|
|
3
|
+
# High-level service for managing rule versions
|
|
4
|
+
# Provides a framework-agnostic API using pluggable storage adapters
|
|
5
|
+
class VersionManager
|
|
6
|
+
attr_reader :adapter
|
|
7
|
+
|
|
8
|
+
# Initialize with a storage adapter
|
|
9
|
+
# @param adapter [Adapter] The storage adapter to use
|
|
10
|
+
def initialize(adapter: nil)
|
|
11
|
+
@adapter = adapter || default_adapter
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Save a new version of a rule
|
|
15
|
+
# @param rule_id [String] Unique identifier for the rule
|
|
16
|
+
# @param rule_content [Hash] The rule definition
|
|
17
|
+
# @param created_by [String] User who created this version
|
|
18
|
+
# @param changelog [String] Description of changes
|
|
19
|
+
# @return [Hash] The created version
|
|
20
|
+
def save_version(rule_id:, rule_content:, created_by: "system", changelog: nil)
|
|
21
|
+
validate_rule_content!(rule_content)
|
|
22
|
+
|
|
23
|
+
metadata = {
|
|
24
|
+
created_by: created_by,
|
|
25
|
+
changelog: changelog || generate_default_changelog(rule_id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@adapter.create_version(
|
|
29
|
+
rule_id: rule_id,
|
|
30
|
+
content: rule_content,
|
|
31
|
+
metadata: metadata
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get all versions for a rule
|
|
36
|
+
# @param rule_id [String] The rule identifier
|
|
37
|
+
# @param limit [Integer, nil] Optional limit
|
|
38
|
+
# @return [Array<Hash>] Array of versions
|
|
39
|
+
def get_versions(rule_id:, limit: nil)
|
|
40
|
+
@adapter.list_versions(rule_id: rule_id, limit: limit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get a specific version
|
|
44
|
+
# @param version_id [String, Integer] The version identifier
|
|
45
|
+
# @return [Hash, nil] The version or nil
|
|
46
|
+
def get_version(version_id:)
|
|
47
|
+
@adapter.get_version(version_id: version_id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get the currently active version for a rule
|
|
51
|
+
# @param rule_id [String] The rule identifier
|
|
52
|
+
# @return [Hash, nil] The active version or nil
|
|
53
|
+
def get_active_version(rule_id:)
|
|
54
|
+
@adapter.get_active_version(rule_id: rule_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Rollback to a previous version (activate it)
|
|
58
|
+
# @param version_id [String, Integer] The version to rollback to
|
|
59
|
+
# @param performed_by [String] User performing the rollback
|
|
60
|
+
# @return [Hash] The activated version
|
|
61
|
+
def rollback(version_id:, performed_by: "system")
|
|
62
|
+
version = @adapter.activate_version(version_id: version_id)
|
|
63
|
+
|
|
64
|
+
# Create an audit trail of the rollback
|
|
65
|
+
save_version(
|
|
66
|
+
rule_id: version[:rule_id],
|
|
67
|
+
rule_content: version[:content],
|
|
68
|
+
created_by: performed_by,
|
|
69
|
+
changelog: "Rolled back to version #{version[:version_number]}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
version
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Compare two versions
|
|
76
|
+
# @param version_id_1 [String, Integer] First version
|
|
77
|
+
# @param version_id_2 [String, Integer] Second version
|
|
78
|
+
# @return [Hash] Comparison result
|
|
79
|
+
def compare(version_id_1:, version_id_2:)
|
|
80
|
+
@adapter.compare_versions(
|
|
81
|
+
version_id_1: version_id_1,
|
|
82
|
+
version_id_2: version_id_2
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get version history with metadata
|
|
87
|
+
# @param rule_id [String] The rule identifier
|
|
88
|
+
# @return [Hash] History with statistics
|
|
89
|
+
def get_history(rule_id:)
|
|
90
|
+
versions = get_versions(rule_id: rule_id)
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
rule_id: rule_id,
|
|
94
|
+
total_versions: versions.length,
|
|
95
|
+
active_version: get_active_version(rule_id: rule_id),
|
|
96
|
+
versions: versions,
|
|
97
|
+
created_at: versions.last&.dig(:created_at),
|
|
98
|
+
updated_at: versions.first&.dig(:created_at)
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Delete a specific version
|
|
103
|
+
# @param version_id [String, Integer] The version to delete
|
|
104
|
+
# @return [Boolean] True if deleted successfully
|
|
105
|
+
def delete_version(version_id:)
|
|
106
|
+
@adapter.delete_version(version_id: version_id)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def default_adapter
|
|
112
|
+
# Auto-detect the best adapter based on available frameworks
|
|
113
|
+
if defined?(ActiveRecord) && defined?(::RuleVersion)
|
|
114
|
+
require_relative "activerecord_adapter"
|
|
115
|
+
ActiveRecordAdapter.new
|
|
116
|
+
else
|
|
117
|
+
require_relative "file_storage_adapter"
|
|
118
|
+
FileStorageAdapter.new
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_rule_content!(content)
|
|
123
|
+
raise DecisionAgent::ValidationError, "Rule content cannot be nil" if content.nil?
|
|
124
|
+
raise DecisionAgent::ValidationError, "Rule content must be a Hash" unless content.is_a?(Hash)
|
|
125
|
+
raise DecisionAgent::ValidationError, "Rule content cannot be empty" if content.empty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def generate_default_changelog(rule_id)
|
|
129
|
+
versions = get_versions(rule_id: rule_id)
|
|
130
|
+
version_num = versions.empty? ? 1 : versions.first[:version_number] + 1
|
|
131
|
+
"Version #{version_num}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|