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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -919
  3. data/bin/decision_agent +5 -5
  4. data/lib/decision_agent/agent.rb +19 -26
  5. data/lib/decision_agent/audit/null_adapter.rb +1 -2
  6. data/lib/decision_agent/decision.rb +3 -1
  7. data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
  8. data/lib/decision_agent/dsl/rule_parser.rb +4 -6
  9. data/lib/decision_agent/dsl/schema_validator.rb +27 -31
  10. data/lib/decision_agent/errors.rb +21 -6
  11. data/lib/decision_agent/evaluation.rb +3 -1
  12. data/lib/decision_agent/evaluation_validator.rb +78 -0
  13. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
  14. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
  15. data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
  16. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
  17. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
  18. data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
  19. data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
  20. data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
  21. data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
  22. data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
  23. data/lib/decision_agent/replay/replay.rb +12 -22
  24. data/lib/decision_agent/scoring/base.rb +1 -1
  25. data/lib/decision_agent/scoring/consensus.rb +5 -5
  26. data/lib/decision_agent/scoring/weighted_average.rb +1 -1
  27. data/lib/decision_agent/version.rb +1 -1
  28. data/lib/decision_agent/versioning/activerecord_adapter.rb +141 -0
  29. data/lib/decision_agent/versioning/adapter.rb +100 -0
  30. data/lib/decision_agent/versioning/file_storage_adapter.rb +290 -0
  31. data/lib/decision_agent/versioning/version_manager.rb +127 -0
  32. data/lib/decision_agent/web/public/app.js +318 -0
  33. data/lib/decision_agent/web/public/index.html +56 -1
  34. data/lib/decision_agent/web/public/styles.css +219 -0
  35. data/lib/decision_agent/web/server.rb +169 -9
  36. data/lib/decision_agent.rb +11 -0
  37. data/lib/generators/decision_agent/install/install_generator.rb +40 -0
  38. data/lib/generators/decision_agent/install/templates/README +47 -0
  39. data/lib/generators/decision_agent/install/templates/migration.rb +37 -0
  40. data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
  41. data/lib/generators/decision_agent/install/templates/rule_version.rb +66 -0
  42. data/spec/activerecord_thread_safety_spec.rb +553 -0
  43. data/spec/agent_spec.rb +13 -13
  44. data/spec/api_contract_spec.rb +16 -16
  45. data/spec/audit_adapters_spec.rb +3 -3
  46. data/spec/comprehensive_edge_cases_spec.rb +86 -86
  47. data/spec/dsl_validation_spec.rb +83 -83
  48. data/spec/edge_cases_spec.rb +23 -23
  49. data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
  50. data/spec/examples.txt +548 -0
  51. data/spec/issue_verification_spec.rb +685 -0
  52. data/spec/json_rule_evaluator_spec.rb +15 -15
  53. data/spec/monitoring/alert_manager_spec.rb +378 -0
  54. data/spec/monitoring/metrics_collector_spec.rb +281 -0
  55. data/spec/monitoring/monitored_agent_spec.rb +222 -0
  56. data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
  57. data/spec/replay_edge_cases_spec.rb +58 -58
  58. data/spec/replay_spec.rb +11 -11
  59. data/spec/rfc8785_canonicalization_spec.rb +215 -0
  60. data/spec/scoring_spec.rb +1 -1
  61. data/spec/spec_helper.rb +9 -0
  62. data/spec/thread_safety_spec.rb +482 -0
  63. data/spec/thread_safety_spec.rb.broken +878 -0
  64. data/spec/versioning_spec.rb +777 -0
  65. data/spec/web_ui_rack_spec.rb +135 -0
  66. metadata +84 -11
data/bin/decision_agent CHANGED
@@ -24,11 +24,14 @@ def print_help
24
24
  decision_agent version
25
25
 
26
26
  For more information, visit:
27
- https://github.com/yourusername/decision_agent
27
+ https://github.com/samaswin87/decision_agent
28
28
  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
+ Bundler.const_set(:ORIGINAL_ENV, ENV.to_h.dup) if defined?(Bundler) && !Bundler.const_defined?(:ORIGINAL_ENV)
34
+
32
35
  puts "🎯 Starting DecisionAgent Rule Builder..."
