Class: Ace::Review::Molecules::FeedbackSynthesizer

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

Overview

Synthesizes feedback items from multiple review reports.

Reads ALL reports in a single LLM pass and outputs deduplicated FeedbackItems with reviewer arrays tracking which models found each issue.

Examples:

Basic usage

synthesizer = FeedbackSynthesizer.new
result = synthesizer.synthesize(
  report_paths: [
    "session/review-report-gemini-2.5-flash.md",
    "session/review-report-claude-3.5-sonnet.md",
    "session/review-report-gpt-4.md"
  ],
  session_dir: "session"
)
result[:success] #=> true
result[:items]   #=> [FeedbackItem, ...] (with reviewers arrays)

Constant Summary collapse

FALLBACK_SYSTEM_PROMPT =

Cached system prompt and prompt-path lookups to avoid repeated shell/file reads during test and command-heavy runs.

"Synthesize feedback from code review reports into unique findings.\n\nFor each unique issue found:\n1. Track which reviewers identified it (by their model names)\n2. Merge file references from all sources\n3. Use the most comprehensive description\n4. Mark consensus=true if 3+ reviewers agree\n\nReturn valid JSON with this schema:\n{\n  \"findings\": [\n    {\n      \"title\": \"Short title (max 60 chars)\",\n      \"files\": [\"path/file.rb:10-20\"],\n      \"reviewers\": [\"gemini-2.5-flash\", \"claude-3.5-sonnet\"],\n      \"consensus\": false,\n      \"priority\": \"high|medium|low|critical\",\n      \"finding\": \"Description of the issue\",\n      \"context\": \"Why this matters\"\n    }\n  ]\n}\n\nIMPORTANT:\n- When only one report: extract all findings as-is with that reviewer\n- When multiple reports: deduplicate findings that describe the same issue\n- When multiple reviewers find the same issue, list ALL of them in reviewers array\n- Preserve every distinct finding, including ones reported by only one reviewer\n- Consensus is metadata, never a reason to omit a finding\n- Merge file arrays from all sources for each finding\n- Return ONLY the JSON, no markdown code fences\n"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(llm_executor: nil) ⇒ FeedbackSynthesizer

Returns a new instance of FeedbackSynthesizer.



125
126
127
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 125

def initialize(llm_executor: nil)
  @llm_executor = llm_executor || LlmExecutor.new
end

Instance Attribute Details

#llm_executor ⇒ Object (readonly)

Returns the value of attribute llm_executor.



123
124
125
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 123

def llm_executor
  @llm_executor
end

Class Method Details

.clear_prompt_cache! ⇒ Object



95
96
97
98
99
100
101
102
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 95

def clear_prompt_cache!
  system_prompt_cache_mutex.synchronize do
    @system_prompt_cache = {}
  end
  prompt_path_cache_mutex.synchronize do
    @prompt_path_cache = {}
  end
end

.resolve_prompt_path_cached(prompt_name) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 81

def resolve_prompt_path_cached(prompt_name)
  prompt_path_cache[prompt_name] ||= begin
    nav_result = begin
      `ace-nav prompt://#{prompt_name} 2>/dev/null`.strip
    rescue
      ""
    end

    return nav_result unless nav_result.empty?

    File.join(__dir__, "../../../../handbook/prompts", prompt_name)
  end
end

.system_prompt(prompt_name) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 67

def system_prompt(prompt_name)
  system_prompt_cache_mutex.synchronize do
    system_prompt_cache.fetch(prompt_name) do
      prompt_path = resolve_prompt_path_cached(prompt_name)

      system_prompt_cache[prompt_name] = if prompt_path && File.exist?(prompt_path)
        File.read(prompt_path)
      else
        FALLBACK_SYSTEM_PROMPT
      end
    end
  end
end

Instance Method Details

#synthesize(report_paths:, session_dir: nil, model: nil) ⇒ Hash

Synthesize feedback items from review reports

Unified process that handles single or multiple reports:

  • Single report: extracts findings directly
  • Multiple reports: deduplicates and tracks consensus across reviewers

Parameters:

  • report_paths (Array<String>) —

    Paths to review report files

  • session_dir (String, nil) (defaults to: nil) —

    Session directory for LLM output files

  • model (String, nil) (defaults to: nil) —

    Model to use for synthesis (default: config setting)

Returns:

  • (Hash) —

    Result with :success, :items, :metadata or :error



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 139

def synthesize(report_paths:, session_dir: nil, model: nil)
  # Validate inputs
  return error_result("No report paths provided") if report_paths.nil? || report_paths.empty?

  # Create session dir if needed
  session_dir ||= create_temp_session_dir
  FileUtils.mkdir_p(session_dir) unless Dir.exist?(session_dir)

  # Read all reports
  reports = read_reports(report_paths)
  return error_result("No valid reports found") if reports.empty?

  # Synthesize (handles both single and multiple reports uniformly)
  synthesize_reports(reports, session_dir, model)
rescue => e
  error_result("Synthesis failed: #{e.message}", backtrace: e.backtrace.first(5))
end