cleo_quality_review 0.1.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/cleo_quality_review.gemspec +31 -0
- data/config/default.yml +7 -0
- data/exe/check_quality +20 -0
- data/lib/cleo_quality_review/changes_diff.rb +67 -0
- data/lib/cleo_quality_review/checks/debride.rb +65 -0
- data/lib/cleo_quality_review/checks/fasterer.rb +35 -0
- data/lib/cleo_quality_review/checks/flog.rb +35 -0
- data/lib/cleo_quality_review/checks/quality_check.rb +143 -0
- data/lib/cleo_quality_review/checks/reek.rb +53 -0
- data/lib/cleo_quality_review/checks/registry.rb +72 -0
- data/lib/cleo_quality_review/checks.rb +38 -0
- data/lib/cleo_quality_review/cli.rb +105 -0
- data/lib/cleo_quality_review/command_result.rb +21 -0
- data/lib/cleo_quality_review/command_runner.rb +27 -0
- data/lib/cleo_quality_review/configuration.rb +193 -0
- data/lib/cleo_quality_review/diff_map.rb +95 -0
- data/lib/cleo_quality_review/formatter.rb +58 -0
- data/lib/cleo_quality_review/github_review_builder.rb +140 -0
- data/lib/cleo_quality_review/github_review_publisher.rb +150 -0
- data/lib/cleo_quality_review/llm_client.rb +59 -0
- data/lib/cleo_quality_review/llm_config.rb +40 -0
- data/lib/cleo_quality_review/llm_errors.rb +19 -0
- data/lib/cleo_quality_review/llm_logger.rb +66 -0
- data/lib/cleo_quality_review/llm_providers/open_ai.rb +188 -0
- data/lib/cleo_quality_review/llm_providers/open_ai_config.rb +83 -0
- data/lib/cleo_quality_review/llm_providers/registry.rb +61 -0
- data/lib/cleo_quality_review/llm_providers/stub.rb +107 -0
- data/lib/cleo_quality_review/llm_providers.rb +44 -0
- data/lib/cleo_quality_review/options.rb +171 -0
- data/lib/cleo_quality_review/prompt_builder.rb +95 -0
- data/lib/cleo_quality_review/prompt_loader.rb +49 -0
- data/lib/cleo_quality_review/result.rb +58 -0
- data/lib/cleo_quality_review/run.rb +78 -0
- data/lib/cleo_quality_review/run_artifacts/raw_check_outputs.rb +97 -0
- data/lib/cleo_quality_review/run_artifacts.rb +146 -0
- data/lib/cleo_quality_review/runner.rb +158 -0
- data/lib/cleo_quality_review/target_resolver.rb +127 -0
- data/lib/cleo_quality_review/version.rb +7 -0
- data/lib/cleo_quality_review.rb +23 -0
- data/prompts/agent.md +53 -0
- data/prompts/github.md +29 -0
- data/prompts/human.md +23 -0
- data/prompts/pr_review.md +62 -0
- metadata +141 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "changes_diff"
|
|
7
|
+
require_relative "checks"
|
|
8
|
+
require_relative "command_runner"
|
|
9
|
+
require_relative "run"
|
|
10
|
+
require_relative "run_artifacts"
|
|
11
|
+
require_relative "target_resolver"
|
|
12
|
+
|
|
13
|
+
module CleoQualityReview
|
|
14
|
+
##
|
|
15
|
+
# Orchestrates a complete quality review run
|
|
16
|
+
class Runner
|
|
17
|
+
##
|
|
18
|
+
# Grouped values resolved at the start of an analysis run
|
|
19
|
+
AnalysisContext = Struct.new(:timestamp, :target, :changes, :review_id, :check_classes, keyword_init: true) do
|
|
20
|
+
##
|
|
21
|
+
# @return [Hash] run construction attributes derived from this context
|
|
22
|
+
def run_attributes
|
|
23
|
+
{
|
|
24
|
+
timestamp: timestamp,
|
|
25
|
+
review_id: review_id,
|
|
26
|
+
checks: check_classes.map(&:check_name),
|
|
27
|
+
target_files: target.files,
|
|
28
|
+
ruby_files: target.ruby_files,
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @param [Options::ParseResult] options parsed command-line options
|
|
35
|
+
# @param [CommandRunner] command_runner for executing shell commands
|
|
36
|
+
# @param [#now] clock time source for timestamps
|
|
37
|
+
# @param [CheckRegistry] check_registry registry for resolving check names
|
|
38
|
+
def initialize(options:, command_runner: CommandRunner.new, clock: Time, check_registry: Checks)
|
|
39
|
+
@options = options
|
|
40
|
+
@command_runner = command_runner
|
|
41
|
+
@clock = clock
|
|
42
|
+
@check_registry = check_registry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Execute the quality review
|
|
47
|
+
# @return [Run] results of the quality review
|
|
48
|
+
def run
|
|
49
|
+
context = analysis_context
|
|
50
|
+
artifacts = prepare_artifacts(context)
|
|
51
|
+
return reusable_run(artifacts) if artifacts.complete?
|
|
52
|
+
|
|
53
|
+
execute_fresh_run(context, artifacts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :options, :command_runner, :clock, :check_registry
|
|
59
|
+
|
|
60
|
+
def epoch_milliseconds
|
|
61
|
+
(clock.now.to_r * 1_000).to_i
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def analysis_context
|
|
65
|
+
timestamp = epoch_milliseconds
|
|
66
|
+
target = resolve_target
|
|
67
|
+
changes = changes_diff(target)
|
|
68
|
+
check_classes = resolve_checks
|
|
69
|
+
|
|
70
|
+
AnalysisContext.new(
|
|
71
|
+
timestamp: timestamp,
|
|
72
|
+
target: target,
|
|
73
|
+
changes: changes,
|
|
74
|
+
review_id: review_id_for(changes, check_classes),
|
|
75
|
+
check_classes: check_classes,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_target
|
|
80
|
+
files = options.files
|
|
81
|
+
changed = options.changed || files.empty?
|
|
82
|
+
TargetResolver.new(command_runner: command_runner).resolve(files, changed: changed)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def changes_diff(target)
|
|
86
|
+
ChangesDiff.new(target_files: target.files, command_runner: command_runner)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def prepare_artifacts(context)
|
|
90
|
+
RunArtifacts.new(
|
|
91
|
+
timestamp: context.timestamp,
|
|
92
|
+
review_id: context.review_id,
|
|
93
|
+
target_files: context.target.files,
|
|
94
|
+
changes_diff: context.changes.to_s,
|
|
95
|
+
).prepare!
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reusable_run(artifacts)
|
|
99
|
+
artifacts.to_run(format: options.format, log: options.log)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def execute_fresh_run(context, artifacts)
|
|
103
|
+
check_outputs = run_checks(context.check_classes, context.target.ruby_files, context.timestamp)
|
|
104
|
+
write_check_outputs(artifacts, check_outputs)
|
|
105
|
+
run = build_run(context, artifacts, check_outputs)
|
|
106
|
+
persist_run(artifacts, run)
|
|
107
|
+
run
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def resolve_checks
|
|
111
|
+
all_checks = check_registry.resolve(options.checks)
|
|
112
|
+
filter_excluded_checks(all_checks, options.exclude)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def filter_excluded_checks(checks, excluded)
|
|
116
|
+
return checks if excluded.empty?
|
|
117
|
+
|
|
118
|
+
excluded_names = excluded.map(&:downcase)
|
|
119
|
+
checks.reject { |check| excluded_names.include?(check.check_name.downcase) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run_checks(check_classes, ruby_files, timestamp)
|
|
123
|
+
check_classes.map do |check_class|
|
|
124
|
+
check_class.new(command_runner: command_runner, timestamp: timestamp).run(ruby_files)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def write_check_outputs(artifacts, check_outputs)
|
|
129
|
+
check_outputs.each do |output|
|
|
130
|
+
artifacts.write_check_output(output)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_run(context, artifacts, check_outputs)
|
|
135
|
+
Run.new(
|
|
136
|
+
**context.run_attributes,
|
|
137
|
+
format: options.format,
|
|
138
|
+
run_directory: artifacts.to_s,
|
|
139
|
+
results: check_outputs.flat_map(&:results),
|
|
140
|
+
artifacts: artifacts,
|
|
141
|
+
log: options.log,
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def persist_run(artifacts, run)
|
|
146
|
+
artifacts.write_run(run)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def review_id_for(changes, check_classes)
|
|
150
|
+
Digest::SHA256.hexdigest(
|
|
151
|
+
JSON.generate(
|
|
152
|
+
diff: changes.to_s,
|
|
153
|
+
checks: check_classes.map(&:check_name).sort,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration"
|
|
4
|
+
|
|
5
|
+
module CleoQualityReview
|
|
6
|
+
##
|
|
7
|
+
# Resolves target files for quality review based on git changes and configuration
|
|
8
|
+
class TargetResolver
|
|
9
|
+
BASE_REF = "origin/main"
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# Value object containing resolved file lists
|
|
13
|
+
#
|
|
14
|
+
# @!attribute [r] files
|
|
15
|
+
# @return [Array<String>] all target file paths
|
|
16
|
+
# @!attribute [r] ruby_files
|
|
17
|
+
# @return [Array<String>] Ruby file paths
|
|
18
|
+
Target = Struct.new(:files, :ruby_files, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @param [CommandRunner] command_runner for executing git commands
|
|
22
|
+
# @param [Configuration] configuration file filtering configuration
|
|
23
|
+
def initialize(command_runner:, configuration: Configuration.load)
|
|
24
|
+
@command_runner = command_runner
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Resolve target files for quality review
|
|
30
|
+
# @param [Array<String>] files explicit file paths
|
|
31
|
+
# @param [Boolean] changed when true, filter to git-changed files only
|
|
32
|
+
# @return [Target]
|
|
33
|
+
def resolve(files, changed: false)
|
|
34
|
+
target_files = resolve_target_files(files, changed: changed)
|
|
35
|
+
|
|
36
|
+
Target.new(
|
|
37
|
+
files: target_files,
|
|
38
|
+
ruby_files: target_files,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :command_runner, :configuration
|
|
45
|
+
|
|
46
|
+
def resolve_target_files(files, changed:)
|
|
47
|
+
candidates = resolve_candidates(files, changed: changed)
|
|
48
|
+
filter_to_configured(candidates)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_candidates(files, changed:)
|
|
52
|
+
return changed_files if files.empty?
|
|
53
|
+
|
|
54
|
+
resolve_explicit_paths(files, changed: changed)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_explicit_paths(files, changed:)
|
|
58
|
+
expanded = expand_target_paths(files)
|
|
59
|
+
changed ? filter_to_changed(expanded) : expanded
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def filter_to_configured(candidates)
|
|
63
|
+
candidates.select { |path| File.file?(path) && configuration.target_file?(path) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def filter_to_changed(paths)
|
|
67
|
+
git_changed = changed_files
|
|
68
|
+
paths.select { |path| git_changed.include?(path) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def changed_files
|
|
72
|
+
(tracked_changed_files + untracked_files).uniq
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def tracked_changed_files
|
|
76
|
+
result = command_runner.run(
|
|
77
|
+
"git",
|
|
78
|
+
"diff",
|
|
79
|
+
"--name-only",
|
|
80
|
+
"--diff-filter=ACMRT",
|
|
81
|
+
diff_base,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result.stdout.lines.map(&:strip)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def untracked_files
|
|
88
|
+
result = command_runner.run("git", "ls-files", "--others", "--exclude-standard")
|
|
89
|
+
|
|
90
|
+
result.stdout.lines.map(&:strip)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def diff_base
|
|
94
|
+
@diff_base ||= begin
|
|
95
|
+
result = command_runner.run("git", "merge-base", BASE_REF, "HEAD")
|
|
96
|
+
base = result.stdout.strip
|
|
97
|
+
|
|
98
|
+
result.success? && !base.empty? ? base : BASE_REF
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def expand_target_paths(paths)
|
|
103
|
+
cleaned_paths = paths.map(&:to_s).map(&:strip).reject(&:empty?)
|
|
104
|
+
files, directories, missing = classify_paths(cleaned_paths)
|
|
105
|
+
raise ArgumentError, "Path not found: #{missing.join(', ')}" unless missing.empty?
|
|
106
|
+
|
|
107
|
+
(files + directories.flat_map { |path| directory_files(path) }).uniq
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def classify_paths(paths)
|
|
111
|
+
paths.group_by { |path| path_type(path) }.values_at(:file, :directory, :missing).map { |v| v || [] }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def path_type(path)
|
|
115
|
+
return :file if File.file?(path)
|
|
116
|
+
return :directory if File.directory?(path)
|
|
117
|
+
|
|
118
|
+
:missing
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def directory_files(path)
|
|
122
|
+
Dir.glob(File.join(path, "**", "*"), File::FNM_DOTMATCH).sort.select do |expanded_path|
|
|
123
|
+
File.file?(expanded_path)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Quality review tool for Ruby code analysis
|
|
5
|
+
module CleoQualityReview
|
|
6
|
+
class Error < StandardError;end
|
|
7
|
+
|
|
8
|
+
require_relative "cleo_quality_review/version"
|
|
9
|
+
require_relative "cleo_quality_review/checks"
|
|
10
|
+
require_relative "cleo_quality_review/llm_providers"
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Register all supported tools for analysing code here
|
|
14
|
+
Checks.register("Reek", Checks::Reek, tool_type: :smell_detection)
|
|
15
|
+
Checks.register("Flog", Checks::Flog, tool_type: :complexity)
|
|
16
|
+
Checks.register("Fasterer", Checks::Fasterer, tool_type: :performance)
|
|
17
|
+
Checks.register("Debride", Checks::Debride, tool_type: :dead_code)
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Register all supported LLM APIs for formatting output here
|
|
21
|
+
LlmProviders.register("openai", LlmProviders::OpenAi::Provider)
|
|
22
|
+
LlmProviders.register("stub", LlmProviders::Stub::Provider)
|
|
23
|
+
end
|
data/prompts/agent.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
You are reviewing Ruby code quality findings for consumption by AI coding assistants.
|
|
2
|
+
|
|
3
|
+
Analyze the raw tool outputs and git diff provided. Prioritize actionable issues affecting maintainability, readability, performance, and complexity. Filter out low-signal findings.
|
|
4
|
+
|
|
5
|
+
## Tool Thresholds
|
|
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.
|
|
11
|
+
|
|
12
|
+
## Output Format
|
|
13
|
+
|
|
14
|
+
Output valid JSON matching this exact schema:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"run": {
|
|
19
|
+
"timestamp": <integer from metadata>,
|
|
20
|
+
"checks": [<check names from metadata>],
|
|
21
|
+
"target_files": [<file paths from metadata>],
|
|
22
|
+
"findings": [
|
|
23
|
+
{
|
|
24
|
+
"tool_name": "<reek|flog|fasterer|debride>",
|
|
25
|
+
"tool_type": "<smell_detection|complexity|performance|dead_code>",
|
|
26
|
+
"check": "<specific check type>",
|
|
27
|
+
"filepath": "<relative file path>",
|
|
28
|
+
"line": <line number or null>,
|
|
29
|
+
"result": "<concise description of the issue>"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"check_outputs": [
|
|
34
|
+
{
|
|
35
|
+
"check_name": "<check name>",
|
|
36
|
+
"tool_name": "<reek|flog|fasterer|debride>",
|
|
37
|
+
"tool_type": "<smell_detection|complexity|performance|dead_code>",
|
|
38
|
+
"extension": "<json|txt>",
|
|
39
|
+
"path": "<raw output artifact path>",
|
|
40
|
+
"raw_output": "<raw tool output>"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"instructions": "Prioritized code quality findings for automated remediation."
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Guidelines
|
|
48
|
+
|
|
49
|
+
1. Include only findings that exceed thresholds and are actionable
|
|
50
|
+
2. Order findings by priority: high-complexity methods first, then code smells, then performance
|
|
51
|
+
3. Write concise `result` descriptions an agent can act on
|
|
52
|
+
4. Include the raw check outputs in `check_outputs` for reference
|
|
53
|
+
5. Output ONLY valid JSON - no markdown fences, no explanatory text
|
data/prompts/github.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
You are the pipeline interface between a series of code reviews for a git diff, and the GitHub Actions automation pipeline.
|
|
2
|
+
|
|
3
|
+
You will collate data about code from multiple code sources (including, but not limited to Flog, Flay, Reek, Fasterer, Brakeman, etc.), and produce useful, meaningful output for the engineer whose PR has triggered this flow.
|
|
4
|
+
|
|
5
|
+
The output from all of these reports together is very noisy, and so your role is to determine what is the most important things to report back on the PR, and what items can be disregarded.
|
|
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.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
You MUST NOT return so many items that the feedback is noisy and confusing. Limit yourself to maximum 10 comments.
|
|
26
|
+
|
|
27
|
+
You MUST return your feedback in the Github Workflow Annotations format, as described on their website. (You can use this link as a source if you need to: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands).
|
|
28
|
+
|
|
29
|
+
You SHOULD group feedback from the various tools in the Github workflow logs using the `::group::{title}` ...`::endgroup::` notation.
|
data/prompts/human.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
You are reviewing a local code change for code quality.
|
|
2
|
+
|
|
3
|
+
The files provided include git diffs for local code changes, as well as generated output files from various code quality assessment tools including (but not limited to) Reek, Flog, Fasterer, Debride, etc.
|
|
4
|
+
|
|
5
|
+
Your task is to parse the static output files generated by these tools, and provide feedback to the human user. The diff provided is to allow you to map tool output to changes in the code.
|
|
6
|
+
|
|
7
|
+
YOU MUST NOT comment on the code diff itself, unless the comment is in relatinon to an issue reported by a tool.
|
|
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.
|
|
18
|
+
|
|
19
|
+
1. Highest-impact issues first, with file and line references as clickable links when available.
|
|
20
|
+
2. Suggested changes that are specific enough for an engineer or coding agent to implement.
|
|
21
|
+
3. A short note if the automated checks found no meaningful issues.
|
|
22
|
+
|
|
23
|
+
Finish your feedback with a short note (one or two sentences) that begin with: `Summary:`, and then summarise the report findings.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
You are the pipeline interface between code quality tools and GitHub pull request review comments.
|
|
2
|
+
|
|
3
|
+
You will collate data from code quality tools including Reek, Flog, Fasterer, and Debride. The raw output is noisy, so your job is to identify only the most useful comments for the engineer whose PR triggered this flow.
|
|
4
|
+
|
|
5
|
+
You MUST NOT comment on the code diff itself unless the comment is directly supported by a tool finding.
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Comment Selection
|
|
15
|
+
|
|
16
|
+
1. Limit yourself to ten comments at most.
|
|
17
|
+
2. Prefer findings that map directly to a changed or commentable right-side line in the git diff.
|
|
18
|
+
3. Omit low-value, duplicated, stale, or ambiguous findings.
|
|
19
|
+
4. If a tool finding points to a file or line that is not visible in the provided diff, omit the inline comment.
|
|
20
|
+
5. Keep comments concise and actionable. Mention the tool and check name.
|
|
21
|
+
|
|
22
|
+
## Output Format
|
|
23
|
+
|
|
24
|
+
Output ONLY valid JSON. Do not wrap it in markdown fences. Do not include explanatory text before or after the JSON.
|
|
25
|
+
|
|
26
|
+
The JSON MUST match this schema:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"body": "<short markdown summary for the PR review body>",
|
|
31
|
+
"comments": [
|
|
32
|
+
{
|
|
33
|
+
"path": "<repository-relative file path>",
|
|
34
|
+
"line": <right-side line number from the diff>,
|
|
35
|
+
"body": "<markdown review comment>"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Comment format:
|
|
43
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
Example format:
|
|
47
|
+
```
|
|
48
|
+
This code appears to have X issue. That may be likely to cause Y problem. Consider an alternative soltion, such as Z.
|
|
49
|
+
|
|
50
|
+
_(Ref: Reek TooManyStatements, DuplicateMethodCall; Fasterer HashKeysEach)_
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Empty output:
|
|
54
|
+
|
|
55
|
+
If there are no high-confidence inline comments, return:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"body": "Cleo quality review did not find any high-confidence issues worth inline PR comments.",
|
|
60
|
+
"comments": []
|
|
61
|
+
}
|
|
62
|
+
```
|
metadata
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cleo_quality_review
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gavin Morrice
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: debride
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fasterer
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: flog
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: reek
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: Runs local quality checks and summarizes their output for humans, agents,
|
|
69
|
+
or GitHub.
|
|
70
|
+
email:
|
|
71
|
+
- gavin@gavinmorrice.com
|
|
72
|
+
executables:
|
|
73
|
+
- check_quality
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- cleo_quality_review.gemspec
|
|
78
|
+
- config/default.yml
|
|
79
|
+
- exe/check_quality
|
|
80
|
+
- lib/cleo_quality_review.rb
|
|
81
|
+
- lib/cleo_quality_review/changes_diff.rb
|
|
82
|
+
- lib/cleo_quality_review/checks.rb
|
|
83
|
+
- lib/cleo_quality_review/checks/debride.rb
|
|
84
|
+
- lib/cleo_quality_review/checks/fasterer.rb
|
|
85
|
+
- lib/cleo_quality_review/checks/flog.rb
|
|
86
|
+
- lib/cleo_quality_review/checks/quality_check.rb
|
|
87
|
+
- lib/cleo_quality_review/checks/reek.rb
|
|
88
|
+
- lib/cleo_quality_review/checks/registry.rb
|
|
89
|
+
- lib/cleo_quality_review/cli.rb
|
|
90
|
+
- lib/cleo_quality_review/command_result.rb
|
|
91
|
+
- lib/cleo_quality_review/command_runner.rb
|
|
92
|
+
- lib/cleo_quality_review/configuration.rb
|
|
93
|
+
- lib/cleo_quality_review/diff_map.rb
|
|
94
|
+
- lib/cleo_quality_review/formatter.rb
|
|
95
|
+
- lib/cleo_quality_review/github_review_builder.rb
|
|
96
|
+
- lib/cleo_quality_review/github_review_publisher.rb
|
|
97
|
+
- lib/cleo_quality_review/llm_client.rb
|
|
98
|
+
- lib/cleo_quality_review/llm_config.rb
|
|
99
|
+
- lib/cleo_quality_review/llm_errors.rb
|
|
100
|
+
- lib/cleo_quality_review/llm_logger.rb
|
|
101
|
+
- lib/cleo_quality_review/llm_providers.rb
|
|
102
|
+
- lib/cleo_quality_review/llm_providers/open_ai.rb
|
|
103
|
+
- lib/cleo_quality_review/llm_providers/open_ai_config.rb
|
|
104
|
+
- lib/cleo_quality_review/llm_providers/registry.rb
|
|
105
|
+
- lib/cleo_quality_review/llm_providers/stub.rb
|
|
106
|
+
- lib/cleo_quality_review/options.rb
|
|
107
|
+
- lib/cleo_quality_review/prompt_builder.rb
|
|
108
|
+
- lib/cleo_quality_review/prompt_loader.rb
|
|
109
|
+
- lib/cleo_quality_review/result.rb
|
|
110
|
+
- lib/cleo_quality_review/run.rb
|
|
111
|
+
- lib/cleo_quality_review/run_artifacts.rb
|
|
112
|
+
- lib/cleo_quality_review/run_artifacts/raw_check_outputs.rb
|
|
113
|
+
- lib/cleo_quality_review/runner.rb
|
|
114
|
+
- lib/cleo_quality_review/target_resolver.rb
|
|
115
|
+
- lib/cleo_quality_review/version.rb
|
|
116
|
+
- prompts/agent.md
|
|
117
|
+
- prompts/github.md
|
|
118
|
+
- prompts/human.md
|
|
119
|
+
- prompts/pr_review.md
|
|
120
|
+
licenses:
|
|
121
|
+
- MIT
|
|
122
|
+
metadata:
|
|
123
|
+
rubygems_mfa_required: 'true'
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 3.2.0
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 4.0.10
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: Local Cleo code quality checks
|
|
141
|
+
test_files: []
|