Class: RosettAi::Compiler::CompilationPipeline

Inherits:
Object
  • Object
show all
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

BehaviourCompiler

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

Instance Method Summary collapse

Constructor Details

#initialize(source_dir:, target_dir:, schema_dir:, backend:, scope: :global, additional_source_dirs: []) ⇒ CompilationPipeline

Returns a new instance of CompilationPipeline.

Parameters:

  • source_dir (String, Pathname)

    primary conf/ root (project scope when in project)

  • target_dir (String, Pathname)

    output directory

  • schema_dir (String, Pathname)

    JSON Schema directory

  • backend (RosettAi::Compiler::Backend)

    rendering backend

  • scope (Symbol) (defaults to: :global)

    compilation scope (:global or :project)

  • additional_source_dirs (Array<String, Pathname>) (defaults to: [])

    extra source directories (e.g. global conf/ when compiling in project scope). Files from additional sources are included unless overridden by a file with the same basename in the primary source_dir. Empty by default for backward compatibility.

  • source_dir (String, Pathname)

    primary conf/ root

  • target_dir (String, Pathname)

    output directory

  • schema_dir (String, Pathname)

    JSON Schema directory

  • backend (RosettAi::Compiler::Backend)

    rendering backend

  • scope (Symbol) (defaults to: :global)

    compilation scope (:global or :project)

  • additional_source_dirs (Array<String, Pathname>) (defaults to: [])

    extra source directories (e.g. global conf/ when compiling in project scope)



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_dirsObject (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

#backendObject (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_dirObject (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

#scopeObject (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_dirObject (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_dirObject (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.

Parameters:

  • content (String)

    content to hash

Returns:

  • (String)

    hex-encoded SHA-256 digest



155
156
157
# File 'lib/rosett_ai/compiler/compilation_pipeline.rb', line 155

def checksum(content)
  Digest::SHA256.hexdigest(content)
end

#compileHash{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.

Returns:



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_warningsArray<String>

Returns conflict warnings from the most recent compile run.

Returns:

  • (Array<String>)

    conflict warning messages



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.

Parameters:

  • existing_path (String)

    path to the existing file

  • new_content (String)

    proposed new content

Returns:

  • (String, nil)

    unified diff string, or nil if unchanged/missing



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_categoriesArray<String>

Discovers compilable config categories across all source directories.

Returns:

  • (Array<String>)

    category names with matching schemas



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.

Parameters:

  • compiled (Hash)

    compiled output map (filename => info)

Returns:

  • (Hash)

    lockfile structure suitable for YAML serialization



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)
  timestamp = 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' => timestamp,
      'rules_count' => info[:rules_count],
      'enabled_rules_count' => info[:enabled_rules_count]
    }
  end

  {
    'generated_at' => timestamp,
    '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.

Parameters:

  • path (String)

    file path to check

Returns:

  • (Boolean)


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.

Parameters:

  • compiled_names (Array<String>)

    basenames of compiled outputs

Returns:

  • (Array<String>)

    full paths of orphaned files



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_categoriesArray<String>

Returns project-only categories that were skipped in this scope.

Returns:

  • (Array<String>)

    skipped category names (empty for project 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.

Parameters:

  • category (String)

    configuration category name

  • data (Hash)

    parsed YAML data to validate

  • file (String)

    source file path (used in error messages)

Raises:



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?

  messages = errors.map do |err|
    path = err['data_pointer'].empty? ? 'root' : err['data_pointer']
    "#{path}: #{err['type']}"
  end
  raise RosettAi::CompileError, "Validation failed for #{file}: #{messages.join(', ')}"
end