33
36
  puts "📍 Server: http://localhost:#{port}"
34
37
  puts "⚡️ Press Ctrl+C to stop"
@@ -55,19 +58,16 @@ def validate_file(filepath)
55
58
  puts " Version: #{data['version'] || data[:version]}"
56
59
  puts " Ruleset: #{data['ruleset'] || data[:ruleset]}"
57
60
  puts " Rules: #{data['rules']&.size || 0}"
58
-
59
61
  rescue JSON::ParserError => e
60
62
  puts "❌ JSON Parsing Error:"
61
63
  puts " #{e.message}"
62
64
  exit 1
63
-
64
65
  rescue DecisionAgent::InvalidRuleDslError => e
65
66
  puts "❌ Validation Failed:"
66
67
  puts ""
67
68
  puts e.message
68
69
  exit 1
69
-
70
- rescue => e
70
+ rescue StandardError => e
71
71
  puts "❌ Unexpected Error:"
72
72
  puts " #{e.message}"
73
73
  exit 1
@@ -1,5 +1,6 @@
1
1
  require "digest"
2
2
  require "json"
3
+ require "json/canonicalization"
3
4
 
4
5
  module DecisionAgent
5
6
  class Agent
@@ -11,6 +12,9 @@ module DecisionAgent
11
12
  @audit_adapter = audit_adapter || Audit::NullAdapter.new
12
13
 
13
14
  validate_configuration!
15
+
16
+ # Freeze instance variables for thread-safety
17
+ @evaluators.freeze
14
18
  end
15
19
 
16
20
  def decide(context:, feedback: {})
@@ -20,6 +24,9 @@ module DecisionAgent
20
24
 
21
25
  raise NoEvaluationsError if evaluations.empty?
22
26
 
27
+ # Validate all evaluations for correctness and thread-safety
28
+ EvaluationValidator.validate_all!(evaluations)
29
+
23
30
  scored_result = @scoring_strategy.score(evaluations)
24
31
 
25
32
  decision_value = scored_result[:decision]
@@ -51,32 +58,24 @@ module DecisionAgent
51
58
  private
52
59
 
53
60
  def validate_configuration!
54
- if @evaluators.empty?
55
- raise InvalidConfigurationError, "At least one evaluator is required"
56
- end
61
+ raise InvalidConfigurationError, "At least one evaluator is required" if @evaluators.empty?
57
62
 
58
63
  @evaluators.each do |evaluator|
59
- unless evaluator.respond_to?(:evaluate)
60
- raise InvalidEvaluatorError
61
- end
64
+ raise InvalidEvaluatorError unless evaluator.respond_to?(:evaluate)
62
65
  end
63
66
 
64
- unless @scoring_strategy.respond_to?(:score)
65
- raise InvalidScoringStrategyError
66
- end
67
+ raise InvalidScoringStrategyError unless @scoring_strategy.respond_to?(:score)
67
68
 
68
- unless @audit_adapter.respond_to?(:record)
69
- raise InvalidAuditAdapterError
70
- end
69
+ return if @audit_adapter.respond_to?(:record)
70
+
71
+ raise InvalidAuditAdapterError
71
72
  end
72
73
 
73
74
  def collect_evaluations(context, feedback)
74
75
  @evaluators.map do |evaluator|
75
- begin
76
- evaluator.evaluate(context, feedback: feedback)
77
- rescue => e
78
- nil
79
- end
76
+ evaluator.evaluate(context, feedback: feedback)
77
+ rescue StandardError
78
+ nil
80
79
  end.compact
81
80
  end
82
81
 
@@ -132,16 +131,10 @@ module DecisionAgent
132
131
  Digest::SHA256.hexdigest(canonical)
133
132
  end
134
133
 
134
+ # Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
135
+ # This is the industry standard for cryptographic hashing of JSON data
135
136
  def canonical_json(obj)
136
- case obj
137
- when Hash
138
- sorted = obj.keys.sort.map { |k| [k.to_s, canonical_json(obj[k])] }.to_h
139
- JSON.generate(sorted, quirks_mode: false)
140
- when Array
141
- JSON.generate(obj.map { |v| canonical_json(v) }, quirks_mode: false)
142
- else
143
- obj.to_s
144
- end
137
+ obj.to_json_c14n
145
138
  end
