Class: RosettAi::Adopter::RuleAdopter
- Inherits:
-
Object
- Object
- RosettAi::Adopter::RuleAdopter
- Defined in:
- lib/rosett_ai/adopter/rule_adopter.rb
Overview
Analyzes compiled markdown rule files for inconsistencies, conflicts, harmful content, duplicates, and other issues.
Supports four layers of data privacy protection:
- Opt-in per file — sensitive: true in YAML excludes from API analysis
- Redaction — regex patterns replace matches before sending to API
- Configurable endpoint — ANTHROPIC_API_BASE_URL for proxy/Bedrock/Vertex
- Local-only mode — structural checks without API calls
Constant Summary collapse
- GENERATED_MARKER =
Returns Marker string identifying generated files.
'<!-- rosett-ai-'
Instance Attribute Summary collapse
-
#cache_path ⇒ Object
readonly
Returns the value of attribute cache_path.
-
#engine ⇒ Object
readonly
Returns the value of attribute engine.
-
#redactions_path ⇒ Object
readonly
Returns the value of attribute redactions_path.
-
#rules_dir ⇒ Object
readonly
Returns the value of attribute rules_dir.
Instance Method Summary collapse
-
#analyze(files) ⇒ Hash
Perform analysis on the given files.
-
#analyze_local(files) ⇒ Hash
Perform local-only structural analysis.
-
#build_prompt(files) ⇒ String
Build the analysis prompt from file contents.
-
#cached_result(checksum) ⇒ Hash?
Retrieve a cached analysis result by checksum.
-
#content_checksum(files) ⇒ String
Compute a SHA-256 checksum of the given files.
-
#discover_managed_files ⇒ Array<String>
Find all rai-managed rule files in the rules directory.
- #evaluate(local_only: false) ⇒ Object
-
#filter_sensitive(files, sensitive_sources) ⇒ Array<String>
Exclude files marked as sensitive from the file list.
-
#initialize(rules_dir:, cache_path:, redactions_path:, engine: 'claude') ⇒ RuleAdopter
constructor
A new instance of RuleAdopter.
-
#redact(content) ⇒ String
Apply redaction patterns to content before API submission.
-
#sensitive_files ⇒ Array<String>
List behaviour files marked as sensitive.
-
#write_cache(checksum, result) ⇒ void
Persist analysis results to the cache file.
Constructor Details
#initialize(rules_dir:, cache_path:, redactions_path:, engine: 'claude') ⇒ RuleAdopter
Returns a new instance of RuleAdopter.
28 29 30 31 32 33 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 28 def initialize(rules_dir:, cache_path:, redactions_path:, engine: 'claude') @rules_dir = Pathname.new(rules_dir) @cache_path = Pathname.new(cache_path) @redactions_path = Pathname.new(redactions_path) @engine = engine.to_s end |
Instance Attribute Details
#cache_path ⇒ Object (readonly)
Returns the value of attribute cache_path.
26 27 28 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 26 def cache_path @cache_path end |
#engine ⇒ Object (readonly)
Returns the value of attribute engine.
26 27 28 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 26 def engine @engine end |
#redactions_path ⇒ Object (readonly)
Returns the value of attribute redactions_path.
26 27 28 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 26 def redactions_path @redactions_path end |
#rules_dir ⇒ Object (readonly)
Returns the value of attribute rules_dir.
26 27 28 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 26 def rules_dir @rules_dir end |
Instance Method Details
#analyze(files) ⇒ Hash
Perform analysis on the given files.
133 134 135 136 137 138 139 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 133 def analyze(files) return empty_result if files.empty? prompt = build_prompt(files) executor = resolve_executor executor.analyze(prompt) end |
#analyze_local(files) ⇒ Hash
Perform local-only structural analysis.
145 146 147 148 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 145 def analyze_local(files) collector = LocalAnalysisCollector.new collector.analyze(files) end |
#build_prompt(files) ⇒ String
Build the analysis prompt from file contents.
107 108 109 110 111 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 107 def build_prompt(files) parts = [prompt_header] append_file_contents(parts, files) parts.join("\n") end |
#cached_result(checksum) ⇒ Hash?
Retrieve a cached analysis result by checksum.
75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 75 def cached_result(checksum) return nil unless cache_path.exist? data = RosettAi::YamlLoader.load_file(cache_path.to_s, permitted_classes: [Time, Date]) return nil unless data.is_a?(Hash) && data['checksum'] == checksum return nil if cache_expired?(data) data['result'] rescue Psych::SyntaxError, Psych::DisallowedClass => e RosettAi.logger.warn("Corrupt adopt cache (#{e.}), will re-analyze") nil end |
#content_checksum(files) ⇒ String
Compute a SHA-256 checksum of the given files.
66 67 68 69 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 66 def content_checksum(files) content = files.sort.map { |f| File.read(f) }.join Digest::SHA256.hexdigest(content) end |
#discover_managed_files ⇒ Array<String>
Find all rai-managed rule files in the rules directory.
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 51 def discover_managed_files return [] unless rules_dir.exist? Dir.glob(rules_dir.join('*.md')).select do |file| first_line = File.open(file, &:readline) first_line.start_with?(GENERATED_MARKER) rescue EOFError false end.sort end |
#evaluate(local_only: false) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 35 def evaluate(local_only: false) files = discover_managed_files raise RosettAi::AdoptError, 'No managed rule files found in rules directory' if files.empty? sensitive = sensitive_files api_files = filter_sensitive(files, sensitive) if local_only evaluate_local(files, sensitive) else evaluate_remote(files, api_files, sensitive) end end |
#filter_sensitive(files, sensitive_sources) ⇒ Array<String>
Exclude files marked as sensitive from the file list.
169 170 171 172 173 174 175 176 177 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 169 def filter_sensitive(files, sensitive_sources) return files if sensitive_sources.empty? sensitive_names = sensitive_sources.map { |f| RosettAi::TextSanitizer.normalize_nfc(File.basename(f, '.yml')) } files.reject do |file| name = File.basename(file, '.md').sub(/\A[^-]+-/, '') sensitive_names.include?(name) end end |
#redact(content) ⇒ String
Apply redaction patterns to content before API submission.
117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 117 def redact(content) patterns = load_redaction_patterns result = content.dup patterns.each do |entry| regex = Regexp.new(entry['pattern']) result.gsub!(regex, entry['replacement']) rescue RegexpError => e RosettAi.logger.warn("Invalid redaction pattern '#{entry['pattern']}': #{e.}") end result end |
#sensitive_files ⇒ Array<String>
List behaviour files marked as sensitive.
152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 152 def sensitive_files behaviour_dir = RosettAi.root.join('conf', 'behaviour') return [] unless behaviour_dir.exist? Dir.glob(behaviour_dir.join('*.yml')).select do |file| data = RosettAi::YamlLoader.load_file(file) data.is_a?(Hash) && data['sensitive'] == true rescue StandardError false end end |
#write_cache(checksum, result) ⇒ void
This method returns an undefined value.
Persist analysis results to the cache file.
93 94 95 96 97 98 99 100 101 |
# File 'lib/rosett_ai/adopter/rule_adopter.rb', line 93 def write_cache(checksum, result) FileUtils.mkdir_p(cache_path.dirname) data = { 'checksum' => checksum, 'analyzed_at' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'), 'result' => result } File.open(cache_path, 'w', 0o644) { |f| f.write(data.to_yaml) } end |