Class: Ace::Review::Molecules::PresetManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/review/molecules/preset_manager.rb

Overview

Manages loading and resolving review presets from configuration

Constant Summary collapse

COMPOSITION_METADATA_KEYS =

Metadata keys that are added during composition and should be stripped before use

%w[success composed composed_from].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_path: nil, project_root: nil) ⇒ PresetManager

Returns a new instance of PresetManager.



17
18
19
20
21
22
23
# File 'lib/ace/review/molecules/preset_manager.rb', line 17

def initialize(config_path: nil, project_root: nil)
  @project_root = project_root || find_project_root
  @config_path = resolve_config_path(config_path)
  @config = load_configuration
  @preset_cache = {}  # Final preset cache (after merging with defaults)
  @composition_cache = {}  # Intermediate composition cache (before defaults merge)
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



12
13
14
# File 'lib/ace/review/molecules/preset_manager.rb', line 12

def config
  @config
end

#config_pathObject (readonly)

Returns the value of attribute config_path.



12
13
14
# File 'lib/ace/review/molecules/preset_manager.rb', line 12

def config_path
  @config_path
end

#project_rootObject (readonly)

Returns the value of attribute project_root.



12
13
14
# File 'lib/ace/review/molecules/preset_manager.rb', line 12

def project_root
  @project_root
end

Instance Method Details

#available_presetsObject

Get list of available preset names



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ace/review/molecules/preset_manager.rb', line 53

def available_presets
  presets = []

  # Add presets from main config
  presets.concat(config_presets) if config

  # Add presets from preset directory
  presets.concat(file_presets)

  presets.uniq.sort
end

#default_contextObject

Get the default context from configuration



77
78
79
80
# File 'lib/ace/review/molecules/preset_manager.rb', line 77

def default_context
  config&.dig("defaults", "bundle") ||
    Ace::Review.get("defaults", "bundle")
end

#default_modelObject

Get the default model from configuration



71
72
73
74
# File 'lib/ace/review/molecules/preset_manager.rb', line 71

def default_model
  config&.dig("defaults", "model") ||
    Ace::Review.get("defaults", "model")
end

#default_output_formatObject

Get the default output format



83
84
85
86
87
# File 'lib/ace/review/molecules/preset_manager.rb', line 83

def default_output_format
  config&.dig("defaults", "output_format") ||
    Ace::Review.get("defaults", "output_format") ||
    "markdown"
end

#load_preset(preset_name) ⇒ Object

Load a specific preset by name Cached results are returned immediately to avoid redundant composition



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ace/review/molecules/preset_manager.rb', line 27

def load_preset(preset_name)
  return nil unless preset_name

  # Check cache first (composition can be expensive for deeply nested presets)
  return @preset_cache[preset_name] if @preset_cache.key?(preset_name)

  # Load with composition support
  result = load_preset_with_composition(preset_name)

  # Handle composition errors
  unless result && result["success"]
    # Log composition failure for debugging
    if result && result["error"]
      warn "Failed to compose preset '#{preset_name}': #{result["error"]}" if Ace::Review.debug?
    end
    return nil
  end

  # Extract preset data (remove composition metadata)
  preset = (result)

  # Merge with defaults and cache
  @preset_cache[preset_name] = merge_with_defaults(preset)
end

#load_preset_with_composition(name, visited = Set.new) ⇒ Object

Load a preset with composition support Returns fully composed preset data with all dependent presets merged Composition order: base presets first, then composing preset (last wins for scalars) Uses intermediate caching to avoid redundant composition of shared dependencies



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ace/review/molecules/preset_manager.rb', line 131