146
139
  end
147
140
  end
@@ -1,8 +1,7 @@
1
1
  module DecisionAgent
2
2
  module Audit
3
3
  class NullAdapter < Adapter
4
- def record(decision, context)
5
- end
4
+ def record(decision, context); end
6
5
  end
7
6
  end
8
7
  end
@@ -10,6 +10,8 @@ module DecisionAgent
10
10
  @explanations = Array(explanations).map(&:freeze).freeze
11
11
  @evaluations = Array(evaluations).freeze
12
12
  @audit_payload = deep_freeze(audit_payload)
13
+
14
+ freeze
13
15
  end
14
16
 
15
17
  def to_h
@@ -34,7 +36,7 @@ module DecisionAgent
34
36
 
35
37
  def validate_confidence!(confidence)
36
38
  c = confidence.to_f
37
- raise InvalidConfidenceError.new(confidence) unless c >= 0.0 && c <= 1.0
39
+ raise InvalidConfidenceError, confidence unless c.between?(0.0, 1.0)
38
40
  end
39
41
 
40
42
  def deep_freeze(obj)
@@ -21,13 +21,12 @@ module DecisionAgent
21
21
  end
22
22
  end
23
23
 
24
- private
25
-
26
24
  # Evaluates 'all' condition - returns true only if ALL sub-conditions are true
27
25
  # Empty array returns true (vacuous truth)
28
26
  def self.evaluate_all(conditions, context)
29
27
  return true if conditions.is_a?(Array) && conditions.empty?
30
28
  return false unless conditions.is_a?(Array)
29
+
31
30
  conditions.all? { |cond| evaluate(cond, context) }
32
31
  end
33
32
 
@@ -35,6 +34,7 @@ module DecisionAgent
35
34
  # Empty array returns false (no options to match)
36
35
  def self.evaluate_any(conditions, context)
37
36
  return false unless conditions.is_a?(Array)
37
+
38
38
  conditions.any? { |cond| evaluate(cond, context) }
39
39
  end
40
40
 
@@ -117,6 +117,7 @@ module DecisionAgent
117
117
  keys = key_path.to_s.split(".")
118
118
  keys.reduce(hash) do |memo, key|
119
119
  return nil unless memo.is_a?(Hash)
120
+
120
121
  memo[key] || memo[key.to_sym]
121
122
  end
122
123
  end
@@ -126,7 +127,7 @@ module DecisionAgent
126
127
  def self.comparable?(val1, val2)
127
128
  (val1.is_a?(Numeric) || val1.is_a?(String)) &&
128
129
  (val2.is_a?(Numeric) || val2.is_a?(String)) &&
129
- val1.class == val2.class
130
+ val1.instance_of?(val2.class)
130
131
  end
131
132
  end
132
133
  end
@@ -13,14 +13,12 @@ module DecisionAgent
13
13
  rescue JSON::ParserError => e
14
14
  raise InvalidRuleDslError, "Invalid JSON syntax: #{e.message}\n\n" \
15
15
  "Please ensure your JSON is properly formatted. " \
16
- "Common issues:\n" \
17
- " - Missing or extra commas\n" \
18
- " - Unquoted keys or values\n" \
19
- " - Unmatched brackets or braces"
16
+ "Common issues:\n " \
17
+ "- Missing or extra commas\n " \
18
+ "- Unquoted keys or values\n " \
19
+ "- Unmatched brackets or braces"
20
20
  end
21
21
 
22
- private
23
-
24
22
  def self.parse_json(input)
25
23
  if input.is_a?(String)
26
24
  JSON.parse(input)
@@ -31,18 +31,18 @@ module DecisionAgent
31
31
  private
32
32
 
33
33
  def validate_root_structure
34
- unless @data.is_a?(Hash)
35
- @errors << "Root element must be a hash/object, got #{@data.class}"
36
- return
37
- end
34
+ return if @data.is_a?(Hash)
35
+
36
+ @errors << "Root element must be a hash/object, got #{@data.class}"
37
+ nil
38
38
  end
39
39
 
40
40
  def validate_version
