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.

<<~PROMPT.freeze
  Synthesize feedback from code review reports into unique findings.

  For each unique issue found:
  1. Track which reviewers identified it (by their model names)
  2. Merge file references from all sources
  3. Use the most comprehensive description
  4. Mark consensus=true if 3+ reviewers agree

  Return valid JSON with this schema:
  {
    "findings": [
      {
        "title": "Short title (max 60 chars)",
        "files": ["path/file.rb:10-20"],
        "reviewers": ["gemini-2.5-flash", "claude-3.5-sonnet"],
        "consensus": false,
        "priority": "high|medium|low|critical",
        "finding": "Description of the issue",
        "context": "Why this matters"
      }
    ]
  }

  IMPORTANT:
  - When only one report: extract all findings as-is with that reviewer
  - When multiple reports: deduplicate findings that describe the same issue
  - When multiple reviewers find the same issue, list ALL of them in reviewers array
  - Merge file arrays from all sources for each finding
  - Return ONLY the JSON, no markdown code fences
PROMPT
MAX_COMBINED_SIZE =

Maximum combined report size before truncation (characters)

200_000

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



92
93
94
95
96
97
98
99
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 92

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



78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 78

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



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ace/review/molecules/feedback_synthesizer.rb', line 64

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)

      if prompt_path && File.exist?(prompt_path)
        system_prompt_cache[prompt_name] = File.read(prompt_path)
      else
        system_prompt_cache[prompt_name] = 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