Class: Ace::Review::Organisms::FeedbackManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/review/organisms/feedback_manager.rb

Overview

Central orchestrator for feedback item lifecycle management.

Coordinates the extraction, storage, querying, and state transitions of feedback items from code reviews. Works with atoms and molecules to provide a unified interface for feedback management.

With the feedback synthesis architecture, multiple review reports are synthesized into unique, deduplicated feedback items with reviewer arrays tracking which models found each issue.

Examples:

Extract and save feedback from review reports

manager = FeedbackManager.new
result = manager.extract_and_save(
  report_paths: ["review-report-gemini.md"],
  base_path: "/project"
)
result[:success] #=> true
result[:items_count] #=> 5

Multi-report synthesis (deduplicated with reviewer arrays)

result = manager.extract_and_save(
  report_paths: [
    "review-report-gemini.md",
    "review-report-claude.md",
    "review-report-gpt.md"
  ],
  base_path: "/project"
)
# Produces ~11 unique findings (not 33 duplicates)

Query feedback items

items = manager.list("/project", status: "pending")
item = manager.find("/project", "abc123")
stats = manager.stats("/project")

State transitions

manager.verify("/project", "abc123", valid: true)
manager.skip("/project", "abc123", reason: "Not applicable")
manager.resolve("/project", "abc123", resolution: "Fixed in commit abc")

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(synthesizer: nil, file_writer: nil, file_reader: nil, directory_manager: nil) ⇒ FeedbackManager

Returns a new instance of FeedbackManager.



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/ace/review/organisms/feedback_manager.rb', line 51

def initialize(
  synthesizer: nil,
  file_writer: nil,
  file_reader: nil,
  directory_manager: nil
)
  @synthesizer = synthesizer || Molecules::FeedbackSynthesizer.new
  @file_writer = file_writer || Molecules::FeedbackFileWriter.new
  @file_reader = file_reader || Molecules::FeedbackFileReader.new
  @directory_manager = directory_manager || Molecules::FeedbackDirectoryManager.new
end

Instance Attribute Details

#directory_managerObject (readonly)

Returns the value of attribute directory_manager.



49
50
51
# File 'lib/ace/review/organisms/feedback_manager.rb', line 49

def directory_manager
  @directory_manager
end

#file_readerObject (readonly)

Returns the value of attribute file_reader.



49
50
51
# File 'lib/ace/review/organisms/feedback_manager.rb', line 49

def file_reader
  @file_reader
end

#file_writerObject (readonly)

Returns the value of attribute file_writer.



49
50
51
# File 'lib/ace/review/organisms/feedback_manager.rb', line 49

def file_writer
  @file_writer
end

#synthesizerObject (readonly)

Returns the value of attribute synthesizer.



49
50
51
# File 'lib/ace/review/organisms/feedback_manager.rb', line 49

def synthesizer
  @synthesizer
end

Instance Method Details

#extract_and_save(report_paths:, base_path:, model: nil, session_dir: nil) ⇒ Hash

Extract and synthesize feedback items from review reports and save to disk

For multiple reports, uses FeedbackSynthesizer to produce deduplicated findings with reviewer arrays. For single reports, extracts directly.

Examples:

Single report

result = manager.extract_and_save(
  report_paths: ["session/review-report-gemini.md"],
  base_path: "/project"
)
result #=> { success: true, items_count: 3, paths: [...] }

Multi-report synthesis

result = manager.extract_and_save(
  report_paths: [
    "session/review-report-gemini.md",
    "session/review-report-claude.md"
  ],
  base_path: "/project"
)
# Produces deduplicated findings with reviewers arrays

Parameters:

  • report_paths (Array<String>)

    Paths to review report files

  • base_path (String)

    Base project path for feedback directory

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

    Model for synthesis/extraction (optional)

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

    Session directory for LLM output (optional)

Returns:

  • (Hash)

    Result with :success, :items_count, :paths, :metadata or :error



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ace/review/organisms/feedback_manager.rb', line 94

def extract_and_save(report_paths:, base_path:, model: nil, session_dir: nil)
  # Step 1: Synthesize feedback items from reports (handles deduplication)
  synthesis_result = @synthesizer.synthesize(
    report_paths: report_paths,
    session_dir: session_dir,
    model: model
  )

  unless synthesis_result[:success]
    return {success: false, error: synthesis_result[:error]}
  end

  items = synthesis_result[:items]
  return {success: true, items_count: 0, paths: [], metadata: synthesis_result[:metadata]} if items.empty?

  # Step 2: Ensure feedback directory exists
  feedback_dir = @directory_manager.ensure_directory(base_path)

  # Step 3: Save each item
  saved_paths = []
  errors = []

  items.each do |item|
    write_result = @file_writer.write(item, feedback_dir)

    if write_result[:success]
      saved_paths << write_result[:path]
    else
      errors << "Failed to save #{item.id}: #{write_result[:error]}"
    end
  end

  if errors.any? && saved_paths.empty?
    return {success: false, error: errors.join("; ")}
  end

  {
    success: true,
    items_count: saved_paths.length,
    paths: saved_paths,
    metadata: synthesis_result[:metadata],
    warnings: errors.any? ? errors : nil
  }.compact
end

#find(base_path, id) ⇒ Models::FeedbackItem?

Find a specific feedback item by ID

Examples:

item = manager.find("/project", "abc123")
item&.title #=> "Missing error handling"

Parameters:

  • base_path (String)

    Base project path

  • id (String)

    Feedback item ID (10-char Base36)

Returns:



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ace/review/organisms/feedback_manager.rb', line 183