41
41
  return if @errors.any? # Skip if root structure is invalid
42
42
 
43
- unless @data.key?("version") || @data.key?(:version)
44
- @errors << "Missing required field 'version'. Example: { \"version\": \"1.0\", ... }"
45
- end
43
+ return if @data.key?("version") || @data.key?(:version)
44
+
45
+ @errors << "Missing required field 'version'. Example: { \"version\": \"1.0\", ... }"
46
46
  end
47
47
 
48
48
  def validate_rules_array
@@ -55,9 +55,9 @@ module DecisionAgent
55
55
  return
56
56
  end
57
57
 
58
- unless rules.is_a?(Array)
59
- @errors << "Field 'rules' must be an array, got #{rules.class}. Example: \"rules\": [...]"
60
- end
58
+ return if rules.is_a?(Array)
59
+
60
+ @errors << "Field 'rules' must be an array, got #{rules.class}. Example: \"rules\": [...]"
61
61
  end
62
62
 
63
63
  def validate_each_rule
@@ -93,9 +93,9 @@ module DecisionAgent
93
93
  return
94
94
  end
95
95
 
96
- unless rule_id.is_a?(String) || rule_id.is_a?(Symbol)
97
- @errors << "#{rule_path}: Field 'id' must be a string, got #{rule_id.class}"
98
- end
96
+ return if rule_id.is_a?(String) || rule_id.is_a?(Symbol)
97
+
98
+ @errors << "#{rule_path}: Field 'id' must be a string, got #{rule_id.class}"
99
99
  end
100
100
 
101
101
  def validate_if_clause(rule, rule_path)
@@ -150,9 +150,7 @@ module DecisionAgent
150
150
  value = condition["value"] || condition[:value]
151
151
 
152
152
  # Validate field
153
- unless field
154
- @errors << "#{path}: Field condition missing 'field' key"
155
- end
153
+ @errors << "#{path}: Field condition missing 'field' key" unless field
156
154
 
157
155
  # Validate operator
158
156
  unless operator
@@ -174,10 +172,10 @@ module DecisionAgent
174
172
  def validate_operator(operator, path)
175
173
  operator_str = operator.to_s
176
174
 
177
- unless SUPPORTED_OPERATORS.include?(operator_str)
178
- @errors << "#{path}: Unsupported operator '#{operator}'. " \
179
- "Supported operators: #{SUPPORTED_OPERATORS.join(', ')}"
180
- end
175
+ return if SUPPORTED_OPERATORS.include?(operator_str)
176
+
177
+ @errors << "#{path}: Unsupported operator '#{operator}'. " \
178
+ "Supported operators: #{SUPPORTED_OPERATORS.join(', ')}"
181
179
  end
182
180
 
183
181
  def validate_field_path(field, path)
@@ -191,11 +189,11 @@ module DecisionAgent
191
189
  # Validate dot-notation
192
190
  parts = field.split(".")
193
191
 
194
- if parts.any?(&:empty?)
195
- @errors << "#{path}: Invalid field path '#{field}'. " \
196
- "Dot-notation paths cannot have empty segments. " \
197
- "Example: 'user.profile.role'"
198
- end
192
+ return unless parts.any?(&:empty?)
193
+
194
+ @errors << "#{path}: Invalid field path '#{field}'. " \
195
+ "Dot-notation paths cannot have empty segments. " \
196
+ "Example: 'user.profile.role'"
199
197
  end
200
198
 
201
199
  def validate_all_condition(condition, path)
@@ -241,9 +239,7 @@ module DecisionAgent
241
239
  # Validate decision
242
240
  decision = then_clause["decision"] || then_clause[:decision]
243
241
 
244
- unless decision
245
- @errors << "#{rule_path}.then: Missing required field 'decision'"
246
- end
242
+ @errors << "#{rule_path}.then: Missing required field 'decision'" unless decision
247
243
 
248
244
  # Validate optional weight
249
245
  weight = then_clause["weight"] || then_clause[:weight]
@@ -257,9 +253,9 @@ module DecisionAgent
257
253
  # Validate optional reason
258
254
  reason = then_clause["reason"] || then_clause[:reason]
259
255
 
