Class: RosettAi::Compiler::CompilationPipeline
- Inherits:
-
Object
- Object
- RosettAi::Compiler::CompilationPipeline
- Defined in:
- lib/rosett_ai/compiler/compilation_pipeline.rb
Overview
Core compilation pipeline that transforms YAML configuration files into target-specific output formats.
Delegates rendering to a pluggable Backend instance. Owns discovery, validation, checksumming, diffing, lockfile generation, and orphan management.
Direct Known Subclasses
Constant Summary collapse
- NON_COMPILABLE_CATEGORIES =
Categories excluded from compilation. These directories contain configuration files validated by their respective modules (e.g., Policy::DenyList, Build::Package) rather than the compile pipeline.
['packaging', 'policy', 'tooling'].freeze
- PROJECT_ONLY_CATEGORIES =
Categories compiled only in project scope. Design documents are project specifications, not operational rules — compiling them globally would pollute ~/.claude/rules/ with empty files.
['design'].freeze
- VALID_SCOPES =
[:global, :project].freeze
Instance Attribute Summary collapse
-
#additional_source_dirs ⇒ Object
readonly
Returns the value of attribute additional_source_dirs.
-
#backend ⇒ Object
readonly
Returns the value of attribute backend.
-
#schema_dir ⇒ Object
readonly
Returns the value of attribute schema_dir.
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
-
#source_dir ⇒ Object
readonly
Returns the value of attribute source_dir.
-
#target_dir ⇒ Object
readonly
Returns the value of attribute target_dir.
Instance Method Summary collapse
-
#checksum(content) ⇒ String
Computes a SHA-256 hex digest of the given content.
-
#compile ⇒ Hash{String => CompiledOutput}
Compiles all discovered categories into rendered output.
-
#conflict_warnings ⇒ Array<String>
Returns conflict warnings from the most recent compile run.
-
#diff(existing_path, new_content) ⇒ String?
Generates a unified diff between an existing file and new content.
-
#discover_categories ⇒ Array<String>
Discovers compilable config categories across all source directories.
-
#initialize(source_dir:, target_dir:, schema_dir:, backend:, scope: :global, additional_source_dirs: []) ⇒ CompilationPipeline
constructor
A new instance of CompilationPipeline.
-
#lockfile_data(compiled) ⇒ Hash
Builds a lockfile hash from compiled outputs.
-
#managed_file?(path) ⇒ Boolean
Checks whether the file at path was generated by the compiler.
-
#orphaned_files(compiled_names) ⇒ Array<String>
Finds managed target files not present in the compiled set.
-
#skipped_project_categories ⇒ Array<String>
Returns project-only categories that were skipped in this scope.
-
#validate!(category, data, file)
Validates data against the JSON Schema for the given category.
Constructor Details
#initialize(source_dir:, target_dir:, schema_dir:, backend:, scope: :global, additional_source_dirs: []) ⇒ CompilationPipeline
Returns a new instance of CompilationPipeline.
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 52 def initialize(source_dir:, target_dir:, schema_dir:, backend:, # rubocop:disable Metrics/ParameterLists scope: :global, additional_source_dirs: []) validate_scope!(scope) @source_dir = Pathname.new(source_dir) @target_dir = Pathname.new(target_dir) @schema_dir = Pathname.new(schema_dir) @backend = backend @scope = scope @additional_source_dirs = additional_source_dirs.map { |d| Pathname.new(d) } end |
Instance Attribute Details
#additional_source_dirs ⇒ Object (readonly)
Returns the value of attribute additional_source_dirs.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def additional_source_dirs @additional_source_dirs end |
#backend ⇒ Object (readonly)
Returns the value of attribute backend.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def backend @backend end |
#schema_dir ⇒ Object (readonly)
Returns the value of attribute schema_dir.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def schema_dir @schema_dir end |
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def scope @scope end |
#source_dir ⇒ Object (readonly)
Returns the value of attribute source_dir.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def source_dir @source_dir end |
#target_dir ⇒ Object (readonly)
Returns the value of attribute target_dir.
33 34 35 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 33 def target_dir @target_dir end |
Instance Method Details
#checksum(content) ⇒ String
Computes a SHA-256 hex digest of the given content.
155 156 157 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 155 def checksum(content) Digest::SHA256.hexdigest(content) end |
#compile ⇒ Hash{String => CompiledOutput}
Compiles all discovered categories into rendered output. When additional_source_dirs are present, files are collected from all sources. The primary source_dir takes precedence: a file with the same basename in source_dir overrides one from additional sources.
After compilation, runs duplicate rule ID detection across all behaviour files. Access warnings via #conflict_warnings.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 103 def compile results = {} all_category_files = {} discover_categories.each do |category| yaml_files = collect_category_files(category) yaml_files.concat(premium_files_for(category)) all_category_files[category] = yaml_files yaml_files.each do |file| data = load_yaml(file) next unless data validate!(category, data, file) key, output = compile_file(category, data, file) results[key] = output end end @conflict_warnings = detect_duplicate_rule_ids(all_category_files) results end |
#conflict_warnings ⇒ Array<String>
Returns conflict warnings from the most recent compile run.
126 127 128 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 126 def conflict_warnings @conflict_warnings || [] end |
#diff(existing_path, new_content) ⇒ String?
Generates a unified diff between an existing file and new content.
184 185 186 187 188 189 190 191 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 184 def diff(existing_path, new_content) return nil unless File.exist?(existing_path) existing = File.read(existing_path, encoding: 'utf-8') return nil if existing == new_content generate_unified_diff(existing_path, existing, new_content) end |
#discover_categories ⇒ Array<String>
Discovers compilable config categories across all source directories.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 73 def discover_categories skip = ['schemas', 'targets'] + NON_COMPILABLE_CATEGORIES skip += PROJECT_ONLY_CATEGORIES if scope == :global categories = Set.new all_source_dirs.each do |sdir| next unless sdir.exist? Dir.children(sdir).each do |entry| next unless sdir.join(entry).directory? next if skip.include?(entry) categories << entry end end categories.select do |category| schema_dir.join("#{category}_schema.json").exist? end.sort end |
#lockfile_data(compiled) ⇒ Hash
Builds a lockfile hash from compiled outputs.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 197 def lockfile_data(compiled) = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ') categories = {} compiled.each do |filename, info| cat = info[:category] categories[cat] ||= { 'schema' => "conf/schemas/#{cat}_schema.json", 'files' => {} } categories[cat]['files'][info[:name]] = { 'version' => info[:version], 'source' => relative_path(info[:source]), 'output' => "#{RosettAi.paths.rules_display_path}/#{filename}", 'checksum' => "sha256:#{checksum(info[:content])}", 'compiled_at' => , 'rules_count' => info[:rules_count], 'enabled_rules_count' => info[:enabled_rules_count] } end { 'generated_at' => , 'generator' => 'rosett-ai', 'generator_version' => RosettAi::VERSION, 'target_directory' => RosettAi.paths.rules_display_path, 'categories' => categories } end |
#managed_file?(path) ⇒ Boolean
Checks whether the file at path was generated by the compiler.
163 164 165 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 163 def managed_file?(path) backend.managed_file?(path) end |
#orphaned_files(compiled_names) ⇒ Array<String>
Finds managed target files not present in the compiled set.
171 172 173 174 175 176 177 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 171 def orphaned_files(compiled_names) return [] unless target_dir.exist? Dir.glob(target_dir.join("*#{backend.file_extension}")).select do |file| managed_file?(file) && !compiled_names.include?(File.basename(file)) end end |
#skipped_project_categories ⇒ Array<String>
Returns project-only categories that were skipped in this scope.
66 67 68 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 66 def skipped_project_categories scope == :global ? PROJECT_ONLY_CATEGORIES : [] end |
#validate!(category, data, file)
This method returns an undefined value.
Validates data against the JSON Schema for the given category.
137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 137 def validate!(category, data, file) schema_file = schema_dir.join("#{category}_schema.json") schema = JSON.parse(schema_file.read) schemer = JSONSchemer.schema(schema) errors = schemer.validate(data).to_a return if errors.empty? = errors.map do |err| path = err['data_pointer'].empty? ? 'root' : err['data_pointer'] "#{path}: #{err['type']}" end raise RosettAi::CompileError, "Validation failed for #{file}: #{.join(', ')}" end |