def find(base_path, id)
  feedback_dir = @directory_manager.feedback_path(base_path)
  return nil unless Dir.exist?(feedback_dir)

  # Find file matching ID pattern
  files = Dir.glob(File.join(feedback_dir, "#{id}-*.s.md"))
  return nil if files.empty?

  # Read and return the item
  result = @file_reader.read(files.first)
  result[:success] ? result[:feedback_item] : nil
end

#list(base_path, status: nil, priority: nil) ⇒ Array<Models::FeedbackItem>

List feedback items with optional filters

Examples:

List all items

items = manager.list("/project")

Filter by status

pending_items = manager.list("/project", status: "pending")

Filter by status and priority

high_pending = manager.list("/project", status: "pending", priority: "high")

Parameters:

  • base_path (String)

    Base project path

  • status (String, nil) (defaults to: nil)

    Filter by status (draft, pending, invalid, skip, done)

  • priority (String, nil) (defaults to: nil)

    Filter by priority (critical, high, medium, low)

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ace/review/organisms/feedback_manager.rb', line 158

def list(base_path, status: nil, priority: nil)
  feedback_dir = @directory_manager.feedback_path(base_path)
  return [] unless Dir.exist?(feedback_dir)

  items = @file_reader.read_all(feedback_dir)

  # Apply status filter
  items = items.select { |item| item.status == status } if status

  # Apply priority filter (supports exact match "high" or range "high+")
  items = items.select { |item| Atoms::PriorityFilter.matches?(item.priority, priority) } if priority

  # Sort by ID (chronological since IDs are timestamp-based)
  items.sort_by(&:id)
end

#resolve(base_path, id, resolution:) ⇒ Hash

Resolve a feedback item (pending -> done)

Examples:

result = manager.resolve("/project", "abc123", resolution: "Added try-catch in commit abc")

Parameters:

  • base_path (String)

    Base project path

  • id (String)

    Feedback item ID

  • resolution (String)

    Description of how the issue was resolved

Returns:

  • (Hash)

    Result with :success, :item or :error



303
304
305
306
307
308
309
310
311
# File 'lib/ace/review/organisms/feedback_manager.rb', line 303

def resolve(base_path, id, resolution:)
  transition(
    base_path: base_path,
    id: id,
    to_status: "done",
    allowed_from: ["pending"],
    updates: {resolution: resolution}
  )
end

#skip(base_path, id, reason: nil) ⇒ Hash

Note:

For new code, prefer verify(base_path, id, skip: true, research: reason)

Skip a feedback item (draft/pending -> skip)

Examples:

result = manager.skip("/project", "abc123", reason: "Out of scope for this PR")

Parameters:

  • base_path (String)

    Base project path

  • id (String)

    Feedback item ID

  • reason (String, nil) (defaults to: nil)

    Reason for skipping (optional, aliased to research)

Returns:

  • (Hash)

    Result with :success, :item or :error



290
291
292
# File 'lib/ace/review/organisms/feedback_manager.rb', line 290

def skip(base_path, id, reason: nil)
  verify(base_path, id, skip: true, research: reason)
end

#stats(base_path) ⇒ Hash

Get statistics about feedback items

Examples:

stats = manager.stats("/project")
stats #=> { draft: 2, pending: 3, invalid: 1, skip: 0, done: 5, total: 11 }

Parameters:

  • base_path (String)

    Base project path

Returns:

  • (Hash)

    Statistics with status counts



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ace/review/organisms/feedback_manager.rb', line 204

def stats(base_path)
  # Get items from active directory
  active_items = list(base_path)

  # Get archived items
  archive_dir = @directory_manager.archive_path(base_path)
  archived_items = []
  if Dir.exist?(archive_dir)
    archived_items = @file_reader.read_all(archive_dir)
  end

  all_items = active_items + archived_items

  # Count by status
  counts = Models::FeedbackItem::VALID_STATUSES.map do |status|
    [status.to_sym, all_items.count { |item| item.status == status }]
  end.to_h

  counts[:total] = all_items.length
  counts
end

#verify(base_path, id, valid: nil, skip: nil, research: nil) ⇒ Hash

Verify a feedback item (draft -> pending, draft -> invalid, or draft/pending -> skip)

Examples:

Mark as valid

result = manager.verify("/project", "abc123", valid: true, research: "Confirmed issue")

Mark as invalid

result = manager.verify("/project", "abc123", valid: false, research: "False positive")

Skip

result = manager.verify("/project", "abc123", skip: true, research: "Design decision")

Parameters:

  • base_path (String)

    Base project path

  • id (String)

    Feedback item ID

  • valid (Boolean, nil) (defaults to: nil)

    Whether the feedback is valid (mutually exclusive with skip:)

  • skip (Boolean, nil) (defaults to: nil)

    Whether to skip the feedback (mutually exclusive with valid:)

  • research (String, nil) (defaults to: nil)

    Verification research notes (optional)

Returns:

  • (Hash)

    Result with :success, :item or :error



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/ace/review/organisms/feedback_manager.rb', line 247

def verify(base_path, id, valid: nil, skip: nil, research: nil)
  # Validate mutually exclusive options
  if valid.nil? && skip.nil?
    return {success: false, error: "Must specify either valid: or skip:"}
  end
  if !valid.nil? && !skip.nil?
    return {success: false, error: "Cannot specify both valid: and skip:"}
  end

  target_status = if skip
    "skip"
  elsif valid
    "pending"
  else
    "invalid"
  end

  allowed_from = if skip
    %w[draft pending]
  else
    ["draft"]
  end

  transition(
    base_path: base_path,
    id: id,
    to_status: target_status,
    allowed_from: allowed_from,
    updates: research ? {research: research} : {}
  )
end