260
- if reason && !reason.is_a?(String)
261
- @errors << "#{rule_path}.then.reason: Must be a string, got #{reason.class}"
262
- end
256
+ return unless reason && !reason.is_a?(String)
257
+
258
+ @errors << "#{rule_path}.then.reason: Must be a string, got #{reason.class}"
263
259
  end
264
260
 
265
261
  def format_errors
@@ -3,13 +3,13 @@ module DecisionAgent
3
3
 
4
4
  class InvalidRuleDslError < Error
5
5
  def initialize(message = "Invalid rule DSL structure")
6
- super(message)
6
+ super
7
7
  end
8
8
  end
9
9
 
10
10
  class NoEvaluationsError < Error
11
11
  def initialize(message = "No evaluators returned a decision")
12
- super(message)
12
+ super
13
13
  end
14
14
  end
15
15
 
@@ -26,25 +26,25 @@ module DecisionAgent
26
26
 
27
27
  class InvalidConfigurationError < Error
28
28
  def initialize(message = "Invalid agent configuration")
29
- super(message)
29
+ super
30
30
  end
31
31
  end
32
32
 
33
33
  class InvalidEvaluatorError < Error
34
34
  def initialize(message = "Evaluator must respond to #evaluate")
35
- super(message)
35
+ super
36
36
  end
37
37
  end
38
38
 
39
39
  class InvalidScoringStrategyError < Error
40
40
  def initialize(message = "Scoring strategy must respond to #score")
41
- super(message)
41
+ super
42
42
  end
43
43
  end
44
44
 
45
45
  class InvalidAuditAdapterError < Error
46
46
  def initialize(message = "Audit adapter must respond to #record")
47
- super(message)
47
+ super
48
48
  end
49
49
  end
50
50
 
@@ -59,4 +59,19 @@ 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
66
+ end
67
+ end
68
+
69
+ class ValidationError < Error
70
+ def initialize(message = "Validation failed")
71
+ super
72
+ end
73
+ end
74
+
75
+ # Alias for backward compatibility and clearer naming
76
+ ConfigurationError = InvalidConfigurationError
62
77
  end
@@ -10,6 +10,8 @@ module DecisionAgent
10
10
  @reason = reason.to_s.freeze
11
11
  @evaluator_name = evaluator_name.to_s.freeze
12
12
  @metadata = deep_freeze(metadata)
13
+
14
+ freeze
13
15
  end
14
16
 
15
17
  def to_h
@@ -35,7 +37,7 @@ module DecisionAgent
35
37
 
36
38
  def validate_weight!(weight)
37
39
  w = weight.to_f
38
- raise InvalidWeightError.new(weight) unless w >= 0.0 && w <= 1.0
40
+ raise InvalidWeightError, weight unless w.between?(0.0, 1.0)
39
41
  end
40
42
 
41
43
  def deep_freeze(obj)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ # Validates evaluation objects for correctness and thread-safety
