code_qualia 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/README.md +254 -0
- data/bin/code-qualia +12 -0
- data/bin/update_smoke_tests +107 -0
- data/lib/code_qualia/cli.rb +217 -0
- data/lib/code_qualia/complexity_analyzer.rb +115 -0
- data/lib/code_qualia/config.rb +83 -0
- data/lib/code_qualia/config_helper.rb +122 -0
- data/lib/code_qualia/config_installer.rb +170 -0
- data/lib/code_qualia/coverage_analyzer.rb +82 -0
- data/lib/code_qualia/git_analyzer.rb +69 -0
- data/lib/code_qualia/logger.rb +47 -0
- data/lib/code_qualia/pattern_expander.rb +118 -0
- data/lib/code_qualia/score_calculator.rb +143 -0
- data/lib/code_qualia.rb +68 -0
- metadata +91 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module CodeQualia
|
7
|
+
class PatternExpander
|
8
|
+
def self.expand_brace_patterns(pattern)
|
9
|
+
new.expand_brace_patterns(pattern)
|
10
|
+
end
|
11
|
+
|
12
|
+
def expand_brace_patterns(pattern)
|
13
|
+
parts = pattern.split('{', 2)
|
14
|
+
return [pattern] unless parts.length == 2
|
15
|
+
|
16
|
+
prefix = parts[0]
|
17
|
+
suffix_and_rest = parts[1].split('}', 2)
|
18
|
+
return [pattern] unless suffix_and_rest.length == 2
|
19
|
+
|
20
|
+
options = suffix_and_rest[0].split(',')
|
21
|
+
suffix = suffix_and_rest[1]
|
22
|
+
|
23
|
+
expanded = []
|
24
|
+
options.each do |option|
|
25
|
+
expanded_pattern = "#{prefix}#{option}#{suffix}"
|
26
|
+
if expanded_pattern.include?('{') && expanded_pattern.include?('}')
|
27
|
+
expanded.concat(expand_brace_patterns(expanded_pattern))
|
28
|
+
else
|
29
|
+
expanded << expanded_pattern
|
30
|
+
end
|
31
|
+
end
|
32
|
+
expanded
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.extract_base_directories(patterns)
|
36
|
+
new.extract_base_directories(patterns)
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_base_directories(patterns)
|
40
|
+
directories = Set.new
|
41
|
+
|
42
|
+
Array(patterns).each do |pattern|
|
43
|
+
expanded_patterns = if pattern.include?('{') && pattern.include?('}')
|
44
|
+
expand_brace_patterns(pattern)
|
45
|
+
else
|
46
|
+
[pattern]
|
47
|
+
end
|
48
|
+
|
49
|
+
expanded_patterns.each do |expanded_pattern|
|
50
|
+
base_dirs = extract_base_directory_from_pattern(expanded_pattern)
|
51
|
+
|
52
|
+
# Handle both single directory (string) and multiple directories (array)
|
53
|
+
base_dirs = Array(base_dirs)
|
54
|
+
base_dirs.each do |base_dir|
|
55
|
+
directories.add("#{base_dir}/") if base_dir && !base_dir.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
directories.to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def extract_base_directory_from_pattern(pattern)
|
66
|
+
if pattern.include?('**')
|
67
|
+
base_path = pattern.split('**').first.chomp('/')
|
68
|
+
|
69
|
+
# Handle special case of '**/*.rb' - detect common Ruby project directories
|
70
|
+
return detect_ruby_project_directories if base_path.empty? && pattern == '**/*.rb'
|
71
|
+
|
72
|
+
base_path
|
73
|
+
elsif pattern.include?('*')
|
74
|
+
# Handle patterns like 'app/models/*.rb' -> 'app'
|
75
|
+
pattern.split('*').first.chomp('/')
|
76
|
+
elsif File.directory?(pattern)
|
77
|
+
# Handle direct directory paths
|
78
|
+
pattern.chomp('/')
|
79
|
+
elsif pattern.end_with?('.rb')
|
80
|
+
# Handle direct file paths
|
81
|
+
dir = File.dirname(pattern)
|
82
|
+
dir == '.' ? nil : dir
|
83
|
+
else
|
84
|
+
# Assume it's a directory pattern
|
85
|
+
pattern.chomp('/')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def detect_ruby_project_directories
|
92
|
+
# Use Pathname.glob to find all Ruby files, then extract their top-level directories
|
93
|
+
ruby_files = Pathname.glob('**/*.rb')
|
94
|
+
|
95
|
+
# Extract unique top-level directories that contain Ruby files
|
96
|
+
ruby_files
|
97
|
+
.map { |file| extract_top_level_directory(file) } # Get top-level directory directly from file
|
98
|
+
.compact # Remove nils
|
99
|
+
.uniq # Remove duplicates
|
100
|
+
.map(&:to_s) # Convert back to strings
|
101
|
+
.sort # Sort for consistency
|
102
|
+
end
|
103
|
+
|
104
|
+
# Extract the top-level directory from a path
|
105
|
+
# Examples:
|
106
|
+
# app/models/user.rb -> app
|
107
|
+
# lib/my_gem/version.rb -> lib
|
108
|
+
# src/main.rb -> src
|
109
|
+
# script/console.rb -> script
|
110
|
+
# user.rb -> nil (root level file)
|
111
|
+
def extract_top_level_directory(pathname)
|
112
|
+
parts = pathname.each_filename.to_a
|
113
|
+
return nil if parts.length < 2 # Skip root-level files (need at least dir/file.rb)
|
114
|
+
|
115
|
+
parts.first
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'config_helper'
|
4
|
+
|
5
|
+
module CodeQualia
|
6
|
+
class ScoreCalculator
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def calculate(coverage_data:, complexity_data:, git_data:)
|
12
|
+
results = []
|
13
|
+
|
14
|
+
# Get target patterns from configuration using ConfigHelper
|
15
|
+
target_patterns = ConfigHelper.get_target_patterns
|
16
|
+
|
17
|
+
# Collect files from configured patterns and data sources
|
18
|
+
config_files = []
|
19
|
+
target_patterns.each do |pattern|
|
20
|
+
config_files.concat(Dir.glob(pattern))
|
21
|
+
end
|
22
|
+
|
23
|
+
all_files = (
|
24
|
+
config_files +
|
25
|
+
coverage_data.keys +
|
26
|
+
complexity_data.keys +
|
27
|
+
git_data.keys
|
28
|
+
).uniq
|
29
|
+
|
30
|
+
all_files.each do |file_path|
|
31
|
+
next if @config.excluded?(file_path)
|
32
|
+
|
33
|
+
file_methods = extract_methods_for_file(file_path, coverage_data, complexity_data, git_data)
|
34
|
+
results.concat(file_methods)
|
35
|
+
end
|
36
|
+
|
37
|
+
results.sort_by { |method| -method[:score] }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def extract_methods_for_file(file_path, coverage_data, complexity_data, git_data)
|
43
|
+
methods = []
|
44
|
+
|
45
|
+
# Get complexity data for this file
|
46
|
+
complexity_methods = complexity_data[file_path] || []
|
47
|
+
|
48
|
+
# If no complexity data, try to extract methods from file
|
49
|
+
complexity_methods = extract_methods_from_file(file_path) if complexity_methods.empty?
|
50
|
+
|
51
|
+
complexity_methods.each do |method_data|
|
52
|
+
method_info = {
|
53
|
+
file_path: file_path,
|
54
|
+
method_name: method_data[:method_name],
|
55
|
+
line_number: method_data[:line_number],
|
56
|
+
score: calculate_method_score(file_path, method_data, coverage_data, git_data),
|
57
|
+
details: calculate_method_details(file_path, method_data, coverage_data, git_data)
|
58
|
+
}
|
59
|
+
|
60
|
+
methods << method_info
|
61
|
+
end
|
62
|
+
|
63
|
+
methods
|
64
|
+
end
|
65
|
+
|
66
|
+
def extract_methods_from_file(file_path)
|
67
|
+
return [] unless File.exist?(file_path)
|
68
|
+
|
69
|
+
methods = []
|
70
|
+
File.readlines(file_path).each_with_index do |line, index|
|
71
|
+
next unless line.strip.match?(/^\s*def\s+(\w+)/)
|
72
|
+
|
73
|
+
method_name = line.strip.match(/^\s*def\s+(\w+)/)[1]
|
74
|
+
methods << {
|
75
|
+
method_name: method_name,
|
76
|
+
line_number: index + 1,
|
77
|
+
complexity: 1 # Default complexity
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
methods
|
82
|
+
end
|
83
|
+
|
84
|
+
def calculate_method_score(file_path, method_data, coverage_data, git_data)
|
85
|
+
quality_score = calculate_quality_score(file_path, method_data, coverage_data)
|
86
|
+
importance_score = calculate_importance_score(file_path, git_data)
|
87
|
+
|
88
|
+
final_score = quality_score * importance_score
|
89
|
+
final_score.round(2)
|
90
|
+
end
|
91
|
+
|
92
|
+
def calculate_quality_score(file_path, method_data, coverage_data)
|
93
|
+
score = 0.0
|
94
|
+
|
95
|
+
if @config.quality_weights['test_coverage'] > 0
|
96
|
+
coverage_factor = calculate_coverage_factor(file_path, method_data, coverage_data)
|
97
|
+
score += @config.quality_weights['test_coverage'] * coverage_factor
|
98
|
+
end
|
99
|
+
|
100
|
+
if @config.quality_weights['cyclomatic_complexity'] > 0
|
101
|
+
complexity_factor = method_data[:complexity] || 1
|
102
|
+
score += @config.quality_weights['cyclomatic_complexity'] * complexity_factor
|
103
|
+
end
|
104
|
+
|
105
|
+
score
|
106
|
+
end
|
107
|
+
|
108
|
+
def calculate_importance_score(file_path, git_data)
|
109
|
+
score = 0.0
|
110
|
+
|
111
|
+
if @config.importance_weights['change_frequency'] > 0
|
112
|
+
git_factor = git_data[file_path] || 0
|
113
|
+
score += @config.importance_weights['change_frequency'] * git_factor
|
114
|
+
end
|
115
|
+
|
116
|
+
if @config.importance_weights['architectural_importance'] > 0
|
117
|
+
architectural_factor = @config.architectural_weight_for(file_path)
|
118
|
+
score += @config.importance_weights['architectural_importance'] * architectural_factor
|
119
|
+
end
|
120
|
+
|
121
|
+
score
|
122
|
+
end
|
123
|
+
|
124
|
+
def calculate_coverage_factor(file_path, _method_data, coverage_data)
|
125
|
+
file_coverage = coverage_data[file_path]
|
126
|
+
return 1.0 unless file_coverage # No coverage data means 0% coverage
|
127
|
+
|
128
|
+
# For simplicity, use file-level coverage as method-level coverage
|
129
|
+
# In a more sophisticated implementation, we would calculate method-level coverage
|
130
|
+
1.0 - file_coverage[:coverage_rate]
|
131
|
+
end
|
132
|
+
|
133
|
+
def calculate_method_details(file_path, method_data, coverage_data, git_data)
|
134
|
+
file_coverage = coverage_data[file_path]
|
135
|
+
|
136
|
+
{
|
137
|
+
coverage: file_coverage ? file_coverage[:coverage_rate] : 0.0,
|
138
|
+
complexity: method_data[:complexity] || 1,
|
139
|
+
git_commits: git_data[file_path] || 0
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/code_qualia.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require_relative 'code_qualia/coverage_analyzer'
|
6
|
+
require_relative 'code_qualia/complexity_analyzer'
|
7
|
+
require_relative 'code_qualia/git_analyzer'
|
8
|
+
require_relative 'code_qualia/score_calculator'
|
9
|
+
require_relative 'code_qualia/config'
|
10
|
+
require_relative 'code_qualia/config_installer'
|
11
|
+
require_relative 'code_qualia/cli'
|
12
|
+
require_relative 'code_qualia/logger'
|
13
|
+
|
14
|
+
module CodeQualia
|
15
|
+
class Error < StandardError; end
|
16
|
+
|
17
|
+
DEFAULT_CONFIG_FILE = 'qualia.yml'
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def analyze(config_path = './qualia.yml', verbose: false)
|
21
|
+
Logger.verbose = verbose
|
22
|
+
Logger.log_step('Analysis')
|
23
|
+
|
24
|
+
config = Config.load(config_path)
|
25
|
+
Logger.log('Configuration loaded')
|
26
|
+
|
27
|
+
coverage_data = if config.quality_weights['test_coverage'] > 0
|
28
|
+
Logger.log_step('Coverage analysis')
|
29
|
+
result = CoverageAnalyzer.analyze
|
30
|
+
Logger.log_result('Coverage analysis', result.size)
|
31
|
+
result
|
32
|
+
else
|
33
|
+
Logger.log_skip('Coverage analysis', 'weight is 0')
|
34
|
+
{}
|
35
|
+
end
|
36
|
+
|
37
|
+
complexity_data = if config.quality_weights['cyclomatic_complexity'] > 0
|
38
|
+
Logger.log_step('Complexity analysis')
|
39
|
+
result = ComplexityAnalyzer.analyze
|
40
|
+
Logger.log_result('Complexity analysis', result.values.sum(&:size))
|
41
|
+
result
|
42
|
+
else
|
43
|
+
Logger.log_skip('Complexity analysis', 'weight is 0')
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
git_data = if config.importance_weights['change_frequency'] > 0
|
48
|
+
Logger.log_step('Git history analysis')
|
49
|
+
result = GitAnalyzer.analyze(config.git_history_days)
|
50
|
+
Logger.log_result('Git history analysis', result.size)
|
51
|
+
result
|
52
|
+
else
|
53
|
+
Logger.log_skip('Git history analysis', 'weight is 0')
|
54
|
+
{}
|
55
|
+
end
|
56
|
+
|
57
|
+
Logger.log_step('Score calculation')
|
58
|
+
results = ScoreCalculator.new(config).calculate(
|
59
|
+
coverage_data: coverage_data,
|
60
|
+
complexity_data: complexity_data,
|
61
|
+
git_data: git_data
|
62
|
+
)
|
63
|
+
Logger.log_result('Score calculation', results.size)
|
64
|
+
|
65
|
+
results
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: code_qualia
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- euglena1215
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
description: Code Qualia helps developers express their subjective understanding and
|
42
|
+
feelings about code quality to AI systems by combining quantitative metrics (coverage,
|
43
|
+
complexity, git activity) with configurable weights that reflect development priorities
|
44
|
+
and intuitions.
|
45
|
+
email:
|
46
|
+
- teppest1215@gmail.com
|
47
|
+
executables:
|
48
|
+
- code-qualia
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- README.md
|
53
|
+
- bin/code-qualia
|
54
|
+
- bin/update_smoke_tests
|
55
|
+
- lib/code_qualia.rb
|
56
|
+
- lib/code_qualia/cli.rb
|
57
|
+
- lib/code_qualia/complexity_analyzer.rb
|
58
|
+
- lib/code_qualia/config.rb
|
59
|
+
- lib/code_qualia/config_helper.rb
|
60
|
+
- lib/code_qualia/config_installer.rb
|
61
|
+
- lib/code_qualia/coverage_analyzer.rb
|
62
|
+
- lib/code_qualia/git_analyzer.rb
|
63
|
+
- lib/code_qualia/logger.rb
|
64
|
+
- lib/code_qualia/pattern_expander.rb
|
65
|
+
- lib/code_qualia/score_calculator.rb
|
66
|
+
homepage: https://github.com/euglena1215/code-qualia
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata:
|
70
|
+
rubygems_mfa_required: 'true'
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.5.9
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: A tool for communicating developer intuition and code quality perception
|
90
|
+
to AI through configurable parameters
|
91
|
+
test_files: []
|