code_sage 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/.github/workflows/gem.yml +47 -0
- data/.gitignore +54 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +150 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/Rakefile +20 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/code_sage.gemspec +39 -0
- data/exe/code_sage +6 -0
- data/lib/code_sage/cli.rb +161 -0
- data/lib/code_sage/config.rb +87 -0
- data/lib/code_sage/git_analyzer.rb +183 -0
- data/lib/code_sage/report_formatter.rb +171 -0
- data/lib/code_sage/reviewer.rb +226 -0
- data/lib/code_sage/version.rb +3 -0
- data/lib/code_sage.rb +15 -0
- metadata +179 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module CodeSage
|
5
|
+
class CLI < Thor
|
6
|
+
desc "review", "Review code changes in current repository"
|
7
|
+
option :branch, aliases: '-b', desc: "Branch to compare against (default: main)"
|
8
|
+
option :files, aliases: '-f', type: :array, desc: "Specific files to review"
|
9
|
+
option :format, aliases: '--format', default: 'console', desc: "Output format (console, json, markdown)"
|
10
|
+
option :config, aliases: '-c', desc: "Path to configuration file"
|
11
|
+
option :verbose, aliases: '-v', type: :boolean, desc: "Verbose output"
|
12
|
+
option :rag, type: :boolean,
|
13
|
+
desc: "Enable RAG (Retrieval Augmented Generation) functionality (requires vector database)"
|
14
|
+
def review
|
15
|
+
puts "š® CodeSage - Wisdom for your code".colorize(:cyan)
|
16
|
+
puts
|
17
|
+
|
18
|
+
begin
|
19
|
+
reviewer = Reviewer.new(
|
20
|
+
branch: options[:branch] || 'main',
|
21
|
+
files: options[:files],
|
22
|
+
format: options[:format],
|
23
|
+
config_path: options[:config],
|
24
|
+
verbose: options[:verbose],
|
25
|
+
enable_rag: options[:rag] || false
|
26
|
+
)
|
27
|
+
|
28
|
+
result = reviewer.review
|
29
|
+
|
30
|
+
if result[:success]
|
31
|
+
puts "ā
Code review completed successfully!".colorize(:green)
|
32
|
+
else
|
33
|
+
puts "ā Code review failed: #{result[:error]}".colorize(:red)
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
|
37
|
+
rescue => e
|
38
|
+
puts "š„ Error: #{e.message}".colorize(:red)
|
39
|
+
puts e.backtrace.join("\n").colorize(:yellow) if options[:verbose]
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "config", "Show or set configuration"
|
45
|
+
option :show, type: :boolean, desc: "Show current configuration"
|
46
|
+
option :reset, type: :boolean, desc: "Reset configuration to defaults"
|
47
|
+
option :key, type: :string, desc: "Configuration key to set"
|
48
|
+
option :value, type: :string, desc: "Configuration value to set"
|
49
|
+
def config
|
50
|
+
config_instance = Config.new
|
51
|
+
|
52
|
+
if options[:show]
|
53
|
+
puts "š CodeSage Configuration".colorize(:cyan).bold
|
54
|
+
puts "=" * 50
|
55
|
+
puts YAML.dump(config_instance.data)
|
56
|
+
elsif options[:reset]
|
57
|
+
config_instance.reset!
|
58
|
+
puts "ā
Configuration reset to defaults".colorize(:green)
|
59
|
+
elsif options[:key] && options[:value]
|
60
|
+
config_instance.set(options[:key], options[:value])
|
61
|
+
config_instance.save!
|
62
|
+
puts "ā
Configuration updated: #{options[:key]} = #{options[:value]}".colorize(:green)
|
63
|
+
else
|
64
|
+
puts "š Current configuration file: #{config_instance.config_path}".colorize(:cyan)
|
65
|
+
puts "Use --show to display configuration"
|
66
|
+
puts "Use --key KEY --value VALUE to update settings"
|
67
|
+
puts "Use --reset to restore defaults"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "diagnose", "Run system diagnostics"
|
72
|
+
def diagnose
|
73
|
+
puts "š CodeSage System Diagnostics".colorize(:cyan).bold
|
74
|
+
puts "=" * 50
|
75
|
+
|
76
|
+
# Check Ruby version
|
77
|
+
print "Ruby: "
|
78
|
+
puts "ā
(#{RUBY_VERSION})".colorize(:green)
|
79
|
+
|
80
|
+
# Check Git
|
81
|
+
print "Git: "
|
82
|
+
begin
|
83
|
+
git_version = `git --version 2>/dev/null`.strip
|
84
|
+
if $?.success?
|
85
|
+
puts "ā
(#{git_version})".colorize(:green)
|
86
|
+
else
|
87
|
+
puts "ā Not found".colorize(:red)
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
puts "ā Not found".colorize(:red)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check llm_chain availability
|
94
|
+
print "LLMChain: "
|
95
|
+
begin
|
96
|
+
require 'llm_chain'
|
97
|
+
puts "ā
Available".colorize(:green)
|
98
|
+
|
99
|
+
# Run llm_chain diagnostics if available
|
100
|
+
if defined?(LLMChain) && LLMChain.respond_to?(:diagnose_system)
|
101
|
+
puts "\nš LLMChain Diagnostics:".colorize(:yellow)
|
102
|
+
LLMChain.diagnose_system
|
103
|
+
end
|
104
|
+
rescue LoadError
|
105
|
+
puts "ā Not available".colorize(:red)
|
106
|
+
puts " Run: gem install llm_chain".colorize(:yellow)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check API Keys
|
110
|
+
puts "\nš API Keys:".colorize(:yellow)
|
111
|
+
|
112
|
+
api_keys = {
|
113
|
+
'OpenAI' => ENV['OPENAI_API_KEY'],
|
114
|
+
'Google' => ENV['GOOGLE_API_KEY'],
|
115
|
+
'Anthropic' => ENV['ANTHROPIC_API_KEY']
|
116
|
+
}
|
117
|
+
|
118
|
+
api_keys.each do |name, key|
|
119
|
+
print "#{name}: "
|
120
|
+
if key && !key.empty?
|
121
|
+
puts "ā
Configured".colorize(:green)
|
122
|
+
else
|
123
|
+
puts "ā Not set".colorize(:red)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check configuration
|
128
|
+
puts "\nāļø Configuration:".colorize(:yellow)
|
129
|
+
config = Config.new
|
130
|
+
llm_config = config.data['llm']
|
131
|
+
|
132
|
+
print "LLM Provider: "
|
133
|
+
puts "#{llm_config['provider'] || 'openai'}".colorize(:cyan)
|
134
|
+
|
135
|
+
print "Model: "
|
136
|
+
puts "#{llm_config['model'] || 'gpt-4'}".colorize(:cyan)
|
137
|
+
|
138
|
+
puts "\nš” Recommendations:".colorize(:yellow)
|
139
|
+
recommendations = []
|
140
|
+
|
141
|
+
recommendations << "⢠Configure API keys for your chosen LLM provider" unless api_keys.values.any? do |key|
|
142
|
+
key && !key.empty?
|
143
|
+
end
|
144
|
+
if llm_config['provider'] == 'ollama'
|
145
|
+
recommendations << "⢠Install and start Ollama for local models: ollama serve"
|
146
|
+
end
|
147
|
+
recommendations << "⢠Ensure you're in a Git repository for code review" unless Dir.exist?('.git')
|
148
|
+
|
149
|
+
if recommendations.empty?
|
150
|
+
puts "ā
System looks good!".colorize(:green)
|
151
|
+
else
|
152
|
+
recommendations.each { |rec| puts rec }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
desc "version", "Show version"
|
157
|
+
def version
|
158
|
+
puts "CodeSage version #{CodeSage::VERSION}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module CodeSage
|
4
|
+
class Config
|
5
|
+
DEFAULT_CONFIG = {
|
6
|
+
'llm' => {
|
7
|
+
'provider' => 'openai', # openai, ollama, qwen, gemini
|
8
|
+
'model' => 'gpt-4',
|
9
|
+
'temperature' => 0.1,
|
10
|
+
'max_tokens' => 2000,
|
11
|
+
'api_key' => nil # Will use ENV variables
|
12
|
+
},
|
13
|
+
'git' => {
|
14
|
+
'default_branch' => 'main',
|
15
|
+
'include_patterns' => ['*.rb', '*.rake', 'Gemfile', 'Rakefile'],
|
16
|
+
'exclude_patterns' => ['spec/**/*', 'test/**/*']
|
17
|
+
},
|
18
|
+
'review' => {
|
19
|
+
'focus_areas' => ['security', 'performance', 'maintainability', 'best_practices'],
|
20
|
+
'severity_levels' => ['low', 'medium', 'high', 'critical']
|
21
|
+
},
|
22
|
+
'output' => {
|
23
|
+
'format' => 'console',
|
24
|
+
'verbose' => false,
|
25
|
+
'colors' => true
|
26
|
+
}
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
attr_reader :config_path, :data
|
30
|
+
|
31
|
+
def initialize(config_path = nil)
|
32
|
+
@config_path = config_path || default_config_path
|
33
|
+
@data = load_config
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(key_path)
|
37
|
+
keys = key_path.split('.')
|
38
|
+
keys.reduce(@data) { |config, key| config&.dig(key) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def set(key_path, value)
|
42
|
+
keys = key_path.split('.')
|
43
|
+
last_key = keys.pop
|
44
|
+
target = keys.reduce(@data) { |config, key| config[key] ||= {} }
|
45
|
+
target[last_key] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def save!
|
49
|
+
File.write(@config_path, YAML.dump(@data))
|
50
|
+
end
|
51
|
+
|
52
|
+
def reset!
|
53
|
+
@data = DEFAULT_CONFIG.dup
|
54
|
+
save!
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_config_path
|
60
|
+
File.expand_path('~/.code_sage.yml')
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_config
|
64
|
+
if File.exist?(@config_path)
|
65
|
+
loaded = YAML.load_file(@config_path) || {}
|
66
|
+
deep_merge(DEFAULT_CONFIG, loaded)
|
67
|
+
else
|
68
|
+
DEFAULT_CONFIG.dup
|
69
|
+
end
|
70
|
+
rescue => e
|
71
|
+
puts "Warning: Could not load config file #{@config_path}: #{e.message}".colorize(:yellow)
|
72
|
+
DEFAULT_CONFIG.dup
|
73
|
+
end
|
74
|
+
|
75
|
+
def deep_merge(hash1, hash2)
|
76
|
+
result = hash1.dup
|
77
|
+
hash2.each do |key, value|
|
78
|
+
if result[key].is_a?(Hash) && value.is_a?(Hash)
|
79
|
+
result[key] = deep_merge(result[key], value)
|
80
|
+
else
|
81
|
+
result[key] = value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'rugged'
|
2
|
+
|
3
|
+
module CodeSage
|
4
|
+
class GitAnalyzer
|
5
|
+
attr_reader :repo_path, :options
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options
|
9
|
+
@repo_path = Dir.pwd
|
10
|
+
@repo = Rugged::Repository.new(@repo_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_changes
|
14
|
+
if @options[:files]
|
15
|
+
analyze_specific_files(@options[:files])
|
16
|
+
else
|
17
|
+
analyze_branch_changes(@options[:branch])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def analyze_specific_files(files)
|
24
|
+
changes = []
|
25
|
+
|
26
|
+
files.each do |file|
|
27
|
+
next unless File.exist?(file)
|
28
|
+
next unless ruby_file?(file)
|
29
|
+
|
30
|
+
change = analyze_file(file)
|
31
|
+
changes << change if change
|
32
|
+
end
|
33
|
+
|
34
|
+
changes
|
35
|
+
end
|
36
|
+
|
37
|
+
def analyze_branch_changes(target_branch)
|
38
|
+
changes = []
|
39
|
+
|
40
|
+
begin
|
41
|
+
# Get current HEAD
|
42
|
+
head = @repo.head.target
|
43
|
+
|
44
|
+
# Get target branch
|
45
|
+
target = @repo.branches[target_branch]&.target || @repo.branches["origin/#{target_branch}"]&.target
|
46
|
+
|
47
|
+
unless target
|
48
|
+
# If target branch doesn't exist, analyze staged changes
|
49
|
+
return analyze_staged_changes
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get diff between branches
|
53
|
+
diff = @repo.diff(target, head)
|
54
|
+
|
55
|
+
diff.each_delta do |delta|
|
56
|
+
file_path = delta.new_file[:path]
|
57
|
+
next unless ruby_file?(file_path)
|
58
|
+
|
59
|
+
change = {
|
60
|
+
file: file_path,
|
61
|
+
type: delta.status.to_s,
|
62
|
+
diff: get_file_diff(delta),
|
63
|
+
content: get_file_content(file_path),
|
64
|
+
lines_added: 0,
|
65
|
+
lines_removed: 0
|
66
|
+
}
|
67
|
+
|
68
|
+
# Count lines
|
69
|
+
patch = diff.patch(delta)
|
70
|
+
if patch
|
71
|
+
change[:lines_added] = patch.scan(/^\+/).length
|
72
|
+
change[:lines_removed] = patch.scan(/^-/).length
|
73
|
+
end
|
74
|
+
|
75
|
+
changes << change
|
76
|
+
end
|
77
|
+
|
78
|
+
rescue => e
|
79
|
+
puts "Error analyzing git changes: #{e.message}".colorize(:red) if @options[:verbose]
|
80
|
+
# Fallback to staged changes
|
81
|
+
return analyze_staged_changes
|
82
|
+
end
|
83
|
+
|
84
|
+
changes
|
85
|
+
end
|
86
|
+
|
87
|
+
def analyze_staged_changes
|
88
|
+
changes = []
|
89
|
+
|
90
|
+
# Get staged changes
|
91
|
+
index = @repo.index
|
92
|
+
diff = @repo.diff_index_to_workdir
|
93
|
+
|
94
|
+
diff.each_delta do |delta|
|
95
|
+
file_path = delta.new_file[:path]
|
96
|
+
next unless ruby_file?(file_path)
|
97
|
+
|
98
|
+
change = {
|
99
|
+
file: file_path,
|
100
|
+
type: delta.status.to_s,
|
101
|
+
diff: get_file_diff(delta),
|
102
|
+
content: get_file_content(file_path),
|
103
|
+
lines_added: 0,
|
104
|
+
lines_removed: 0
|
105
|
+
}
|
106
|
+
|
107
|
+
# Count lines from patch
|
108
|
+
patch = diff.patch(delta)
|
109
|
+
if patch
|
110
|
+
change[:lines_added] = patch.scan(/^\+/).length
|
111
|
+
change[:lines_removed] = patch.scan(/^-/).length
|
112
|
+
end
|
113
|
+
|
114
|
+
changes << change
|
115
|
+
end
|
116
|
+
|
117
|
+
changes
|
118
|
+
rescue => e
|
119
|
+
puts "Error analyzing staged changes: #{e.message}".colorize(:red) if @options[:verbose]
|
120
|
+
[]
|
121
|
+
end
|
122
|
+
|
123
|
+
def analyze_file(file_path)
|
124
|
+
return nil unless File.exist?(file_path)
|
125
|
+
|
126
|
+
{
|
127
|
+
file: file_path,
|
128
|
+
type: 'modified',
|
129
|
+
diff: get_simple_diff(file_path),
|
130
|
+
content: get_file_content(file_path),
|
131
|
+
lines_added: 0,
|
132
|
+
lines_removed: 0
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_file_diff(delta)
|
137
|
+
# Generate diff for the file
|
138
|
+
begin
|
139
|
+
patch = @repo.diff(delta.old_file[:oid], delta.new_file[:oid]).patch
|
140
|
+
patch.to_s
|
141
|
+
rescue
|
142
|
+
"Diff not available"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_simple_diff(file_path)
|
147
|
+
# For single file analysis, return recent changes or full content
|
148
|
+
"Full file content (diff not available for single file analysis)"
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_file_content(file_path)
|
152
|
+
return nil unless File.exist?(file_path)
|
153
|
+
|
154
|
+
File.read(file_path)
|
155
|
+
rescue => e
|
156
|
+
puts "Error reading file #{file_path}: #{e.message}".colorize(:red) if @options[:verbose]
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
def ruby_file?(file_path)
|
161
|
+
return false unless file_path
|
162
|
+
|
163
|
+
ruby_extensions = %w[.rb .rake .gemspec]
|
164
|
+
ruby_files = %w[Gemfile Rakefile Guardfile]
|
165
|
+
|
166
|
+
# Check extension
|
167
|
+
return true if ruby_extensions.any? { |ext| file_path.end_with?(ext) }
|
168
|
+
|
169
|
+
# Check specific filenames
|
170
|
+
basename = File.basename(file_path)
|
171
|
+
return true if ruby_files.include?(basename)
|
172
|
+
|
173
|
+
# Check if file starts with ruby shebang
|
174
|
+
if File.exist?(file_path)
|
175
|
+
first_line = File.open(file_path, &:readline).strip rescue ""
|
176
|
+
return true if first_line.start_with?('#!/usr/bin/env ruby') ||
|
177
|
+
first_line.start_with?('#!/usr/bin/ruby')
|
178
|
+
end
|
179
|
+
|
180
|
+
false
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module CodeSage
|
5
|
+
class ReportFormatter
|
6
|
+
attr_reader :format
|
7
|
+
|
8
|
+
def initialize(format = 'console')
|
9
|
+
@format = format.to_s.downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def format(report)
|
13
|
+
case @format
|
14
|
+
when 'json'
|
15
|
+
format_json(report)
|
16
|
+
when 'markdown'
|
17
|
+
format_markdown(report)
|
18
|
+
else
|
19
|
+
format_console(report)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def format_console(report)
|
26
|
+
output = []
|
27
|
+
|
28
|
+
# Header
|
29
|
+
output << "=" * 80
|
30
|
+
output << "š® CodeSage Review Report".colorize(:cyan).bold
|
31
|
+
output << "Generated at: #{report[:generated_at]}"
|
32
|
+
output << "=" * 80
|
33
|
+
output << ""
|
34
|
+
|
35
|
+
# Summary
|
36
|
+
summary = report[:summary]
|
37
|
+
output << "š SUMMARY".colorize(:yellow).bold
|
38
|
+
output << "-" * 40
|
39
|
+
output << "Files reviewed: #{summary[:total_files_reviewed]}"
|
40
|
+
output << "Files with potential issues: #{summary[:files_with_issues]}"
|
41
|
+
output << "Total lines changed: #{summary[:total_lines_changed]}"
|
42
|
+
output << ""
|
43
|
+
|
44
|
+
# Metrics
|
45
|
+
if report[:metrics]
|
46
|
+
metrics = report[:metrics]
|
47
|
+
output << "š METRICS".colorize(:yellow).bold
|
48
|
+
output << "-" * 40
|
49
|
+
output << "Average lines per file: #{metrics[:avg_lines_per_file].round(1)}"
|
50
|
+
|
51
|
+
if metrics[:files_by_type].any?
|
52
|
+
output << "Files by change type:"
|
53
|
+
metrics[:files_by_type].each do |type, count|
|
54
|
+
output << " #{type}: #{count}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
output << ""
|
58
|
+
end
|
59
|
+
|
60
|
+
# Individual reviews
|
61
|
+
output << "š DETAILED REVIEWS".colorize(:yellow).bold
|
62
|
+
output << "-" * 80
|
63
|
+
|
64
|
+
report[:reviews].each_with_index do |review, index|
|
65
|
+
output << ""
|
66
|
+
output << "#{index + 1}. #{review[:file]}".colorize(:cyan).bold
|
67
|
+
output << " Type: #{review[:change_type]} | +#{review[:lines_added]} -#{review[:lines_removed]}"
|
68
|
+
output << " " + "-" * 70
|
69
|
+
|
70
|
+
# Format review content
|
71
|
+
review_lines = review[:review].split("\n")
|
72
|
+
review_lines.each do |line|
|
73
|
+
if line.match(/^(Issue|Problem|Warning):/i)
|
74
|
+
output << " #{line}".colorize(:red)
|
75
|
+
elsif line.match(/^(Suggestion|Recommendation):/i)
|
76
|
+
output << " #{line}".colorize(:yellow)
|
77
|
+
elsif line.match(/^(Good|Positive):/i)
|
78
|
+
output << " #{line}".colorize(:green)
|
79
|
+
else
|
80
|
+
output << " #{line}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Recommendations
|
86
|
+
if report[:recommendations]&.any?
|
87
|
+
output << ""
|
88
|
+
output << "š” RECOMMENDATIONS".colorize(:yellow).bold
|
89
|
+
output << "-" * 40
|
90
|
+
report[:recommendations].each_with_index do |rec, index|
|
91
|
+
output << "#{index + 1}. #{rec}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
output << ""
|
96
|
+
output << "=" * 80
|
97
|
+
|
98
|
+
output.join("\n")
|
99
|
+
end
|
100
|
+
|
101
|
+
def format_json(report)
|
102
|
+
JSON.pretty_generate(report)
|
103
|
+
end
|
104
|
+
|
105
|
+
def format_markdown(report)
|
106
|
+
output = []
|
107
|
+
|
108
|
+
# Header
|
109
|
+
output << "# š® CodeSage Review Report"
|
110
|
+
output << ""
|
111
|
+
output << "**Generated at:** #{report[:generated_at]}"
|
112
|
+
output << ""
|
113
|
+
|
114
|
+
# Summary
|
115
|
+
summary = report[:summary]
|
116
|
+
output << "## š Summary"
|
117
|
+
output << ""
|
118
|
+
output << "| Metric | Value |"
|
119
|
+
output << "|--------|-------|"
|
120
|
+
output << "| Files reviewed | #{summary[:total_files_reviewed]} |"
|
121
|
+
output << "| Files with potential issues | #{summary[:files_with_issues]} |"
|
122
|
+
output << "| Total lines changed | #{summary[:total_lines_changed]} |"
|
123
|
+
output << ""
|
124
|
+
|
125
|
+
# Metrics
|
126
|
+
if report[:metrics]
|
127
|
+
metrics = report[:metrics]
|
128
|
+
output << "## š Metrics"
|
129
|
+
output << ""
|
130
|
+
output << "- **Average lines per file:** #{metrics[:avg_lines_per_file].round(1)}"
|
131
|
+
|
132
|
+
if metrics[:files_by_type].any?
|
133
|
+
output << "- **Files by change type:**"
|
134
|
+
metrics[:files_by_type].each do |type, count|
|
135
|
+
output << " - #{type}: #{count}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
output << ""
|
139
|
+
end
|
140
|
+
|
141
|
+
# Individual reviews
|
142
|
+
output << "## š Detailed Reviews"
|
143
|
+
output << ""
|
144
|
+
|
145
|
+
report[:reviews].each_with_index do |review, index|
|
146
|
+
output << "### #{index + 1}. `#{review[:file]}`"
|
147
|
+
output << ""
|
148
|
+
change_info = "**Change Type:** #{review[:change_type]} | " \
|
149
|
+
"**Lines:** +#{review[:lines_added]} -#{review[:lines_removed]}"
|
150
|
+
output << change_info
|
151
|
+
output << ""
|
152
|
+
output << "```"
|
153
|
+
output << review[:review]
|
154
|
+
output << "```"
|
155
|
+
output << ""
|
156
|
+
end
|
157
|
+
|
158
|
+
# Recommendations
|
159
|
+
if report[:recommendations]&.any?
|
160
|
+
output << "## š” Recommendations"
|
161
|
+
output << ""
|
162
|
+
report[:recommendations].each_with_index do |rec, index|
|
163
|
+
output << "#{index + 1}. #{rec}"
|
164
|
+
end
|
165
|
+
output << ""
|
166
|
+
end
|
167
|
+
|
168
|
+
output.join("\n")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|