cleo_quality_review 0.2.0 → 0.4.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 +4 -4
- data/cleo_quality_review.gemspec +0 -1
- data/lib/cleo_quality_review/changes_diff.rb +16 -13
- data/lib/cleo_quality_review/checks.rb +0 -1
- data/lib/cleo_quality_review/cli.rb +11 -2
- data/lib/cleo_quality_review/concurrent_executor.rb +125 -0
- data/lib/cleo_quality_review/configuration.rb +32 -1
- data/lib/cleo_quality_review/formatter.rb +18 -3
- data/lib/cleo_quality_review/github_client.rb +104 -0
- data/lib/cleo_quality_review/github_review_builder.rb +13 -5
- data/lib/cleo_quality_review/github_review_publisher.rb +12 -57
- data/lib/cleo_quality_review/incremental_base_resolver.rb +157 -0
- data/lib/cleo_quality_review/llm_client.rb +9 -5
- data/lib/cleo_quality_review/llm_providers/open_ai.rb +15 -7
- data/lib/cleo_quality_review/llm_providers/stub.rb +7 -3
- data/lib/cleo_quality_review/options.rb +16 -5
- data/lib/cleo_quality_review/run.rb +8 -0
- data/lib/cleo_quality_review/runner.rb +33 -9
- data/lib/cleo_quality_review/version.rb +1 -1
- data/lib/cleo_quality_review.rb +0 -1
- data/prompts/agent.md +9 -16
- data/prompts/configuration.md +47 -0
- data/prompts/github.md +5 -19
- data/prompts/human.md +5 -13
- data/prompts/pr_review.md +6 -14
- metadata +5 -16
- data/lib/cleo_quality_review/checks/debride.rb +0 -65
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "github_client"
|
|
6
|
+
require_relative "llm_errors"
|
|
7
|
+
|
|
8
|
+
module CleoQualityReview
|
|
9
|
+
##
|
|
10
|
+
# Resolves the git base for an incremental review.
|
|
11
|
+
#
|
|
12
|
+
# On a pull request that cleo-quality-review has already reviewed, this
|
|
13
|
+
# returns the most recent previously-reviewed commit that is still an
|
|
14
|
+
# ancestor of the current head, so only changes made since that review are
|
|
15
|
+
# analysed. It falls back to +nil+ (meaning "review the full diff") outside a
|
|
16
|
+
# pull request context, when no prior review survives in history, or on any
|
|
17
|
+
# lookup error.
|
|
18
|
+
class IncrementalBaseResolver
|
|
19
|
+
REVIEW_MARKER_PREFIX = "<!-- cleo-quality-review:"
|
|
20
|
+
DISABLED_VALUES = %w[0 false no off].freeze
|
|
21
|
+
ENABLED_ENV_KEY = "CLEO_QUALITY_REVIEW_INCREMENTAL"
|
|
22
|
+
REVIEWS_PER_PAGE = 100
|
|
23
|
+
MAX_REVIEW_PAGES = 20
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @param [CommandRunner] command_runner for executing git commands
|
|
27
|
+
# @param [Hash{String => String}] env process environment
|
|
28
|
+
# @param [GitHubClient, nil] client GitHub API client (built from env when omitted)
|
|
29
|
+
def initialize(command_runner:, env: ENV, client: nil)
|
|
30
|
+
@command_runner = command_runner
|
|
31
|
+
@env = env
|
|
32
|
+
@client = client
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Resolve the incremental base commit.
|
|
37
|
+
# @param [String] head git ref for the current head
|
|
38
|
+
# @return [String, nil] commit SHA to diff against, or nil to review the full diff
|
|
39
|
+
def resolve(head: "HEAD")
|
|
40
|
+
return nil unless incremental_lookup_available?
|
|
41
|
+
|
|
42
|
+
newest_reviewed_ancestor(head)
|
|
43
|
+
rescue StandardError => error
|
|
44
|
+
warn("cleo-quality-review: incremental base lookup failed (#{error.message}); reviewing the full diff")
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :command_runner, :env
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# @return [Boolean] whether an incremental lookup can run in this context
|
|
54
|
+
def incremental_lookup_available?
|
|
55
|
+
enabled? && !pull_request_number.nil? && !token.nil? && !repository.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def newest_reviewed_ancestor(head)
|
|
59
|
+
reviewed_commit_ids.find { |sha| ancestor?(sha, head) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reviewed_commit_ids
|
|
63
|
+
reviews
|
|
64
|
+
.select { |review| quality_review?(review) }
|
|
65
|
+
.sort_by { |review| review["submitted_at"].to_s }
|
|
66
|
+
.reverse
|
|
67
|
+
.filter_map { |review| review["commit_id"] }
|
|
68
|
+
.reject { |sha| sha.to_s.strip.empty? }
|
|
69
|
+
.uniq
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Fetch every submitted review, following pagination so the newest reviews
|
|
74
|
+
# are not missed on pull requests with more than one page of reviews.
|
|
75
|
+
# @return [Array<Hash>]
|
|
76
|
+
def reviews
|
|
77
|
+
(1..MAX_REVIEW_PAGES).each_with_object([]) do |page, all|
|
|
78
|
+
page_reviews = reviews_page(page)
|
|
79
|
+
all.concat(page_reviews)
|
|
80
|
+
break all if page_reviews.length < REVIEWS_PER_PAGE
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reviews_page(page)
|
|
85
|
+
response = client.get("/repos/#{repository}/pulls/#{pull_request_number}/reviews?per_page=#{REVIEWS_PER_PAGE}&page=#{page}")
|
|
86
|
+
raise Error, "GitHub review lookup returned status #{response.status_code}" unless response.success?
|
|
87
|
+
|
|
88
|
+
parsed = JSON.parse(response.body)
|
|
89
|
+
parsed.is_a?(Array) ? parsed : []
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Only trust bot-authored reviews that carry our marker. A human contributor
|
|
94
|
+
# could otherwise forge the marker in their own review and steer the base
|
|
95
|
+
# past changes the tool never analysed.
|
|
96
|
+
# @param [Hash] review
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def quality_review?(review)
|
|
99
|
+
bot_authored?(review) && marked?(review)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def bot_authored?(review)
|
|
103
|
+
review.dig("user", "type") == "Bot"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def marked?(review)
|
|
107
|
+
review.fetch("body") { "" }.to_s.include?(REVIEW_MARKER_PREFIX)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ancestor?(sha, head)
|
|
111
|
+
command_runner.run("git", "merge-base", "--is-ancestor", sha, head).success?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def enabled?
|
|
115
|
+
!DISABLED_VALUES.include?(env.fetch(ENABLED_ENV_KEY) { "" }.to_s.strip.downcase)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pull_request_number
|
|
119
|
+
return @pull_request_number if defined?(@pull_request_number)
|
|
120
|
+
|
|
121
|
+
@pull_request_number = event && (event["number"] || event.dig("pull_request", "number"))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def event
|
|
125
|
+
return @event if defined?(@event)
|
|
126
|
+
|
|
127
|
+
@event = load_event
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def load_event
|
|
131
|
+
path = env["GITHUB_EVENT_PATH"]
|
|
132
|
+
return nil if path.to_s.empty? || !File.file?(path)
|
|
133
|
+
|
|
134
|
+
JSON.parse(File.read(path))
|
|
135
|
+
rescue JSON::ParserError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def token
|
|
140
|
+
value = env["GITHUB_TOKEN"].to_s
|
|
141
|
+
value unless value.empty?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def repository
|
|
145
|
+
value = env["GITHUB_REPOSITORY"].to_s
|
|
146
|
+
value unless value.empty?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def api_url
|
|
150
|
+
env.fetch("GITHUB_API_URL") { GitHubClient::DEFAULT_API_URL }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def client
|
|
154
|
+
@client ||= GitHubClient.new(token: token, api_url: api_url)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -19,10 +19,12 @@ module CleoQualityReview
|
|
|
19
19
|
|
|
20
20
|
##
|
|
21
21
|
# Generate a review from the given prompt
|
|
22
|
-
# @param [String] prompt
|
|
22
|
+
# @param [String] prompt the format-specific prompt sent as input
|
|
23
|
+
# @param [String, nil] instructions shared configuration prompt applied to
|
|
24
|
+
# every run
|
|
23
25
|
# @return [String] the generated review
|
|
24
|
-
def generate_review(prompt)
|
|
25
|
-
generate_with_logging(prompt)
|
|
26
|
+
def generate_review(prompt, instructions: nil)
|
|
27
|
+
generate_with_logging(prompt, instructions)
|
|
26
28
|
rescue StandardError => e
|
|
27
29
|
log_error(prompt, e)
|
|
28
30
|
raise
|
|
@@ -32,8 +34,10 @@ module CleoQualityReview
|
|
|
32
34
|
|
|
33
35
|
attr_reader :config, :logger
|
|
34
36
|
|
|
35
|
-
def generate_with_logging(prompt)
|
|
36
|
-
provider_client.generate_review(prompt).tap
|
|
37
|
+
def generate_with_logging(prompt, instructions)
|
|
38
|
+
provider_client.generate_review(prompt, instructions: instructions).tap do |response|
|
|
39
|
+
log_success(prompt, response)
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def log_success(prompt, response)
|
|
@@ -89,11 +89,13 @@ module CleoQualityReview
|
|
|
89
89
|
|
|
90
90
|
##
|
|
91
91
|
# Generate a review using the OpenAI Responses API.
|
|
92
|
-
# @param [String] prompt the prompt to send
|
|
92
|
+
# @param [String] prompt the format-specific prompt to send as input
|
|
93
|
+
# @param [String, nil] instructions shared configuration prompt sent as
|
|
94
|
+
# the system-level instructions applied to every run
|
|
93
95
|
# @return [String] generated review text
|
|
94
96
|
# @raise [ApiError] if the API request fails
|
|
95
|
-
def generate_review(prompt)
|
|
96
|
-
response = execute_request(prompt)
|
|
97
|
+
def generate_review(prompt, instructions: nil)
|
|
98
|
+
response = execute_request(request_body(prompt, instructions))
|
|
97
99
|
parse_response(response)
|
|
98
100
|
end
|
|
99
101
|
|
|
@@ -101,9 +103,9 @@ module CleoQualityReview
|
|
|
101
103
|
|
|
102
104
|
attr_reader :config, :http_transport
|
|
103
105
|
|
|
104
|
-
def execute_request(
|
|
106
|
+
def execute_request(body)
|
|
105
107
|
timeout_seconds = config.timeout_seconds
|
|
106
|
-
http_transport.post_json(build_request(
|
|
108
|
+
http_transport.post_json(build_request(body, timeout_seconds))
|
|
107
109
|
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
|
|
108
110
|
raise ApiError, timeout_error_message(timeout_seconds, e)
|
|
109
111
|
end
|
|
@@ -116,15 +118,21 @@ module CleoQualityReview
|
|
|
116
118
|
raise ApiError, "OpenAI Responses API returned invalid JSON: #{e.message}"
|
|
117
119
|
end
|
|
118
120
|
|
|
119
|
-
def build_request(
|
|
121
|
+
def build_request(body, timeout_seconds)
|
|
120
122
|
HttpRequest.new(
|
|
121
123
|
uri: RESPONSES_API_URL,
|
|
122
124
|
headers: headers,
|
|
123
|
-
body:
|
|
125
|
+
body: body,
|
|
124
126
|
timeout_seconds: timeout_seconds,
|
|
125
127
|
)
|
|
126
128
|
end
|
|
127
129
|
|
|
130
|
+
def request_body(prompt, instructions)
|
|
131
|
+
body = { model: config.model, input: prompt }
|
|
132
|
+
body[:instructions] = instructions unless instructions.to_s.strip.empty?
|
|
133
|
+
body
|
|
134
|
+
end
|
|
135
|
+
|
|
128
136
|
def timeout_error_message(timeout_seconds, error)
|
|
129
137
|
"OpenAI Responses API request timed out after #{timeout_seconds} seconds: #{error.class}: #{error.message}"
|
|
130
138
|
end
|
|
@@ -54,21 +54,25 @@ module CleoQualityReview
|
|
|
54
54
|
##
|
|
55
55
|
# Stub LLM client, mirrors OpenAi::Client interface.
|
|
56
56
|
class Client
|
|
57
|
-
attr_reader :received_prompts
|
|
57
|
+
attr_reader :received_prompts, :received_instructions
|
|
58
58
|
|
|
59
59
|
##
|
|
60
60
|
# @param [Config] config stub configuration
|
|
61
61
|
def initialize(config:)
|
|
62
62
|
@config = config
|
|
63
63
|
@received_prompts = []
|
|
64
|
+
@received_instructions = []
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
##
|
|
67
68
|
# Generate a review by returning the configured response.
|
|
68
|
-
# @param [String] prompt the prompt sent
|
|
69
|
+
# @param [String] prompt the format-specific prompt sent as input
|
|
70
|
+
# @param [String, nil] instructions shared configuration prompt applied
|
|
71
|
+
# to every run
|
|
69
72
|
# @return [String] the configured response
|
|
70
|
-
def generate_review(prompt)
|
|
73
|
+
def generate_review(prompt, instructions: nil)
|
|
71
74
|
received_prompts << prompt
|
|
75
|
+
received_instructions << instructions
|
|
72
76
|
response = config.response
|
|
73
77
|
|
|
74
78
|
case response
|
|
@@ -25,7 +25,9 @@ module CleoQualityReview
|
|
|
25
25
|
# @return [Array<String>] checks to exclude
|
|
26
26
|
# @!attribute [r] changed
|
|
27
27
|
# @return [Boolean] whether to filter to changed files only
|
|
28
|
-
|
|
28
|
+
# @!attribute [r] jobs
|
|
29
|
+
# @return [Integer, nil] max checks to run in parallel, or nil to auto-size
|
|
30
|
+
ParseResult = Struct.new(:format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file, :jobs, keyword_init: true) do
|
|
29
31
|
##
|
|
30
32
|
# @return [String] validated review_id
|
|
31
33
|
# @raise [OptionParser::MissingArgument] if review_id is blank
|
|
@@ -64,6 +66,7 @@ module CleoQualityReview
|
|
|
64
66
|
@log = false
|
|
65
67
|
@review_id = nil
|
|
66
68
|
@review_file = nil
|
|
69
|
+
@jobs = nil
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
##
|
|
@@ -85,17 +88,19 @@ module CleoQualityReview
|
|
|
85
88
|
log: log,
|
|
86
89
|
review_id: review_id,
|
|
87
90
|
review_file: review_file,
|
|
91
|
+
jobs: jobs,
|
|
88
92
|
)
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
private
|
|
92
96
|
|
|
93
|
-
attr_reader :argv, :format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file
|
|
97
|
+
attr_reader :argv, :format, :checks, :files, :exclude, :changed, :base, :log, :review_id, :review_file, :jobs
|
|
94
98
|
|
|
95
99
|
def parser
|
|
96
100
|
OptionParser.new do |opts|
|
|
97
101
|
opts.banner = "Usage: check_quality [options] [files...]"
|
|
98
102
|
register_options(opts)
|
|
103
|
+
register_help_option(opts)
|
|
99
104
|
end
|
|
100
105
|
end
|
|
101
106
|
|
|
@@ -104,7 +109,13 @@ module CleoQualityReview
|
|
|
104
109
|
register_check_options(opts)
|
|
105
110
|
register_target_options(opts)
|
|
106
111
|
register_output_options(opts)
|
|
107
|
-
|
|
112
|
+
register_jobs_option(opts)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def register_jobs_option(opts)
|
|
116
|
+
opts.on("-j", "--jobs N", Integer, "Max checks to run in parallel (default: CPU cores)") do |value|
|
|
117
|
+
@jobs = value
|
|
118
|
+
end
|
|
108
119
|
end
|
|
109
120
|
|
|
110
121
|
def register_format_option(opts)
|
|
@@ -120,7 +131,7 @@ module CleoQualityReview
|
|
|
120
131
|
end
|
|
121
132
|
|
|
122
133
|
def register_checks_option(opts)
|
|
123
|
-
opts.on("-c", "--checks CHECKS", Array, "Checks to run: all, reek, flog, fasterer
|
|
134
|
+
opts.on("-c", "--checks CHECKS", Array, "Checks to run: all, reek, flog, fasterer") { |values| checks.concat(values) }
|
|
124
135
|
end
|
|
125
136
|
|
|
126
137
|
def register_only_option(opts)
|
|
@@ -128,7 +139,7 @@ module CleoQualityReview
|
|
|
128
139
|
end
|
|
129
140
|
|
|
130
141
|
def register_exclude_option(opts)
|
|
131
|
-
opts.on("-x", "--exclude CHECKS", Array, "Checks to exclude: reek, flog, fasterer
|
|
142
|
+
opts.on("-x", "--exclude CHECKS", Array, "Checks to exclude: reek, flog, fasterer") { |values| exclude.concat(values) }
|
|
132
143
|
end
|
|
133
144
|
|
|
134
145
|
def register_target_options(opts)
|
|
@@ -38,6 +38,14 @@ module CleoQualityReview
|
|
|
38
38
|
:log,
|
|
39
39
|
keyword_init: true,
|
|
40
40
|
) do
|
|
41
|
+
##
|
|
42
|
+
# Whether the run has any files to review. Runs with no target files
|
|
43
|
+
# (e.g. a branch that only changes non-Ruby files) have nothing to analyse.
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def reviewable?
|
|
46
|
+
!Array(target_files).empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
##
|
|
42
50
|
# Convert the run to a hash representation
|
|
43
51
|
# @return [Hash{Symbol => Object}]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
+
require "forwardable"
|
|
4
5
|
require "json"
|
|
5
6
|
|
|
6
7
|
require_relative "changes_diff"
|
|
7
8
|
require_relative "checks"
|
|
8
9
|
require_relative "command_runner"
|
|
10
|
+
require_relative "concurrent_executor"
|
|
9
11
|
require_relative "git_diff_base"
|
|
10
12
|
require_relative "run"
|
|
11
13
|
require_relative "run_artifacts"
|
|
@@ -15,6 +17,8 @@ module CleoQualityReview
|
|
|
15
17
|
##
|
|
16
18
|
# Orchestrates a complete quality review run
|
|
17
19
|
class Runner
|
|
20
|
+
extend Forwardable
|
|
21
|
+
|
|
18
22
|
##
|
|
19
23
|
# Grouped values resolved at the start of an analysis run
|
|
20
24
|
AnalysisContext = Struct.new(:timestamp, :base_ref, :target, :changes, :review_id, :check_classes, keyword_init: true) do
|
|
@@ -32,16 +36,28 @@ module CleoQualityReview
|
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
38
|
|
|
39
|
+
##
|
|
40
|
+
# Runtime collaborators for a quality review run
|
|
41
|
+
Dependencies = Struct.new(:command_runner, :clock, :check_registry, :base_resolver, :executor, keyword_init: true) do
|
|
42
|
+
def self.for(options, overrides)
|
|
43
|
+
new(
|
|
44
|
+
**{
|
|
45
|
+
command_runner: CommandRunner.new,
|
|
46
|
+
clock: Time,
|
|
47
|
+
check_registry: Checks,
|
|
48
|
+
base_resolver: nil,
|
|
49
|
+
executor: ConcurrentExecutor.new(max_workers: options.jobs),
|
|
50
|
+
}.merge(overrides),
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
35
55
|
##
|
|
36
56
|
# @param [Options::ParseResult] options parsed command-line options
|
|
37
|
-
# @param [
|
|
38
|
-
|
|
39
|
-
# @param [CheckRegistry] check_registry registry for resolving check names
|
|
40
|
-
def initialize(options:, command_runner: CommandRunner.new, clock: Time, check_registry: Checks)
|
|
57
|
+
# @param [Hash] dependencies optional runtime collaborators for tests or alternate runners
|
|
58
|
+
def initialize(options:, **dependencies)
|
|
41
59
|
@options = options
|
|
42
|
-
@
|
|
43
|
-
@clock = clock
|
|
44
|
-
@check_registry = check_registry
|
|
60
|
+
@dependencies = Dependencies.for(options, dependencies)
|
|
45
61
|
end
|
|
46
62
|
|
|
47
63
|
##
|
|
@@ -57,7 +73,9 @@ module CleoQualityReview
|
|
|
57
73
|
|
|
58
74
|
private
|
|
59
75
|
|
|
60
|
-
attr_reader :options, :
|
|
76
|
+
attr_reader :options, :dependencies
|
|
77
|
+
def_delegators :dependencies, :command_runner, :clock, :check_registry, :base_resolver, :executor
|
|
78
|
+
private :command_runner, :clock, :check_registry, :base_resolver, :executor
|
|
61
79
|
|
|
62
80
|
def epoch_milliseconds
|
|
63
81
|
(clock.now.to_r * 1_000).to_i
|
|
@@ -127,7 +145,9 @@ module CleoQualityReview
|
|
|
127
145
|
end
|
|
128
146
|
|
|
129
147
|
def run_checks(check_classes, ruby_files, timestamp)
|
|
130
|
-
|
|
148
|
+
return [] if ruby_files.empty?
|
|
149
|
+
|
|
150
|
+
executor.map(check_classes) do |check_class|
|
|
131
151
|
check_class.new(command_runner: command_runner, timestamp: timestamp).run(ruby_files)
|
|
132
152
|
end
|
|
133
153
|
end
|
|
@@ -170,6 +190,10 @@ module CleoQualityReview
|
|
|
170
190
|
end
|
|
171
191
|
|
|
172
192
|
def base_ref
|
|
193
|
+
@base_ref ||= base_resolver&.resolve || default_base_ref
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def default_base_ref
|
|
173
197
|
options.base || GitDiffBase::DEFAULT_BASE_REF
|
|
174
198
|
end
|
|
175
199
|
end
|
data/lib/cleo_quality_review.rb
CHANGED
|
@@ -14,7 +14,6 @@ module CleoQualityReview
|
|
|
14
14
|
Checks.register("Reek", Checks::Reek, tool_type: :smell_detection)
|
|
15
15
|
Checks.register("Flog", Checks::Flog, tool_type: :complexity)
|
|
16
16
|
Checks.register("Fasterer", Checks::Fasterer, tool_type: :performance)
|
|
17
|
-
Checks.register("Debride", Checks::Debride, tool_type: :dead_code)
|
|
18
17
|
|
|
19
18
|
##
|
|
20
19
|
# Register all supported LLM APIs for formatting output here
|
data/prompts/agent.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
You are reviewing Ruby code quality findings for consumption by AI coding assistants.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Flog**: Ignore scores below 40.0
|
|
8
|
-
- **Reek**: Focus on FeatureEnvy, TooManyStatements, DuplicateMethodCall, NestedIterators, LongParameterList
|
|
9
|
-
- **Fasterer**: Include all performance suggestions
|
|
10
|
-
- **Debride**: Treat as lower-confidence static dead-code detection. Include only findings that are clearly actionable and avoid recommending deletion without checking dynamic call paths.
|
|
3
|
+
Apply the shared review rules from the configuration prompt provided alongside this one.
|
|
4
|
+
That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
|
|
5
|
+
This prompt defines only the output format.
|
|
11
6
|
|
|
12
7
|
## Output Format
|
|
13
8
|
|
|
@@ -21,7 +16,7 @@ Output valid JSON matching this exact schema:
|
|
|
21
16
|
"target_files": [<file paths from metadata>],
|
|
22
17
|
"findings": [
|
|
23
18
|
{
|
|
24
|
-
"tool_name": "<reek|flog|fasterer
|
|
19
|
+
"tool_name": "<reek|flog|fasterer>",
|
|
25
20
|
"tool_type": "<smell_detection|complexity|performance|dead_code>",
|
|
26
21
|
"check": "<specific check type>",
|
|
27
22
|
"filepath": "<relative file path>",
|
|
@@ -33,7 +28,7 @@ Output valid JSON matching this exact schema:
|
|
|
33
28
|
"check_outputs": [
|
|
34
29
|
{
|
|
35
30
|
"check_name": "<check name>",
|
|
36
|
-
"tool_name": "<reek|flog|fasterer
|
|
31
|
+
"tool_name": "<reek|flog|fasterer>",
|
|
37
32
|
"tool_type": "<smell_detection|complexity|performance|dead_code>",
|
|
38
33
|
"extension": "<json|txt>",
|
|
39
34
|
"path": "<raw output artifact path>",
|
|
@@ -44,10 +39,8 @@ Output valid JSON matching this exact schema:
|
|
|
44
39
|
}
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
##
|
|
42
|
+
## Output rules
|
|
48
43
|
|
|
49
|
-
1.
|
|
50
|
-
2.
|
|
51
|
-
3.
|
|
52
|
-
4. Include the raw check outputs in `check_outputs` for reference
|
|
53
|
-
5. Output ONLY valid JSON - no markdown fences, no explanatory text
|
|
44
|
+
1. Write concise `result` descriptions an agent can act on.
|
|
45
|
+
2. Include the raw check outputs in `check_outputs` for reference.
|
|
46
|
+
3. Output ONLY valid JSON - no markdown fences, no explanatory text.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
You are reviewing Ruby code quality findings produced by static analysis tools.
|
|
2
|
+
|
|
3
|
+
These are the standard rules that apply to every review, regardless of the output format.
|
|
4
|
+
The output format is defined separately in the format-specific prompt that accompanies these rules.
|
|
5
|
+
|
|
6
|
+
## Inputs
|
|
7
|
+
|
|
8
|
+
You are given the raw output from a series of code quality tools (including, but not limited to, Reek, Flog, Fasterer, Flay, and Brakeman), together with the git diff for the change under review.
|
|
9
|
+
The combined tool output is noisy.
|
|
10
|
+
Your job is to decide what genuinely matters and to discard the rest.
|
|
11
|
+
The diff is provided so you can map tool findings to the lines that changed.
|
|
12
|
+
|
|
13
|
+
## Excluded files
|
|
14
|
+
|
|
15
|
+
Do not review test files: ignore every tool finding that points to one, and never post a comment on a test file.
|
|
16
|
+
Test files are those inside a `test/` or `spec/` directory, for example `test/models/user_test.rb`.
|
|
17
|
+
A file that merely has `test` in its name but lives in application code, such as an A/B-test model under `app/`, is not a test file.
|
|
18
|
+
Tests in this codebase are intentionally verbose and self-contained, so the smells these tools report on them are expected rather than defects.
|
|
19
|
+
|
|
20
|
+
## Tool thresholds and severity
|
|
21
|
+
|
|
22
|
+
- **Flog**: Ignore scores below 40.0. Treat high-complexity methods as the most important findings because they are the most expensive to maintain.
|
|
23
|
+
- **Reek**: Prefer actionable smells such as FeatureEnvy, DuplicateMethodCall, NestedIterators, and LongParameterList.
|
|
24
|
+
- **Fasterer**: Low severity. Include a performance suggestion only when it clearly applies to code changed by this review and the fix is straightforward.
|
|
25
|
+
|
|
26
|
+
## Rule-specific guidance
|
|
27
|
+
|
|
28
|
+
These notes refine how individual rules should be treated.
|
|
29
|
+
Where a note here conflicts with the general guidance above, the note takes precedence for that rule.
|
|
30
|
+
|
|
31
|
+
### Reek: TooManyStatements
|
|
32
|
+
|
|
33
|
+
- Deprioritise this smell in application code.
|
|
34
|
+
Only surface it when the method is a particularly egregious example, such as a long method that clearly juggles several unrelated responsibilities, and omit it otherwise.
|
|
35
|
+
|
|
36
|
+
## Prioritisation
|
|
37
|
+
|
|
38
|
+
1. Prioritise issues that affect maintainability, correctness, readability, performance, and long-term ownership.
|
|
39
|
+
2. Order findings by impact: high-complexity methods first, then code smells, then performance suggestions.
|
|
40
|
+
3. Filter out low-signal findings.
|
|
41
|
+
|
|
42
|
+
## Noise reduction
|
|
43
|
+
|
|
44
|
+
- Do not comment on the code diff itself unless the comment is directly supported by a tool finding.
|
|
45
|
+
- Do not repeat tool output mechanically. When several findings are of the same kind, highlight a couple of representative examples and then make one general recommendation.
|
|
46
|
+
- If a finding is low value, stale, ambiguous, or a likely false positive, omit it or note it briefly.
|
|
47
|
+
- Keep every finding concise and actionable, specific enough for an engineer or coding agent to act on.
|
data/prompts/github.md
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
You are the pipeline interface between a series of code reviews for a git diff, and the GitHub Actions automation pipeline.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Apply the shared review rules from the configuration prompt provided alongside this one.
|
|
4
|
+
That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
|
|
5
|
+
This prompt defines only the output format.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
For weighting, consider the following values as guides:
|
|
8
|
-
|
|
9
|
-
Flog:
|
|
10
|
-
Threshold: 40.0
|
|
11
|
-
ThresholdType: GreaterThanOrEqual
|
|
12
|
-
Severity: Medium to High
|
|
13
|
-
|
|
14
|
-
Reek:
|
|
15
|
-
Severity: Low to Medium
|
|
16
|
-
|
|
17
|
-
Fasterer:
|
|
18
|
-
Severity: Low
|
|
19
|
-
|
|
20
|
-
Debride:
|
|
21
|
-
Severity: Low
|
|
22
|
-
Notes: Lower-confidence static dead-code signal. Only report when the finding is specific, actionable, and unlikely to be a dynamic Rails call.
|
|
7
|
+
You produce useful, meaningful output for the engineer whose PR triggered this flow.
|
|
23
8
|
|
|
9
|
+
## Output Format
|
|
24
10
|
|
|
25
11
|
You MUST NOT return so many items that the feedback is noisy and confusing. Limit yourself to maximum 10 comments.
|
|
26
12
|
|
data/prompts/human.md
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
You are reviewing a local code change for code quality.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Apply the shared review rules from the configuration prompt provided alongside this one.
|
|
4
|
+
That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
|
|
5
|
+
This prompt defines only the output format.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Output Format
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Prioritize issues that are likely to matter to maintainability, correctness, readability, or long-term ownership.
|
|
10
|
-
|
|
11
|
-
Avoid repeating tool output mechanically. If multiple issues of the same sort are reported, it's fine to highlight a couple of examples and then make a general comment for improvement.
|
|
12
|
-
|
|
13
|
-
If a tool finding is low value or likely a false positive, say so briefly or omit it.
|
|
14
|
-
|
|
15
|
-
Debride findings are lower-confidence static dead-code candidates. Do not recommend deleting code unless the finding is clearly supported by the changed code and dynamic call paths have been considered.
|
|
16
|
-
|
|
17
|
-
The output will be printed in a unix terminal, and so colour-coded feedback is preferrable.
|
|
9
|
+
The output will be printed in a Unix terminal, and so colour-coded feedback is preferable.
|
|
18
10
|
|
|
19
11
|
1. Highest-impact issues first, with file and line references as clickable links when available.
|
|
20
12
|
2. Suggested changes that are specific enough for an engineer or coding agent to implement.
|
data/prompts/pr_review.md
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
You are the pipeline interface between code quality tools and GitHub pull request review comments.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Tool Thresholds
|
|
8
|
-
|
|
9
|
-
- **Flog**: Ignore scores below 40.0. Prioritize high-complexity methods because they are the most expensive to maintain.
|
|
10
|
-
- **Reek**: Prefer actionable smells such as FeatureEnvy, TooManyStatements, DuplicateMethodCall, NestedIterators, and LongParameterList.
|
|
11
|
-
- **Fasterer**: Low severity. Include only when the finding is clearly on code changed by this PR and the fix is straightforward.
|
|
12
|
-
- **Debride**: Lower-confidence static dead-code signal. Include only when the candidate method is clearly made obsolete by this PR, and do not suggest deletion without noting possible dynamic Rails calls.
|
|
3
|
+
Apply the shared review rules from the configuration prompt provided alongside this one.
|
|
4
|
+
That prompt defines the inputs, tool thresholds, prioritisation, and noise-reduction rules.
|
|
5
|
+
This prompt defines only the output format.
|
|
13
6
|
|
|
14
7
|
## Comment Selection
|
|
15
8
|
|
|
16
9
|
1. Limit yourself to ten comments at most.
|
|
17
10
|
2. Prefer findings that map directly to a changed or commentable right-side line in the git diff.
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
-
5. Keep comments concise and actionable. Mention the tool and check name.
|
|
11
|
+
3. If a tool finding points to a file or line that is not visible in the provided diff, omit the inline comment.
|
|
12
|
+
4. Mention the tool and check name in each comment.
|
|
21
13
|
|
|
22
14
|
## Output Format
|
|
23
15
|
|
|
@@ -41,7 +33,7 @@ The JSON MUST match this schema:
|
|
|
41
33
|
|
|
42
34
|
## Comment format:
|
|
43
35
|
|
|
44
|
-
The comments should prioritise readability and actionabilty. Assume the reader is a junior developer, or someone who is not familiar with the language and framework. Be helpful, without being overly verbose.
|
|
36
|
+
The comments should prioritise readability and actionabilty. Assume the reader is a junior developer, or someone who is not familiar with the language and framework. Be helpful, without being overly verbose.
|
|
45
37
|
|
|
46
38
|
Example format:
|
|
47
39
|
```
|