5
+ class EvaluationValidator
6
+ class ValidationError < StandardError; end
7
+
8
+ # Validates a single evaluation
9
+ # @param evaluation [Evaluation] the evaluation to validate
10
+ # @raise [ValidationError] if validation fails
11
+ def self.validate!(evaluation)
12
+ raise ValidationError, "Evaluation cannot be nil" if evaluation.nil?
13
+ raise ValidationError, "Evaluation must be an Evaluation instance" unless evaluation.is_a?(Evaluation)
14
+
15
+ validate_decision!(evaluation.decision)
16
+ validate_weight!(evaluation.weight)
17
+ validate_reason!(evaluation.reason)
18
+ validate_evaluator_name!(evaluation.evaluator_name)
19
+ validate_frozen!(evaluation)
20
+
21
+ true
22
+ end
23
+
24
+ # Validates an array of evaluations
25
+ # @param evaluations [Array<Evaluation>] the evaluations to validate
26
+ # @raise [ValidationError] if validation fails
27
+ def self.validate_all!(evaluations)
28
+ raise ValidationError, "Evaluations must be an Array" unless evaluations.is_a?(Array)
29
+ raise ValidationError, "Evaluations array cannot be empty" if evaluations.empty?
30
+
31
+ evaluations.each_with_index do |evaluation, index|
32
+ validate!(evaluation)
33
+ rescue ValidationError => e
34
+ raise ValidationError, "Validation failed for evaluation at index #{index}: #{e.message}"
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ private_class_method def self.validate_decision!(decision)
41
+ raise ValidationError, "Decision cannot be nil" if decision.nil?
42
+ raise ValidationError, "Decision must be a String" unless decision.is_a?(String)
43
+ raise ValidationError, "Decision cannot be empty" if decision.strip.empty?
44
+ end
45
+
46
+ private_class_method def self.validate_weight!(weight)
47
+ raise ValidationError, "Weight cannot be nil" if weight.nil?
48
+ raise ValidationError, "Weight must be a Numeric" unless weight.is_a?(Numeric)
49
+ raise ValidationError, "Weight must be between 0 and 1" unless weight.between?(0, 1)
50
+ end
51
+
52
+ private_class_method def self.validate_reason!(reason)
53
+ raise ValidationError, "Reason cannot be nil" if reason.nil?
54
+ raise ValidationError, "Reason must be a String" unless reason.is_a?(String)
55
+ raise ValidationError, "Reason cannot be empty" if reason.strip.empty?
56
+ end
57
+
58
+ private_class_method def self.validate_evaluator_name!(name)
59
+ raise ValidationError, "Evaluator name cannot be nil" if name.nil?
60
+ raise ValidationError, "Evaluator name must be a String or Symbol" unless name.is_a?(String) || name.is_a?(Symbol)
61
+ end
62
+
63
+ private_class_method def self.validate_frozen!(evaluation)
64
+ raise ValidationError, "Evaluation must be frozen for thread-safety (call .freeze)" unless evaluation.frozen?
65
+
66
+ # Verify nested structures are also frozen
67
+ raise ValidationError, "Evaluation decision must be frozen" unless evaluation.decision.frozen?
68
+
69
+ raise ValidationError, "Evaluation reason must be frozen" unless evaluation.reason.frozen?
70
+
71
+ raise ValidationError, "Evaluation evaluator_name must be frozen" unless evaluation.evaluator_name.frozen?
72
+
73
+ return unless evaluation.metadata && !evaluation.metadata.frozen?
74
+
75
+ raise ValidationError, "Evaluation metadata must be frozen"
76
+ end
77
+ end
78
+ end
@@ -10,6 +10,12 @@ module DecisionAgent
10
10
  @ruleset = Dsl::RuleParser.parse(@rules_json)
11
11
  @ruleset_name = @ruleset["ruleset"] || "unknown"
12
12
  @name = name || "JsonRuleEvaluator(#{@ruleset_name})"
13
+
14
+ # Freeze ruleset to ensure thread-safety
15
+ deep_freeze(@ruleset)
16
+ @rules_json.freeze
17
+ @ruleset_name.freeze
18
+ @name.freeze
13
19
  end
14
20
 
15
21
  def evaluate(context, feedback: {})
@@ -46,6 +52,26 @@ module DecisionAgent
46
52
  Dsl::ConditionEvaluator.evaluate(if_clause, context)
47
53
  end
48
54
  end
55
+
56
+ # Deep freeze helper method
57
+ def deep_freeze(obj)
58
+ case obj
59
+ when Hash
60
+ obj.each do |k, v|
61
+ deep_freeze(k)
62
+ deep_freeze(v)
63
+ end
64
+ obj.freeze
65
+ when Array
66
+ obj.each { |item| deep_freeze(item) }
67
+ obj.freeze
68
+ when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
69
+ obj.freeze
70
+ else
71
+ obj.freeze if obj.respond_to?(:freeze)
72
+ end
73
+ obj
74
+ end
49
75
  end
50
76
  end
51
77
  end
@@ -11,12 +11,8 @@ module DecisionAgent
11
11
  @custom_metadata = metadata
12
12
  end
13
13
 
14
- def evaluate(context, feedback: {})
15
- metadata = if @custom_metadata
16
- @custom_metadata
17
- else
18
- { type: "static" }
19
- end
14
+ def evaluate(_context, feedback: {})
15
+ metadata = @custom_metadata || { type: "static" }
20
16
 
21
17
  Evaluation.new(
22
18
  decision: @decision,