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 |
# 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_value do |info| cat = info[:category] categories[cat] ||= { 'schema' => "conf/schemas/#{cat}_schema.json", 'files' => {} } categories[cat]['files'][info[:name]] = info.to_lockfile_hash( display_path: RosettAi.paths.rules_display_path, source_path: relative_path(info[:source]), content_checksum: checksum(info[:content]), timestamp: ) 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 |