ace-review 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-review.yml +10 -0
- data/.ace-defaults/nav/protocols/prompt-sources/ace-review.yml +36 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-review.yml +10 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-review.yml +19 -0
- data/.ace-defaults/review/config.yml +79 -0
- data/.ace-defaults/review/presets/code-fit.yml +64 -0
- data/.ace-defaults/review/presets/code-shine.yml +44 -0
- data/.ace-defaults/review/presets/code-valid.yml +39 -0
- data/.ace-defaults/review/presets/docs.yml +42 -0
- data/.ace-defaults/review/presets/spec.yml +37 -0
- data/CHANGELOG.md +1780 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +14 -0
- data/exe/ace-review +27 -0
- data/exe/ace-review-feedback +17 -0
- data/handbook/guides/code-review-process.g.md +234 -0
- data/handbook/prompts/base/sections.md +23 -0
- data/handbook/prompts/base/system.md +60 -0
- data/handbook/prompts/focus/architecture/atom.md +30 -0
- data/handbook/prompts/focus/architecture/reflection.md +60 -0
- data/handbook/prompts/focus/frameworks/rails.md +40 -0
- data/handbook/prompts/focus/frameworks/vue-firebase.md +45 -0
- data/handbook/prompts/focus/languages/ruby.md +50 -0
- data/handbook/prompts/focus/phase/correctness.md +51 -0
- data/handbook/prompts/focus/phase/polish.md +43 -0
- data/handbook/prompts/focus/phase/quality.md +42 -0
- data/handbook/prompts/focus/quality/performance.md +48 -0
- data/handbook/prompts/focus/quality/security.md +47 -0
- data/handbook/prompts/focus/scope/docs.md +38 -0
- data/handbook/prompts/focus/scope/spec.md +58 -0
- data/handbook/prompts/focus/scope/tests.md +36 -0
- data/handbook/prompts/format/compact.md +12 -0
- data/handbook/prompts/format/detailed.md +39 -0
- data/handbook/prompts/format/standard.md +16 -0
- data/handbook/prompts/guidelines/icons.md +19 -0
- data/handbook/prompts/guidelines/tone.md +21 -0
- data/handbook/prompts/synthesis-review-reports.system.md +318 -0
- data/handbook/prompts/synthesize-feedback.system.md +147 -0
- data/handbook/skills/as-review-apply-feedback/SKILL.md +39 -0
- data/handbook/skills/as-review-package/SKILL.md +36 -0
- data/handbook/skills/as-review-pr/SKILL.md +38 -0
- data/handbook/skills/as-review-run/SKILL.md +30 -0
- data/handbook/skills/as-review-verify-feedback/SKILL.md +31 -0
- data/handbook/templates/review-tasks/task-review-summary.template.md +148 -0
- data/handbook/workflow-instructions/review/apply-feedback.wf.md +212 -0
- data/handbook/workflow-instructions/review/package.wf.md +16 -0
- data/handbook/workflow-instructions/review/pr.wf.md +284 -0
- data/handbook/workflow-instructions/review/run.wf.md +262 -0
- data/handbook/workflow-instructions/review/verify-feedback.wf.md +286 -0
- data/lib/ace/review/atoms/context_limit_resolver.rb +162 -0
- data/lib/ace/review/atoms/diff_boundary_finder.rb +133 -0
- data/lib/ace/review/atoms/feedback_id_generator.rb +66 -0
- data/lib/ace/review/atoms/feedback_slug_generator.rb +61 -0
- data/lib/ace/review/atoms/feedback_state_validator.rb +98 -0
- data/lib/ace/review/atoms/pr_comment_formatter.rb +325 -0
- data/lib/ace/review/atoms/preset_validator.rb +103 -0
- data/lib/ace/review/atoms/priority_filter.rb +115 -0
- data/lib/ace/review/atoms/retry_with_backoff.rb +75 -0
- data/lib/ace/review/atoms/slug_generator.rb +50 -0
- data/lib/ace/review/atoms/token_estimator.rb +86 -0
- data/lib/ace/review/cli/commands/feedback/create.rb +173 -0
- data/lib/ace/review/cli/commands/feedback/list.rb +280 -0
- data/lib/ace/review/cli/commands/feedback/resolve.rb +109 -0
- data/lib/ace/review/cli/commands/feedback/session_discovery.rb +70 -0
- data/lib/ace/review/cli/commands/feedback/show.rb +177 -0
- data/lib/ace/review/cli/commands/feedback/skip.rb +125 -0
- data/lib/ace/review/cli/commands/feedback/verify.rb +149 -0
- data/lib/ace/review/cli/commands/feedback.rb +79 -0
- data/lib/ace/review/cli/commands/review.rb +378 -0
- data/lib/ace/review/cli/feedback_cli.rb +71 -0
- data/lib/ace/review/cli.rb +103 -0
- data/lib/ace/review/errors.rb +146 -0
- data/lib/ace/review/models/feedback_item.rb +216 -0
- data/lib/ace/review/models/review_options.rb +208 -0
- data/lib/ace/review/models/reviewer.rb +181 -0
- data/lib/ace/review/molecules/context_composer.rb +123 -0
- data/lib/ace/review/molecules/context_extractor.rb +159 -0
- data/lib/ace/review/molecules/feedback_directory_manager.rb +183 -0
- data/lib/ace/review/molecules/feedback_file_reader.rb +178 -0
- data/lib/ace/review/molecules/feedback_file_writer.rb +210 -0
- data/lib/ace/review/molecules/feedback_synthesizer.rb +588 -0
- data/lib/ace/review/molecules/gh_cli_executor.rb +124 -0
- data/lib/ace/review/molecules/gh_comment_poster.rb +205 -0
- data/lib/ace/review/molecules/gh_comment_resolver.rb +199 -0
- data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +408 -0
- data/lib/ace/review/molecules/gh_pr_fetcher.rb +240 -0
- data/lib/ace/review/molecules/llm_executor.rb +142 -0
- data/lib/ace/review/molecules/multi_model_executor.rb +278 -0
- data/lib/ace/review/molecules/nav_prompt_resolver.rb +145 -0
- data/lib/ace/review/molecules/pr_task_spec_resolver.rb +58 -0
- data/lib/ace/review/molecules/preset_manager.rb +494 -0
- data/lib/ace/review/molecules/prompt_composer.rb +76 -0
- data/lib/ace/review/molecules/prompt_resolver.rb +168 -0
- data/lib/ace/review/molecules/strategies/adaptive_strategy.rb +193 -0
- data/lib/ace/review/molecules/strategies/chunked_strategy.rb +459 -0
- data/lib/ace/review/molecules/strategies/full_strategy.rb +114 -0
- data/lib/ace/review/molecules/subject_extractor.rb +315 -0
- data/lib/ace/review/molecules/subject_filter.rb +199 -0
- data/lib/ace/review/molecules/subject_strategy.rb +96 -0
- data/lib/ace/review/molecules/task_report_saver.rb +161 -0
- data/lib/ace/review/molecules/task_resolver.rb +48 -0
- data/lib/ace/review/organisms/feedback_manager.rb +386 -0
- data/lib/ace/review/organisms/review_manager.rb +1059 -0
- data/lib/ace/review/version.rb +7 -0
- data/lib/ace/review.rb +135 -0
- metadata +351 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Review
|
|
7
|
+
module Models
|
|
8
|
+
# Represents a single feedback item from a code review.
|
|
9
|
+
#
|
|
10
|
+
# FeedbackItem is an immutable data structure containing all information
|
|
11
|
+
# about a review finding, including its status, priority, and resolution.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a feedback item (multiple reviewers with consensus)
|
|
14
|
+
# item = FeedbackItem.new(
|
|
15
|
+
# id: "8o7abcd123",
|
|
16
|
+
# title: "Missing error handling",
|
|
17
|
+
# files: ["src/handlers/user.rb:42-55"],
|
|
18
|
+
# reviewers: ["google:gemini-2.5-flash", "anthropic:claude-3.5-sonnet", "openai:gpt-4"],
|
|
19
|
+
# status: "pending",
|
|
20
|
+
# priority: "high",
|
|
21
|
+
# consensus: true,
|
|
22
|
+
# finding: "The error handling is incomplete..."
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Creating a modified copy
|
|
26
|
+
# resolved_item = item.dup_with(status: "done", resolution: "Added try-catch block")
|
|
27
|
+
#
|
|
28
|
+
class FeedbackItem
|
|
29
|
+
# Valid status values for feedback items
|
|
30
|
+
VALID_STATUSES = %w[draft pending invalid skip done].freeze
|
|
31
|
+
|
|
32
|
+
# Valid priority values for feedback items
|
|
33
|
+
VALID_PRIORITIES = %w[critical high medium low].freeze
|
|
34
|
+
|
|
35
|
+
# Minimum number of reviewers for consensus
|
|
36
|
+
CONSENSUS_THRESHOLD = 3
|
|
37
|
+
|
|
38
|
+
attr_reader :id, :title, :files, :reviewers, :status, :priority,
|
|
39
|
+
:created, :updated, :finding, :context, :research, :resolution, :consensus
|
|
40
|
+
|
|
41
|
+
# Initialize a new FeedbackItem from a hash of attributes
|
|
42
|
+
#
|
|
43
|
+
# @param attrs [Hash] Attributes for the feedback item
|
|
44
|
+
# @option attrs [String] :id 10-character Base36 ID (6-char timestamp + 4-char random)
|
|
45
|
+
# @option attrs [String] :title Short description of the finding
|
|
46
|
+
# @option attrs [Array<String>] :files File references (path:line-range format)
|
|
47
|
+
# @option attrs [Array<String>] :reviewers LLM models that found this
|
|
48
|
+
# @option attrs [String] :reviewer Single reviewer string (converted to reviewers array)
|
|
49
|
+
# @option attrs [String] :status One of: draft, pending, invalid, skip, done
|
|
50
|
+
# @option attrs [String] :priority One of: critical, high, medium, low
|
|
51
|
+
# @option attrs [Boolean] :consensus True if 3+ models agree on this finding
|
|
52
|
+
# @option attrs [String] :created ISO8601 timestamp
|
|
53
|
+
# @option attrs [String] :updated ISO8601 timestamp
|
|
54
|
+
# @option attrs [String] :finding Original finding text
|
|
55
|
+
# @option attrs [String, nil] :context Additional context (optional)
|
|
56
|
+
# @option attrs [String, nil] :research Verification research (optional)
|
|
57
|
+
# @option attrs [String, nil] :resolution How it was resolved (optional)
|
|
58
|
+
# @raise [ArgumentError] If status or priority values are invalid
|
|
59
|
+
def initialize(attrs = {})
|
|
60
|
+
# Support both symbol and string keys
|
|
61
|
+
attrs = symbolize_keys(attrs)
|
|
62
|
+
|
|
63
|
+
@id = attrs[:id]
|
|
64
|
+
@title = attrs[:title]
|
|
65
|
+
@files = Array(attrs[:files])
|
|
66
|
+
|
|
67
|
+
@reviewers = normalize_reviewers(attrs[:reviewers], attrs[:reviewer])
|
|
68
|
+
|
|
69
|
+
@status = attrs[:status] || "draft"
|
|
70
|
+
@priority = attrs[:priority] || "medium"
|
|
71
|
+
@created = attrs[:created] || Time.now.utc.iso8601
|
|
72
|
+
@updated = attrs[:updated] || @created
|
|
73
|
+
@finding = attrs[:finding]
|
|
74
|
+
@context = attrs[:context]
|
|
75
|
+
@research = attrs[:research]
|
|
76
|
+
@resolution = attrs[:resolution]
|
|
77
|
+
|
|
78
|
+
# Consensus: true if 3+ models agree (can be explicitly set or computed)
|
|
79
|
+
@consensus = attrs.key?(:consensus) ? attrs[:consensus] : (@reviewers.length >= CONSENSUS_THRESHOLD)
|
|
80
|
+
|
|
81
|
+
validate!
|
|
82
|
+
freeze_arrays
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Accessor for single reviewer (returns first reviewer)
|
|
86
|
+
# @return [String, nil] First reviewer or nil if none
|
|
87
|
+
def reviewer
|
|
88
|
+
@reviewers.first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Convert the feedback item to a hash
|
|
92
|
+
#
|
|
93
|
+
# @return [Hash] Hash representation with string keys
|
|
94
|
+
def to_h
|
|
95
|
+
hash = {
|
|
96
|
+
"id" => id,
|
|
97
|
+
"title" => title,
|
|
98
|
+
"files" => files.dup,
|
|
99
|
+
"status" => status,
|
|
100
|
+
"priority" => priority,
|
|
101
|
+
"created" => created,
|
|
102
|
+
"updated" => updated,
|
|
103
|
+
"finding" => finding,
|
|
104
|
+
"context" => context,
|
|
105
|
+
"research" => research,
|
|
106
|
+
"resolution" => resolution
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Use reviewers array if multiple reviewers, otherwise use singular reviewer key
|
|
110
|
+
if @reviewers.length > 1
|
|
111
|
+
hash["reviewers"] = @reviewers.dup
|
|
112
|
+
hash["consensus"] = consensus if consensus
|
|
113
|
+
else
|
|
114
|
+
hash["reviewer"] = reviewer
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
hash.compact
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convert the feedback item to YAML
|
|
121
|
+
#
|
|
122
|
+
# @return [String] YAML representation
|
|
123
|
+
def to_yaml
|
|
124
|
+
to_h.to_yaml
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Create a new FeedbackItem with modified attributes
|
|
128
|
+
#
|
|
129
|
+
# @param changes [Hash] Attributes to change
|
|
130
|
+
# @return [FeedbackItem] New instance with merged attributes
|
|
131
|
+
def dup_with(**changes)
|
|
132
|
+
# Auto-update the updated timestamp when making changes
|
|
133
|
+
changes[:updated] ||= Time.now.utc.iso8601
|
|
134
|
+
FeedbackItem.new(to_h.merge(stringify_keys(changes)))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check if two feedback items are equal
|
|
138
|
+
#
|
|
139
|
+
# @param other [FeedbackItem] Other item to compare
|
|
140
|
+
# @return [Boolean] True if equal
|
|
141
|
+
def ==(other)
|
|
142
|
+
return false unless other.is_a?(FeedbackItem)
|
|
143
|
+
|
|
144
|
+
id == other.id &&
|
|
145
|
+
title == other.title &&
|
|
146
|
+
files == other.files &&
|
|
147
|
+
reviewers == other.reviewers &&
|
|
148
|
+
status == other.status &&
|
|
149
|
+
priority == other.priority &&
|
|
150
|
+
finding == other.finding
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
alias_method :eql?, :==
|
|
154
|
+
|
|
155
|
+
# Hash code for use in hash tables
|
|
156
|
+
#
|
|
157
|
+
# @return [Integer] Hash code
|
|
158
|
+
def hash
|
|
159
|
+
[id, title, files, reviewers, status, priority, finding].hash
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# Validate status and priority values
|
|
165
|
+
#
|
|
166
|
+
# @raise [ArgumentError] If values are invalid
|
|
167
|
+
def validate!
|
|
168
|
+
unless VALID_STATUSES.include?(status)
|
|
169
|
+
raise ArgumentError, "Invalid status '#{status}'. Must be one of: #{VALID_STATUSES.join(", ")}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
unless VALID_PRIORITIES.include?(priority)
|
|
173
|
+
raise ArgumentError, "Invalid priority '#{priority}'. Must be one of: #{VALID_PRIORITIES.join(", ")}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Freeze array attributes to ensure immutability
|
|
178
|
+
def freeze_arrays
|
|
179
|
+
@files.freeze
|
|
180
|
+
@reviewers.freeze
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Normalize reviewers from various input formats
|
|
184
|
+
#
|
|
185
|
+
# @param reviewers [Array<String>, nil] Reviewers array
|
|
186
|
+
# @param reviewer [String, nil] Single reviewer string
|
|
187
|
+
# @return [Array<String>] Normalized reviewers array
|
|
188
|
+
def normalize_reviewers(reviewers, reviewer)
|
|
189
|
+
if reviewers.is_a?(Array) && reviewers.any?
|
|
190
|
+
reviewers.map(&:to_s).reject(&:empty?)
|
|
191
|
+
elsif reviewer && !reviewer.to_s.empty?
|
|
192
|
+
[reviewer.to_s]
|
|
193
|
+
else
|
|
194
|
+
[]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Convert hash keys to symbols
|
|
199
|
+
#
|
|
200
|
+
# @param hash [Hash] Hash with string or symbol keys
|
|
201
|
+
# @return [Hash] Hash with symbol keys
|
|
202
|
+
def symbolize_keys(hash)
|
|
203
|
+
hash.transform_keys(&:to_sym)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Convert hash keys to strings
|
|
207
|
+
#
|
|
208
|
+
# @param hash [Hash] Hash with string or symbol keys
|
|
209
|
+
# @return [Hash] Hash with string keys
|
|
210
|
+
def stringify_keys(hash)
|
|
211
|
+
hash.transform_keys(&:to_s)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Review
|
|
5
|
+
module Models
|
|
6
|
+
# Options for code review execution
|
|
7
|
+
class ReviewOptions
|
|
8
|
+
attr_accessor :preset, :output_dir, :output, :context, :subject,
|
|
9
|
+
:prompt_base, :prompt_format, :prompt_focus, :add_focus,
|
|
10
|
+
:prompt_guidelines, :model, :models, :dry_run, :verbose,
|
|
11
|
+
:auto_execute, :save_session, :session_dir,
|
|
12
|
+
:pr, :post_comment, :pr_metadata, :gh_timeout,
|
|
13
|
+
:pr_comments, :pr_comment_data,
|
|
14
|
+
:no_feedback, :feedback_model,
|
|
15
|
+
:list_presets, :list_prompts, :help
|
|
16
|
+
|
|
17
|
+
def initialize(hash = {})
|
|
18
|
+
# Core options
|
|
19
|
+
@preset = hash[:preset] || Ace::Review.get("defaults", "preset")
|
|
20
|
+
@output_dir = hash[:output_dir]
|
|
21
|
+
@output = hash[:output]
|
|
22
|
+
|
|
23
|
+
# Context and subject
|
|
24
|
+
@context = hash[:context]
|
|
25
|
+
@subject = hash[:subject]
|
|
26
|
+
|
|
27
|
+
# Prompt composition overrides
|
|
28
|
+
@prompt_base = hash[:prompt_base]
|
|
29
|
+
@prompt_format = hash[:prompt_format]
|
|
30
|
+
@prompt_focus = hash[:prompt_focus]
|
|
31
|
+
@add_focus = hash[:add_focus]
|
|
32
|
+
@prompt_guidelines = hash[:prompt_guidelines]
|
|
33
|
+
|
|
34
|
+
# Execution options
|
|
35
|
+
@model = hash[:model]
|
|
36
|
+
@models = hash[:models]
|
|
37
|
+
@dry_run = hash[:dry_run] || false
|
|
38
|
+
@verbose = hash[:verbose] || false
|
|
39
|
+
@auto_execute = if @dry_run
|
|
40
|
+
false
|
|
41
|
+
elsif hash[:auto_execute].nil?
|
|
42
|
+
Ace::Review.get("defaults", "auto_execute") || false
|
|
43
|
+
else
|
|
44
|
+
hash[:auto_execute]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Session options
|
|
48
|
+
@save_session = hash.fetch(:save_session, true)
|
|
49
|
+
@session_dir = hash[:session_dir]
|
|
50
|
+
|
|
51
|
+
# PR review options
|
|
52
|
+
@pr = hash[:pr]
|
|
53
|
+
@post_comment = hash[:post_comment] || false
|
|
54
|
+
@pr_metadata = hash[:pr_metadata]
|
|
55
|
+
@gh_timeout = hash[:gh_timeout]
|
|
56
|
+
|
|
57
|
+
# PR comment options
|
|
58
|
+
@pr_comments = hash[:pr_comments] # nil = use default, true/false = explicit
|
|
59
|
+
@pr_comment_data = nil # Populated during execution
|
|
60
|
+
|
|
61
|
+
# Feedback extraction options
|
|
62
|
+
@no_feedback = hash[:no_feedback] || false
|
|
63
|
+
@feedback_model = hash[:feedback_model]
|
|
64
|
+
|
|
65
|
+
# List commands
|
|
66
|
+
@list_presets = hash[:list_presets] || false
|
|
67
|
+
@list_prompts = hash[:list_prompts] || false
|
|
68
|
+
@help = hash[:help] || false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert back to hash for compatibility
|
|
72
|
+
def to_h
|
|
73
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
74
|
+
key = var.to_s.delete_prefix("@").to_sym
|
|
75
|
+
value = instance_variable_get(var)
|
|
76
|
+
hash[key] = value unless value.nil?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if this is a list command
|
|
81
|
+
def list_command?
|
|
82
|
+
list_presets || list_prompts || help
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if this is a PR review
|
|
86
|
+
def pr_review?
|
|
87
|
+
!pr.nil? && !pr.to_s.strip.empty?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if comment posting should be triggered (includes dry-run preview)
|
|
91
|
+
def should_post_comment?
|
|
92
|
+
pr_review? && post_comment
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if PR comments should be included as feedback source
|
|
96
|
+
# Enabled by default for PR reviews, can be disabled with --no-pr-comments
|
|
97
|
+
def include_pr_comments?
|
|
98
|
+
return false unless pr_review?
|
|
99
|
+
|
|
100
|
+
# Explicit flag overrides everything
|
|
101
|
+
return pr_comments unless pr_comments.nil?
|
|
102
|
+
|
|
103
|
+
# Check config default (defaults to true for PR reviews)
|
|
104
|
+
config_default = Ace::Review.get("defaults", "pr_comments")
|
|
105
|
+
config_default.nil? || config_default
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if output should be saved
|
|
109
|
+
def save_output?
|
|
110
|
+
!dry_run && save_session
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if feedback extraction is enabled
|
|
114
|
+
def feedback_enabled?
|
|
115
|
+
!@no_feedback
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get effective model (single model)
|
|
119
|
+
# Priority: model scalar > first model in models array > config_model > default
|
|
120
|
+
def effective_model(config_model = nil)
|
|
121
|
+
return model if model
|
|
122
|
+
return models.first if models&.any?
|
|
123
|
+
config_model || "google:gemini-2.5-flash"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get effective models array
|
|
127
|
+
# Returns array of models, handling both single model and multi-model cases
|
|
128
|
+
# Priority: models array > model scalar > config_models > default
|
|
129
|
+
def effective_models(config_models = nil)
|
|
130
|
+
# If models array is set (from CLI), use it
|
|
131
|
+
return models if models&.any?
|
|
132
|
+
|
|
133
|
+
# If model scalar is set, wrap in array
|
|
134
|
+
return [model] if model
|
|
135
|
+
|
|
136
|
+
# If config provides models array, use it
|
|
137
|
+
if config_models.is_a?(Array) && config_models.any?
|
|
138
|
+
return config_models
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# If config provides single model, wrap in array
|
|
142
|
+
if config_models.is_a?(String) && !config_models.empty?
|
|
143
|
+
return [config_models]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Default to single model
|
|
147
|
+
["google:gemini-2.5-flash"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Merge with config values
|
|
151
|
+
def merge_config(config)
|
|
152
|
+
return if config.nil?
|
|
153
|
+
|
|
154
|
+
# Merge system prompt configuration if not overridden
|
|
155
|
+
@prompt_base ||= config.dig("system_prompt", "base")
|
|
156
|
+
@prompt_format ||= config.dig("system_prompt", "format")
|
|
157
|
+
|
|
158
|
+
# Handle focus modules
|
|
159
|
+
if @add_focus && config.dig("system_prompt", "focus")
|
|
160
|
+
existing_focus = config.dig("system_prompt", "focus") || []
|
|
161
|
+
additional_focus = @add_focus.split(",").map(&:strip)
|
|
162
|
+
@prompt_focus = (existing_focus + additional_focus).uniq.join(",")
|
|
163
|
+
elsif !@prompt_focus && config.dig("system_prompt", "focus")
|
|
164
|
+
@prompt_focus = Array(config.dig("system_prompt", "focus")).join(",")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@prompt_guidelines ||= Array(config.dig("system_prompt", "guidelines")).join(",") if config.dig("system_prompt", "guidelines")
|
|
168
|
+
|
|
169
|
+
# Merge other config values
|
|
170
|
+
@context ||= config["context"]
|
|
171
|
+
@subject ||= config["subject"]
|
|
172
|
+
|
|
173
|
+
# Handle models from config
|
|
174
|
+
# CLI models override preset models
|
|
175
|
+
unless @models&.any?
|
|
176
|
+
if config["models"].is_a?(Array) && config["models"].any?
|
|
177
|
+
@models = config["models"]
|
|
178
|
+
elsif config["model"]
|
|
179
|
+
@model ||= config["model"]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@gh_timeout ||= config["gh_timeout"]
|
|
184
|
+
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Build system prompt composition hash
|
|
189
|
+
def system_prompt_composition
|
|
190
|
+
composition = {}
|
|
191
|
+
|
|
192
|
+
composition["base"] = prompt_base if prompt_base
|
|
193
|
+
composition["format"] = prompt_format if prompt_format
|
|
194
|
+
|
|
195
|
+
if prompt_focus
|
|
196
|
+
composition["focus"] = prompt_focus.split(",").map(&:strip)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if prompt_guidelines
|
|
200
|
+
composition["guidelines"] = prompt_guidelines.split(",").map(&:strip)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
composition.empty? ? nil : composition
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Review
|
|
5
|
+
module Models
|
|
6
|
+
# Represents a configured reviewer entity with focus areas and filtering capabilities.
|
|
7
|
+
#
|
|
8
|
+
# Reviewers are configured entities that transform from simple model names to
|
|
9
|
+
# full-featured review participants with focus areas, file filtering, and
|
|
10
|
+
# weighted contributions.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a reviewer from config
|
|
13
|
+
# reviewer = Reviewer.new(
|
|
14
|
+
# name: "code-fit",
|
|
15
|
+
# model: "google:gemini-2.5-pro",
|
|
16
|
+
# focus: "code_quality",
|
|
17
|
+
# system_prompt_additions: "Focus on SOLID principles...",
|
|
18
|
+
# file_patterns: { include: ["lib/**/*.rb"], exclude: ["**/*_test.rb"] },
|
|
19
|
+
# weight: 1.0,
|
|
20
|
+
# critical: false
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
class Reviewer
|
|
24
|
+
# Default weight for reviewers (1.0 = full contribution)
|
|
25
|
+
DEFAULT_WEIGHT = 1.0
|
|
26
|
+
|
|
27
|
+
attr_reader :name, :model, :focus, :system_prompt_additions,
|
|
28
|
+
:file_patterns, :weight, :critical
|
|
29
|
+
|
|
30
|
+
# Initialize a new Reviewer from a configuration hash
|
|
31
|
+
#
|
|
32
|
+
# @param config [Hash] Configuration hash with reviewer settings
|
|
33
|
+
# @option config [String] :name Human-readable name for the reviewer
|
|
34
|
+
# @option config [String] :model LLM model identifier (e.g., "google:gemini-2.5-pro")
|
|
35
|
+
# @option config [String] :focus Review focus area (e.g., "code_quality", "security")
|
|
36
|
+
# @option config [String] :system_prompt_additions Additional system prompt text
|
|
37
|
+
# @option config [Hash] :file_patterns File filtering patterns
|
|
38
|
+
# @option config [Float] :weight Contribution weight (0.0-1.0, default: 1.0)
|
|
39
|
+
# @option config [Boolean] :critical Whether findings are always highlighted
|
|
40
|
+
def initialize(config = {})
|
|
41
|
+
# Support both symbol and string keys
|
|
42
|
+
config = normalize_keys(config)
|
|
43
|
+
|
|
44
|
+
@name = config["name"]
|
|
45
|
+
@model = config["model"]
|
|
46
|
+
@focus = config["focus"]
|
|
47
|
+
@system_prompt_additions = config["system_prompt_additions"]
|
|
48
|
+
@file_patterns = normalize_file_patterns(config["file_patterns"])
|
|
49
|
+
@weight = (config["weight"] || DEFAULT_WEIGHT).to_f
|
|
50
|
+
@critical = config["critical"] || false
|
|
51
|
+
|
|
52
|
+
validate!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create Reviewers from preset config (new reviewers array format)
|
|
56
|
+
#
|
|
57
|
+
# @param config [Hash] Preset configuration
|
|
58
|
+
# @return [Array<Reviewer>] Array of reviewer instances
|
|
59
|
+
def self.from_preset_config(config)
|
|
60
|
+
config = normalize_hash_keys(config)
|
|
61
|
+
|
|
62
|
+
# New format: reviewers array
|
|
63
|
+
if config["reviewers"].is_a?(Array) && config["reviewers"].any?
|
|
64
|
+
return config["reviewers"].map { |r| new(r) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Filter subject content based on reviewer's file patterns
|
|
71
|
+
#
|
|
72
|
+
# Delegates to SubjectFilter molecule for actual filtering logic.
|
|
73
|
+
#
|
|
74
|
+
# @param subject [Hash] Subject configuration with files/diff/content
|
|
75
|
+
# @return [Hash] Filtered subject (deep copy with only matching content)
|
|
76
|
+
def filter_subject(subject)
|
|
77
|
+
Molecules::SubjectFilter.filter(subject, file_patterns)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Enhance system prompt with reviewer's additions
|
|
81
|
+
#
|
|
82
|
+
# @param base_prompt [String] Original system prompt
|
|
83
|
+
# @return [String] Enhanced prompt with reviewer additions
|
|
84
|
+
def enhance_system_prompt(base_prompt)
|
|
85
|
+
return base_prompt unless system_prompt_additions
|
|
86
|
+
return system_prompt_additions if base_prompt.nil? || base_prompt.empty?
|
|
87
|
+
|
|
88
|
+
"#{base_prompt}\n\n#{system_prompt_additions}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if this reviewer has file patterns configured
|
|
92
|
+
#
|
|
93
|
+
# Delegates to SubjectFilter molecule.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] True if file patterns are configured
|
|
96
|
+
def has_file_patterns?
|
|
97
|
+
Molecules::SubjectFilter.has_patterns?(file_patterns)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if a file path matches this reviewer's patterns
|
|
101
|
+
#
|
|
102
|
+
# Delegates to SubjectFilter molecule.
|
|
103
|
+
#
|
|
104
|
+
# @param file_path [String] File path to check
|
|
105
|
+
# @return [Boolean] True if file matches (or no patterns configured)
|
|
106
|
+
def matches_file?(file_path)
|
|
107
|
+
Molecules::SubjectFilter.matches_file?(file_path, file_patterns)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Convert to hash representation
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash] Hash with string keys
|
|
113
|
+
def to_h
|
|
114
|
+
{
|
|
115
|
+
"name" => name,
|
|
116
|
+
"model" => model,
|
|
117
|
+
"focus" => focus,
|
|
118
|
+
"system_prompt_additions" => system_prompt_additions,
|
|
119
|
+
"file_patterns" => file_patterns,
|
|
120
|
+
"weight" => weight,
|
|
121
|
+
"critical" => critical
|
|
122
|
+
}.compact
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check equality with another reviewer
|
|
126
|
+
#
|
|
127
|
+
# @param other [Reviewer] Other reviewer to compare
|
|
128
|
+
# @return [Boolean] True if equal
|
|
129
|
+
def ==(other)
|
|
130
|
+
return false unless other.is_a?(Reviewer)
|
|
131
|
+
|
|
132
|
+
name == other.name &&
|
|
133
|
+
model == other.model &&
|
|
134
|
+
focus == other.focus &&
|
|
135
|
+
weight == other.weight &&
|
|
136
|
+
critical == other.critical
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
alias_method :eql?, :==
|
|
140
|
+
|
|
141
|
+
# Hash code for use in hash tables
|
|
142
|
+
#
|
|
143
|
+
# @return [Integer] Hash code
|
|
144
|
+
def hash
|
|
145
|
+
[name, model, focus, weight, critical].hash
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Validate required fields
|
|
151
|
+
def validate!
|
|
152
|
+
raise ArgumentError, "Reviewer model is required" if model.nil? || model.to_s.strip.empty?
|
|
153
|
+
raise ArgumentError, "Reviewer weight must be between 0 and 1" if weight < 0 || weight > 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Normalize hash keys to strings (supports both symbol and string keys)
|
|
157
|
+
def normalize_keys(hash)
|
|
158
|
+
return {} unless hash.is_a?(Hash)
|
|
159
|
+
hash.transform_keys(&:to_s)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Class method for key normalization
|
|
163
|
+
def self.normalize_hash_keys(hash)
|
|
164
|
+
return {} unless hash.is_a?(Hash)
|
|
165
|
+
hash.transform_keys(&:to_s)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Normalize file patterns structure
|
|
169
|
+
def normalize_file_patterns(patterns)
|
|
170
|
+
return nil unless patterns.is_a?(Hash)
|
|
171
|
+
|
|
172
|
+
normalized = normalize_keys(patterns)
|
|
173
|
+
{
|
|
174
|
+
"include" => Array(normalized["include"]),
|
|
175
|
+
"exclude" => Array(normalized["exclude"])
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|