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.
@@ -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
@@ -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: []