def load_preset_with_composition(name, visited = Set.new)
  start_time = Time.now if Ace::Review.debug?

  # Check circular dependency first (before cache to prevent caching incomplete compositions)
  validation = Atoms::PresetValidator.check_circular_dependency(name, visited.to_a)
  unless validation[:success]
    return {
      "error" => validation[:error],
      "success" => false
    }
  end

  # Check composition cache (enables intermediate caching for shared base presets)
  if @composition_cache.key?(name)
    warn "[COMPOSITION] Cache hit for '#{name}'" if Ace::Review.debug?
    return @composition_cache[name].dup
  end

  # Load preset from file or config
  preset = load_preset_from_file(name) || load_preset_from_config(name)
  unless preset
    return {
      "error" => "Preset '#{name}' not found. Available presets: #{available_presets.join(", ")}",
      "success" => false
    }
  end

  # Mark this preset as visited
  new_visited = visited.dup.add(name)

  # Extract preset references
  preset_refs = Atoms::PresetValidator.extract_preset_references(preset)

  # If no references, return preset as-is
  if preset_refs.empty?
    # Ensure consistent string keys
    result = deep_stringify_keys(preset)
    result["success"] = true
    return result
  end

  # Load all referenced presets recursively
  composed_presets = []
  errors = []

  preset_refs.each do |ref_name|
    composed = load_preset_with_composition(ref_name, new_visited)
    if composed["success"]
      composed_presets << composed
    else
      errors << composed["error"]
    end
  end

  # If there were errors loading dependencies, return error
  if errors.any?
    return {
      "error" => "Failed to load preset dependencies: #{errors.join(", ")}",
      "success" => false,
      "partial_presets" => composed_presets
    }
  end

  # Strip metadata from composed presets before merging
  clean_composed = composed_presets.map { |p| (p) }

  # Merge all composed presets with current preset
  # Order: dependencies first, then current preset (last wins for scalars)
  merged = merge_preset_data(clean_composed + [preset])

  # Ensure consistent string keys and add composition metadata
  merged = deep_stringify_keys(merged)
  merged["success"] = true
  merged["composed"] = true
  merged["composed_from"] = preset_refs + [name]

  # Cache the composed result for future reuse (enables intermediate caching)
  @composition_cache[name] = merged.dup

  # Log composition performance metrics in debug mode
  if Ace::Review.debug?
    elapsed = Time.now - start_time
    depth = visited.size + 1
    ref_count = preset_refs.size
    warn "[COMPOSITION] Composed '#{name}' in #{(elapsed * 1000).round(2)}ms (depth: #{depth}, refs: #{ref_count})"
  end

  merged
end

#preset_exists?(preset_name) ⇒ Boolean

Check if a preset exists

Returns:

  • (Boolean)


66
67
68
# File 'lib/ace/review/molecules/preset_manager.rb', line 66

def preset_exists?(preset_name)
  available_presets.include?(preset_name.to_s)
end

#resolve_preset(preset_name, overrides = {}) ⇒ Object

Resolve a preset configuration into actionable components



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ace/review/molecules/preset_manager.rb', line 90

def resolve_preset(preset_name, overrides = {})
  preset = load_preset(preset_name)
  return nil unless preset

  models_config = resolve_models_config(preset, overrides)

  # Support both bundle: and context: for context resolution
  preset_context = preset["bundle"] || preset["context"]

  {
    description: preset["description"],
    # Extract prompt composition for ace-bundle frontmatter (but let ace-bundle process it)
    system_prompt: preset["system_prompt"] || preset["prompt_composition"],
    # Preserve instructions field for section-based context generation
    instructions: preset["instructions"],
    context: resolve_context_config(preset_context, overrides[:context]),
    subject: resolve_subject_config(preset["subject"], overrides[:subject]),
    models: models_config,
    output_format: overrides[:output_format] || preset["output_format"] || default_output_format
  }
end

#review_base_pathObject

Get the base path for storing reviews



118
119
120
121
122
123
124
125
# File 'lib/ace/review/molecules/preset_manager.rb', line 118

def review_base_path
  # 1. Check for configured path first (user config only)
  configured_path = storage_config["base_path"]
  return expand_path_template(configured_path) if configured_path

  # 2. Fallback to cache directory
  File.join(project_root, ".ace-local/review/sessions")
end

#storage_configObject

Get storage configuration (user config only, no defaults)



113
114
115
# File 'lib/ace/review/molecules/preset_manager.rb', line 113

def storage_config
  config&.dig("storage") || {}
end