decision_agent 0.1.1 → 0.1.3
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 -919
- data/bin/decision_agent +5 -5
- data/lib/decision_agent/agent.rb +19 -26
- data/lib/decision_agent/audit/null_adapter.rb +1 -2
- data/lib/decision_agent/decision.rb +3 -1
- data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
- data/lib/decision_agent/dsl/rule_parser.rb +4 -6
- data/lib/decision_agent/dsl/schema_validator.rb +27 -31
- data/lib/decision_agent/errors.rb +21 -6
- data/lib/decision_agent/evaluation.rb +3 -1
- data/lib/decision_agent/evaluation_validator.rb +78 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
- data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
- data/lib/decision_agent/replay/replay.rb +12 -22
- data/lib/decision_agent/scoring/base.rb +1 -1
- data/lib/decision_agent/scoring/consensus.rb +5 -5
- data/lib/decision_agent/scoring/weighted_average.rb +1 -1
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +141 -0
- data/lib/decision_agent/versioning/adapter.rb +100 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +290 -0
- data/lib/decision_agent/versioning/version_manager.rb +127 -0
- data/lib/decision_agent/web/public/app.js +318 -0
- data/lib/decision_agent/web/public/index.html +56 -1
- data/lib/decision_agent/web/public/styles.css +219 -0
- data/lib/decision_agent/web/server.rb +169 -9
- data/lib/decision_agent.rb +11 -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 +37 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +66 -0
- data/spec/activerecord_thread_safety_spec.rb +553 -0
- data/spec/agent_spec.rb +13 -13
- data/spec/api_contract_spec.rb +16 -16
- data/spec/audit_adapters_spec.rb +3 -3
- data/spec/comprehensive_edge_cases_spec.rb +86 -86
- data/spec/dsl_validation_spec.rb +83 -83
- data/spec/edge_cases_spec.rb +23 -23
- data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
- data/spec/examples.txt +548 -0
- data/spec/issue_verification_spec.rb +685 -0
- data/spec/json_rule_evaluator_spec.rb +15 -15
- data/spec/monitoring/alert_manager_spec.rb +378 -0
- data/spec/monitoring/metrics_collector_spec.rb +281 -0
- data/spec/monitoring/monitored_agent_spec.rb +222 -0
- data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
- data/spec/replay_edge_cases_spec.rb +58 -58
- data/spec/replay_spec.rb +11 -11
- data/spec/rfc8785_canonicalization_spec.rb +215 -0
- data/spec/scoring_spec.rb +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/thread_safety_spec.rb +482 -0
- data/spec/thread_safety_spec.rb.broken +878 -0
- data/spec/versioning_spec.rb +777 -0
- data/spec/web_ui_rack_spec.rb +135 -0
- metadata +84 -11
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
require_relative "adapter"
|
|
2
|
+
require "json"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module DecisionAgent
|
|
6
|
+
module Versioning
|
|
7
|
+
# Status validation shared by adapters
|
|
8
|
+
module StatusValidator
|
|
9
|
+
VALID_STATUSES = %w[draft active archived].freeze
|
|
10
|
+
|
|
11
|
+
def validate_status!(status)
|
|
12
|
+
return if VALID_STATUSES.include?(status)
|
|
13
|
+
|
|
14
|
+
raise DecisionAgent::ValidationError,
|
|
15
|
+
"Invalid status '#{status}'. Must be one of: #{VALID_STATUSES.join(', ')}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# File-based version storage adapter for non-Rails applications
|
|
20
|
+
# Stores versions as JSON files in a directory structure
|
|
21
|
+
class FileStorageAdapter < Adapter
|
|
22
|
+
include StatusValidator
|
|
23
|
+
|
|
24
|
+
attr_reader :storage_path
|
|
25
|
+
|
|
26
|
+
# Initialize with a storage directory
|
|
27
|
+
# @param storage_path [String] Path to store version files (default: ./versions)
|
|
28
|
+
def initialize(storage_path: "./versions")
|
|
29
|
+
@storage_path = storage_path
|
|
30
|
+
# Per-rule mutex for better concurrency - allows different rules to be processed in parallel
|
|
31
|
+
@rule_mutexes = Hash.new { |h, k| h[k] = Mutex.new }
|
|
32
|
+
@rule_mutexes_lock = Mutex.new # Protects the hash itself
|
|
33
|
+
# Index cache: version_id => rule_id mapping for O(1) lookups
|
|
34
|
+
@version_index = {}
|
|
35
|
+
@version_index_lock = Mutex.new
|
|
36
|
+
FileUtils.mkdir_p(@storage_path)
|
|
37
|
+
load_version_index
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_version(rule_id:, content:, metadata: {})
|
|
41
|
+
with_rule_lock(rule_id) do
|
|
42
|
+
create_version_unsafe(rule_id: rule_id, content: content, metadata: metadata)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def create_version_unsafe(rule_id:, content:, metadata: {})
|
|
49
|
+
# Get the next version number
|
|
50
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
51
|
+
next_version_number = versions.empty? ? 1 : versions.first[:version_number] + 1
|
|
52
|
+
|
|
53
|
+
# Validate status if provided
|
|
54
|
+
status = metadata[:status] || "active"
|
|
55
|
+
validate_status!(status)
|
|
56
|
+
|
|
57
|
+
# Deactivate previous active versions
|
|
58
|
+
versions.each do |v|
|
|
59
|
+
update_version_status_unsafe(v[:id], "archived", rule_id) if v[:status] == "active"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create version data
|
|
63
|
+
version_id = generate_version_id(rule_id, next_version_number)
|
|
64
|
+
version = {
|
|
65
|
+
id: version_id,
|
|
66
|
+
rule_id: rule_id,
|
|
67
|
+
version_number: next_version_number,
|
|
68
|
+
content: content,
|
|
69
|
+
created_by: metadata[:created_by] || "system",
|
|
70
|
+
created_at: Time.now.utc.iso8601,
|
|
71
|
+
changelog: metadata[:changelog] || "Version #{next_version_number}",
|
|
72
|
+
status: status
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Write to file
|
|
76
|
+
write_version_file(version)
|
|
77
|
+
|
|
78
|
+
version
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
public
|
|
82
|
+
|
|
83
|
+
def list_versions(rule_id:, limit: nil)
|
|
84
|
+
with_rule_lock(rule_id) do
|
|
85
|
+
list_versions_unsafe(rule_id: rule_id, limit: limit)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_version(version_id:)
|
|
90
|
+
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
91
|
+
rule_id = get_rule_id_from_index(version_id)
|
|
92
|
+
return nil unless rule_id
|
|
93
|
+
|
|
94
|
+
# Now lock on the specific rule
|
|
95
|
+
with_rule_lock(rule_id) do
|
|
96
|
+
# Read only this rule's versions
|
|
97
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
98
|
+
versions.find { |v| v[:id] == version_id }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_version_by_number(rule_id:, version_number:)
|
|
103
|
+
with_rule_lock(rule_id) do
|
|
104
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
105
|
+
versions.find { |v| v[:version_number] == version_number }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def get_active_version(rule_id:)
|
|
110
|
+
with_rule_lock(rule_id) do
|
|
111
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
112
|
+
versions.find { |v| v[:status] == "active" }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def activate_version(version_id:)
|
|
117
|
+
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
118
|
+
rule_id = get_rule_id_from_index(version_id)
|
|
119
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless rule_id
|
|
120
|
+
|
|
121
|
+
# Now lock on the specific rule
|
|
122
|
+
with_rule_lock(rule_id) do
|
|
123
|
+
# Read only this rule's versions
|
|
124
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
125
|
+
version = versions.find { |v| v[:id] == version_id }
|
|
126
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
127
|
+
|
|
128
|
+
# Deactivate all other versions for this rule
|
|
129
|
+
versions.each do |v|
|
|
130
|
+
update_version_status_unsafe(v[:id], "archived", rule_id) if v[:id] != version_id && v[:status] == "active"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Activate this version
|
|
134
|
+
version[:status] = "active"
|
|
135
|
+
write_version_file(version)
|
|
136
|
+
|
|
137
|
+
version
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def delete_version(version_id:)
|
|
142
|
+
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
143
|
+
rule_id = get_rule_id_from_index(version_id)
|
|
144
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless rule_id
|
|
145
|
+
|
|
146
|
+
# Now lock on the specific rule
|
|
147
|
+
with_rule_lock(rule_id) do
|
|
148
|
+
# Read only this rule's versions
|
|
149
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
150
|
+
version = versions.find { |v| v[:id] == version_id }
|
|
151
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
152
|
+
|
|
153
|
+
# Prevent deletion of active versions
|
|
154
|
+
if version[:status] == "active"
|
|
155
|
+
raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Delete the file
|
|
159
|
+
rule_dir = File.join(@storage_path, sanitize_filename(rule_id))
|
|
160
|
+
filename = "#{version[:version_number]}.json"
|
|
161
|
+
filepath = File.join(rule_dir, filename)
|
|
162
|
+
|
|
163
|
+
if File.exist?(filepath)
|
|
164
|
+
File.delete(filepath)
|
|
165
|
+
# Remove from index
|
|
166
|
+
remove_from_index(version_id)
|
|
167
|
+
true
|
|
168
|
+
else
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def list_versions_unsafe(rule_id:, limit: nil)
|
|
177
|
+
versions = []
|
|
178
|
+
rule_dir = File.join(@storage_path, sanitize_filename(rule_id))
|
|
179
|
+
|
|
180
|
+
return versions unless Dir.exist?(rule_dir)
|
|
181
|
+
|
|
182
|
+
Dir.glob(File.join(rule_dir, "*.json")).each do |file|
|
|
183
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
versions.sort_by! { |v| -v[:version_number] }
|
|
187
|
+
limit ? versions.take(limit) : versions
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def all_versions_unsafe
|
|
191
|
+
versions = []
|
|
192
|
+
return versions unless Dir.exist?(@storage_path)
|
|
193
|
+
|
|
194
|
+
Dir.glob(File.join(@storage_path, "*", "*.json")).each do |file|
|
|
195
|
+
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
versions
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def update_version_status_unsafe(version_id, status, rule_id = nil)
|
|
202
|
+
# Validate status first
|
|
203
|
+
validate_status!(status)
|
|
204
|
+
|
|
205
|
+
# Use provided rule_id or look it up from index
|
|
206
|
+
rule_id ||= get_rule_id_from_index(version_id)
|
|
207
|
+
return unless rule_id
|
|
208
|
+
|
|
209
|
+
# Read only this rule's versions
|
|
210
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
211
|
+
version = versions.find { |v| v[:id] == version_id }
|
|
212
|
+
return unless version
|
|
213
|
+
|
|
214
|
+
version[:status] = status
|
|
215
|
+
write_version_file(version)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def write_version_file(version)
|
|
219
|
+
rule_dir = File.join(@storage_path, sanitize_filename(version[:rule_id]))
|
|
220
|
+
FileUtils.mkdir_p(rule_dir)
|
|
221
|
+
|
|
222
|
+
filename = "#{version[:version_number]}.json"
|
|
223
|
+
filepath = File.join(rule_dir, filename)
|
|
224
|
+
|
|
225
|
+
# Use atomic write to prevent race conditions during concurrent access
|
|
226
|
+
# Write to temp file first, then atomically rename
|
|
227
|
+
temp_file = "#{filepath}.tmp.#{Process.pid}.#{Thread.current.object_id}"
|
|
228
|
+
begin
|
|
229
|
+
File.write(temp_file, JSON.pretty_generate(version))
|
|
230
|
+
File.rename(temp_file, filepath)
|
|
231
|
+
# Update index after successful write
|
|
232
|
+
add_to_index(version[:id], version[:rule_id])
|
|
233
|
+
ensure
|
|
234
|
+
# Clean up temp file if rename failed
|
|
235
|
+
FileUtils.rm_f(temp_file)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def generate_version_id(rule_id, version_number)
|
|
240
|
+
"#{rule_id}_v#{version_number}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def sanitize_filename(name)
|
|
244
|
+
name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Get or create a mutex for a specific rule_id
|
|
248
|
+
# This allows different rules to be processed in parallel
|
|
249
|
+
def with_rule_lock(rule_id, &block)
|
|
250
|
+
mutex = @rule_mutexes_lock.synchronize { @rule_mutexes[rule_id] }
|
|
251
|
+
mutex.synchronize(&block)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Index management methods for O(1) version_id -> rule_id lookups
|
|
255
|
+
# This prevents the need to scan all 50,000 files when looking up a single version
|
|
256
|
+
|
|
257
|
+
def load_version_index
|
|
258
|
+
@version_index_lock.synchronize do
|
|
259
|
+
return unless Dir.exist?(@storage_path)
|
|
260
|
+
|
|
261
|
+
Dir.glob(File.join(@storage_path, "*", "*.json")).each do |file|
|
|
262
|
+
version = JSON.parse(File.read(file), symbolize_names: true)
|
|
263
|
+
@version_index[version[:id]] = version[:rule_id]
|
|
264
|
+
rescue JSON::ParserError
|
|
265
|
+
# Skip corrupted files
|
|
266
|
+
next
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def get_rule_id_from_index(version_id)
|
|
272
|
+
@version_index_lock.synchronize do
|
|
273
|
+
@version_index[version_id]
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def add_to_index(version_id, rule_id)
|
|
278
|
+
@version_index_lock.synchronize do
|
|
279
|
+
@version_index[version_id] = rule_id
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def remove_from_index(version_id)
|
|
284
|
+
@version_index_lock.synchronize do
|
|
285
|
+
@version_index.delete(version_id)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
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 without creating a duplicate)
|
|
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
|
+
# Simply activate the previous version without creating a duplicate
|
|
63
|
+
# The version history already contains the full record
|
|
64
|
+
@adapter.activate_version(version_id: version_id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Compare two versions
|
|
68
|
+
# @param version_id_1 [String, Integer] First version
|
|
69
|
+
# @param version_id_2 [String, Integer] Second version
|
|
70
|
+
# @return [Hash] Comparison result
|
|
71
|
+
def compare(version_id_1:, version_id_2:)
|
|
72
|
+
@adapter.compare_versions(
|
|
73
|
+
version_id_1: version_id_1,
|
|
74
|
+
version_id_2: version_id_2
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get version history with metadata
|
|
79
|
+
# @param rule_id [String] The rule identifier
|
|
80
|
+
# @return [Hash] History with statistics
|
|
81
|
+
def get_history(rule_id:)
|
|
82
|
+
versions = get_versions(rule_id: rule_id)
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
rule_id: rule_id,
|
|
86
|
+
total_versions: versions.length,
|
|
87
|
+
active_version: get_active_version(rule_id: rule_id),
|
|
88
|
+
versions: versions,
|
|
89
|
+
created_at: versions.last&.dig(:created_at),
|
|
90
|
+
updated_at: versions.first&.dig(:created_at)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Delete a specific version
|
|
95
|
+
# @param version_id [String, Integer] The version to delete
|
|
96
|
+
# @return [Boolean] True if deleted successfully
|
|
97
|
+
def delete_version(version_id:)
|
|
98
|
+
@adapter.delete_version(version_id: version_id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def default_adapter
|
|
104
|
+
# Auto-detect the best adapter based on available frameworks
|
|
105
|
+
if defined?(ActiveRecord) && defined?(::RuleVersion)
|
|
106
|
+
require_relative "activerecord_adapter"
|
|
107
|
+
ActiveRecordAdapter.new
|
|
108
|
+
else
|
|
109
|
+
require_relative "file_storage_adapter"
|
|
110
|
+
FileStorageAdapter.new
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_rule_content!(content)
|
|
115
|
+
raise DecisionAgent::ValidationError, "Rule content cannot be nil" if content.nil?
|
|
116
|
+
raise DecisionAgent::ValidationError, "Rule content must be a Hash" unless content.is_a?(Hash)
|
|
117
|
+
raise DecisionAgent::ValidationError, "Rule content cannot be empty" if content.empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generate_default_changelog(rule_id)
|
|
121
|
+
versions = get_versions(rule_id: rule_id)
|
|
122
|
+
version_num = versions.empty? ? 1 : versions.first[:version_number] + 1
|
|
123
|
+
"Version #{version_num}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|