stud-finder 0.5.1 → 0.7.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/CHANGELOG.md +14 -0
- data/README.md +2 -2
- data/lib/stud_finder/cli.rb +61 -20
- data/lib/stud_finder/gate.rb +163 -0
- data/lib/stud_finder/version.rb +1 -1
- data/lib/stud_finder/warnings.rb +81 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5c4ae36f3971b4c2aa32bc157c76eea4223b401764e2f895f541d4532f5ed8d
|
|
4
|
+
data.tar.gz: '0180283bc3f6c389e7c4b68ce42819e8d5bd5ec08684031249b4deb7915e7b62'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bc2be78efee09c43411250904494503d23b9080e70bbd962cc043dc92073fc94c2903eef4cbe8dd828cb35fd4a5ce70758a815c5ed110a8bfb7250dcf37fc32
|
|
7
|
+
data.tar.gz: 6a808490d99422741999eb3d559c813b1c7e7646a8195bb4d6cc87541fdf21d4420f914e345acb68522d79ee72510101f1f4fb96549d01080b4f105e0318f347
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `stud-finder gate`, an observation-first markdown gate subcommand for JSON scan output. v1 includes hardcoded checks for touched trunks, high-score files with low/missing evidence, and new files escalated by trunk adjacency.
|
|
13
|
+
- Added `--input FILE`/stdin input handling and an `--enforce` flag for gate failures. `--enforce` exists for CI experiments, but do not recommend it as a required merge gate until the Rec 3 rollout lands and teams have calibrated thresholds on real PRs.
|
|
14
|
+
|
|
15
|
+
## [0.6.0] - Unreleased
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- BREAKING: JSON output now reports `meta.schema_version: 1`, an integer schema marker that will bump on future breaking JSON changes.
|
|
20
|
+
- BREAKING: JSON `warnings` and `meta.warnings` are now normalized to objects shaped as `{code, message}`. Bare warning strings are no longer emitted.
|
|
21
|
+
|
|
8
22
|
## [0.5.1] - Unreleased
|
|
9
23
|
|
|
10
24
|
### Added
|
data/README.md
CHANGED
|
@@ -165,7 +165,7 @@ In shallow clones where auto-unshallow fails (or `--no-auto-unshallow` is set),
|
|
|
165
165
|
|
|
166
166
|
## Warnings
|
|
167
167
|
|
|
168
|
-
`analysis.warnings` (available in JSON output) surfaces conditions the run detected that a consumer should know about
|
|
168
|
+
`analysis.warnings` (available in JSON output) surfaces conditions the run detected that a consumer should know about. Starting in schema version `1`, every warning is an object with `code` and human-readable `message` fields; bare warning strings are no longer emitted.
|
|
169
169
|
|
|
170
170
|
- **`shallow_clone_newness_disabled`** — shallow git clone detected; newness rules auto-disabled (auto-unshallow also failed, or `--no-auto-unshallow` was passed).
|
|
171
171
|
- **`shallow_clone_unshallow_failed`** — `git fetch --unshallow` was attempted but failed (network error or timeout); evidence is unavailable. Use `fetch-depth: 0` in CI or pass `--no-auto-unshallow` to suppress the attempt.
|
|
@@ -245,7 +245,7 @@ Each language gets its own ranking section in the output — Ruby and JS are not
|
|
|
245
245
|
|
|
246
246
|
- `table` — human-readable, aligned columns
|
|
247
247
|
- `csv` — spreadsheet-friendly, pipe to a file
|
|
248
|
-
- `json` — machine-readable with `meta`, `warnings`, `ruby`, `javascript` sections. `meta.formula` labels the active mode (`5-factor + coupling`, `5-factor`, `4-factor + coupling`, `4-factor`). `meta.weights` reports the normalized weights actually used (with `null` for signals that were unavailable).
|
|
248
|
+
- `json` — machine-readable with `meta`, `warnings`, `ruby`, `javascript` sections. `meta.schema_version` is the integer JSON schema version (`1` as of Stud Finder 0.6.0). `meta.formula` labels the active mode (`5-factor + coupling`, `5-factor`, `4-factor + coupling`, `4-factor`). `meta.weights` reports the normalized weights actually used (with `null` for signals that were unavailable).
|
|
249
249
|
- `markdown` — drop directly into a PR comment or issue
|
|
250
250
|
|
|
251
251
|
---
|
data/lib/stud_finder/cli.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'set'
|
|
|
9
9
|
require 'time'
|
|
10
10
|
require_relative 'churn'
|
|
11
11
|
require_relative 'temporal_coupling'
|
|
12
|
+
require_relative 'warnings'
|
|
12
13
|
require_relative 'complexity'
|
|
13
14
|
require_relative 'diff'
|
|
14
15
|
require_relative 'coverage/detector'
|
|
@@ -17,6 +18,7 @@ require_relative 'fan_in'
|
|
|
17
18
|
require_relative 'js_fan_in'
|
|
18
19
|
require_relative 'js_complexity'
|
|
19
20
|
require_relative 'file_collector'
|
|
21
|
+
require_relative 'gate'
|
|
20
22
|
require_relative 'loc_counter'
|
|
21
23
|
require_relative 'newness'
|
|
22
24
|
require_relative 'scorer'
|
|
@@ -73,32 +75,40 @@ module StudFinder
|
|
|
73
75
|
|
|
74
76
|
class ValidationError < StandardError; end
|
|
75
77
|
|
|
76
|
-
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
78
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, stdin: $stdin)
|
|
77
79
|
@argv = argv.dup
|
|
78
80
|
@stdout = stdout
|
|
79
81
|
@stderr = stderr
|
|
82
|
+
@stdin = stdin
|
|
80
83
|
@options = Marshal.load(Marshal.dump(DEFAULT_OPTIONS))
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
def self.start(argv = ARGV, stdout: $stdout, stderr: $stderr)
|
|
84
|
-
new(argv, stdout: stdout, stderr: stderr).run
|
|
86
|
+
def self.start(argv = ARGV, stdout: $stdout, stderr: $stderr, stdin: $stdin)
|
|
87
|
+
new(argv, stdout: stdout, stderr: stderr, stdin: stdin).run
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
def run
|
|
88
91
|
parser = option_parser
|
|
92
|
+
return run_gate if shift_subcommand?('gate')
|
|
93
|
+
return run_edges_subcommand(parser) if shift_subcommand?('edges')
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
run_scan(parser)
|
|
96
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::InvalidArgument, ValidationError,
|
|
97
|
+
FileCollector::Error, Gate::Error, Churn::Error, Complexity::Error, Coverage::Cobertura::Error,
|
|
98
|
+
Coverage::Detector::Error, Coverage::Lcov::Error, Coverage::Resultset::Error, Diff::Error, Newness::Error,
|
|
99
|
+
Scorer::ValidationError => e
|
|
100
|
+
@stderr.puts e.message
|
|
101
|
+
1
|
|
102
|
+
end
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
def shift_subcommand?(name)
|
|
105
|
+
return false unless @argv[0] == name
|
|
106
|
+
|
|
107
|
+
@argv.shift
|
|
108
|
+
true
|
|
109
|
+
end
|
|
101
110
|
|
|
111
|
+
def run_scan(parser)
|
|
102
112
|
parser.parse!(@argv)
|
|
103
113
|
path = @argv.shift || '.'
|
|
104
114
|
raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
@@ -121,11 +131,41 @@ module StudFinder
|
|
|
121
131
|
analysis = warn_if_no_scored_files(analysis)
|
|
122
132
|
emit_results(@repo_path, result, analysis)
|
|
123
133
|
0
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@
|
|
128
|
-
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def run_edges_subcommand(parser)
|
|
137
|
+
parser.parse!(@argv)
|
|
138
|
+
target = @argv.shift
|
|
139
|
+
path = @argv.shift || '.'
|
|
140
|
+
raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
141
|
+
|
|
142
|
+
@repo_path = File.expand_path(path)
|
|
143
|
+
validate_options!
|
|
144
|
+
run_edges(target, path)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def run_gate
|
|
148
|
+
gate_options = { input: nil, enforce: false }
|
|
149
|
+
OptionParser.new do |opts|
|
|
150
|
+
opts.banner = 'Usage: stud-finder gate [--input FILE] [--enforce]'
|
|
151
|
+
opts.on('--input FILE', 'Read stud-finder JSON output from FILE') { |value| gate_options[:input] = value }
|
|
152
|
+
opts.on('--enforce', 'Exit non-zero when gate findings are present') { gate_options[:enforce] = true }
|
|
153
|
+
end.parse!(@argv)
|
|
154
|
+
raise ValidationError, "Error: unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
155
|
+
|
|
156
|
+
json = gate_input(gate_options[:input])
|
|
157
|
+
result = Gate.call(json)
|
|
158
|
+
@stdout.puts Gate.markdown(result, enforce: gate_options[:enforce])
|
|
159
|
+
gate_options[:enforce] && result.findings? ? 1 : 0
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def gate_input(input_path)
|
|
163
|
+
return File.read(input_path) if input_path
|
|
164
|
+
return @stdin.read if !@stdin.respond_to?(:tty?) || !@stdin.tty?
|
|
165
|
+
|
|
166
|
+
raise ValidationError, 'Error: provide --input FILE or pipe JSON to stdin.'
|
|
167
|
+
rescue Errno::ENOENT
|
|
168
|
+
raise ValidationError, "Error: input file not found: #{input_path}"
|
|
129
169
|
end
|
|
130
170
|
|
|
131
171
|
def run_edges(target, path)
|
|
@@ -722,7 +762,7 @@ module StudFinder
|
|
|
722
762
|
def emit_json(path, analysis, ruby_rows, javascript_rows)
|
|
723
763
|
@stdout.puts JSON.generate(
|
|
724
764
|
meta: json_meta(path, analysis),
|
|
725
|
-
warnings: analysis.warnings,
|
|
765
|
+
warnings: Warnings.normalize(analysis.warnings),
|
|
726
766
|
ruby: ruby_rows.map { |row| json_file(row) },
|
|
727
767
|
javascript: javascript_rows.map { |row| json_file(row) }
|
|
728
768
|
)
|
|
@@ -737,7 +777,8 @@ module StudFinder
|
|
|
737
777
|
files_skipped: analysis.ruby.skipped_files.length + analysis.javascript.skipped_files.length,
|
|
738
778
|
formula: json_formula(analysis),
|
|
739
779
|
weights: json_weights(analysis.ruby.weights || analysis.javascript.weights),
|
|
740
|
-
|
|
780
|
+
schema_version: 1,
|
|
781
|
+
warnings: Warnings.normalize(analysis.warnings)
|
|
741
782
|
}
|
|
742
783
|
meta[:filtered] = true if @options[:filter_set]
|
|
743
784
|
meta[:diff_base] = @options[:diff_base] if @options[:diff_base]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module StudFinder
|
|
6
|
+
class Gate
|
|
7
|
+
CHECKS = %w[trunk_touched low_evidence_high_score newness_trunk_adjacent].freeze
|
|
8
|
+
HIGH_SCORE_THRESHOLD = 0.75
|
|
9
|
+
LOW_EVIDENCE_THRESHOLD = 0.50
|
|
10
|
+
|
|
11
|
+
Finding = Struct.new(:path, :language, :score, :evidence, :classification, :reason, keyword_init: true)
|
|
12
|
+
Result = Struct.new(:checks, keyword_init: true) do
|
|
13
|
+
def finding_count
|
|
14
|
+
checks.values.sum(&:length)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def findings?
|
|
18
|
+
finding_count.positive?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
|
|
24
|
+
def self.call(json)
|
|
25
|
+
new(json).call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(json)
|
|
29
|
+
@payload = JSON.parse(json)
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise Error, "Error: invalid JSON input: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
rows = Array(@payload.fetch('ruby', [])) + Array(@payload.fetch('javascript', []))
|
|
36
|
+
Result.new(
|
|
37
|
+
checks: {
|
|
38
|
+
'trunk_touched' => trunk_touched(rows),
|
|
39
|
+
'low_evidence_high_score' => low_evidence_high_score(rows),
|
|
40
|
+
'newness_trunk_adjacent' => newness_trunk_adjacent(rows)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.markdown(result, enforce: false)
|
|
46
|
+
(markdown_header(result, enforce: enforce) + markdown_summary(result) + markdown_details(result)).join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.markdown_header(result, enforce:)
|
|
50
|
+
[
|
|
51
|
+
'## Stud Finder gate',
|
|
52
|
+
'',
|
|
53
|
+
"**Mode:** #{enforce ? 'enforce' : 'observation'}",
|
|
54
|
+
"**Summary:** #{pluralize(result.finding_count, 'finding')} across #{CHECKS.length} checks.",
|
|
55
|
+
''
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
private_class_method :markdown_header
|
|
59
|
+
|
|
60
|
+
def self.markdown_summary(result)
|
|
61
|
+
CHECKS.map do |check|
|
|
62
|
+
findings = result.checks.fetch(check)
|
|
63
|
+
status = findings.empty? ? '✅' : '⚠️'
|
|
64
|
+
"- #{status} `#{check}` — #{pluralize(findings.length, 'finding')}"
|
|
65
|
+
end + ['']
|
|
66
|
+
end
|
|
67
|
+
private_class_method :markdown_summary
|
|
68
|
+
|
|
69
|
+
def self.markdown_details(result)
|
|
70
|
+
CHECKS.flat_map { |check| markdown_check_detail(check, result.checks.fetch(check)) }
|
|
71
|
+
end
|
|
72
|
+
private_class_method :markdown_details
|
|
73
|
+
|
|
74
|
+
def self.markdown_check_detail(check, findings)
|
|
75
|
+
[
|
|
76
|
+
"<details#{' open' if findings.any?}>",
|
|
77
|
+
"<summary><strong>#{check}</strong> — #{pluralize(findings.length, 'finding')}</summary>",
|
|
78
|
+
'',
|
|
79
|
+
*markdown_finding_rows(findings),
|
|
80
|
+
'',
|
|
81
|
+
'</details>',
|
|
82
|
+
''
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
private_class_method :markdown_check_detail
|
|
86
|
+
|
|
87
|
+
def self.markdown_finding_rows(findings)
|
|
88
|
+
return ['_No findings._'] if findings.empty?
|
|
89
|
+
|
|
90
|
+
['| file | class | score | evidence | reason |', '| --- | --- | ---: | ---: | --- |'] +
|
|
91
|
+
findings.map { |finding| markdown_finding_row(finding) }
|
|
92
|
+
end
|
|
93
|
+
private_class_method :markdown_finding_rows
|
|
94
|
+
|
|
95
|
+
def self.markdown_finding_row(finding)
|
|
96
|
+
"| `#{escape_md(finding.path)}` | #{escape_md(finding.classification)} | " \
|
|
97
|
+
"#{format_number(finding.score)} | #{format_evidence(finding.evidence)} | #{escape_md(finding.reason)} |"
|
|
98
|
+
end
|
|
99
|
+
private_class_method :markdown_finding_row
|
|
100
|
+
|
|
101
|
+
def self.pluralize(count, noun)
|
|
102
|
+
"#{count} #{noun}#{'s' unless count == 1}"
|
|
103
|
+
end
|
|
104
|
+
private_class_method :pluralize
|
|
105
|
+
|
|
106
|
+
def self.escape_md(value)
|
|
107
|
+
value.to_s.gsub('|', '\\|')
|
|
108
|
+
end
|
|
109
|
+
private_class_method :escape_md
|
|
110
|
+
|
|
111
|
+
def self.format_number(value)
|
|
112
|
+
value.nil? ? '—' : format('%.4f', value.to_f)
|
|
113
|
+
end
|
|
114
|
+
private_class_method :format_number
|
|
115
|
+
|
|
116
|
+
def self.format_evidence(value)
|
|
117
|
+
value.nil? ? 'nil' : format_number(value)
|
|
118
|
+
end
|
|
119
|
+
private_class_method :format_evidence
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def trunk_touched(rows)
|
|
124
|
+
rows.select { |row| row['class'] == 'trunk' }.map do |row|
|
|
125
|
+
finding(row, reason: 'Changed file is classified as trunk.')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def low_evidence_high_score(rows)
|
|
130
|
+
risky_rows = rows.select do |row|
|
|
131
|
+
row.fetch('score', 0).to_f >= HIGH_SCORE_THRESHOLD && low_evidence?(row['evidence'])
|
|
132
|
+
end
|
|
133
|
+
risky_rows.map do |row|
|
|
134
|
+
finding(row, reason: low_evidence_high_score_reason)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def low_evidence_high_score_reason
|
|
139
|
+
"Score is >= #{HIGH_SCORE_THRESHOLD} while evidence is nil or < #{LOW_EVIDENCE_THRESHOLD}."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def newness_trunk_adjacent(rows)
|
|
143
|
+
rows.select { |row| row['new_file'] && row['escalation'] == 'trunk_adjacent' }.map do |row|
|
|
144
|
+
finding(row, reason: 'New file is trunk-adjacent.')
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def low_evidence?(value)
|
|
149
|
+
value.nil? || value.to_f < LOW_EVIDENCE_THRESHOLD
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def finding(row, reason:)
|
|
153
|
+
Finding.new(
|
|
154
|
+
path: row.fetch('path'),
|
|
155
|
+
language: row['language'],
|
|
156
|
+
score: row['score'],
|
|
157
|
+
evidence: row['evidence'],
|
|
158
|
+
classification: row['class'],
|
|
159
|
+
reason: reason
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
data/lib/stud_finder/version.rb
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StudFinder
|
|
4
|
+
module Warnings
|
|
5
|
+
MESSAGES = {
|
|
6
|
+
'coverage_flag_deprecated' => '--coverage is deprecated; use --ruby-coverage instead',
|
|
7
|
+
'coverage_unavailable' => 'coverage data was not provided; coverage and interaction signals are unavailable',
|
|
8
|
+
'coverage_partial' => 'coverage data did not include every scored file; missing files use uncovered risk',
|
|
9
|
+
'diff_filter_empty' => 'diff filter matched no changed files; output rows are empty',
|
|
10
|
+
'diff_no_scored_files' => 'diff matched no scored files; the PR may only touch unscorable files',
|
|
11
|
+
'fan_in_rails_inference_failed' => 'Rails inference failed; inferred Rails references may be incomplete',
|
|
12
|
+
'fan_in_reference_resolution_failed' => 'constant reference resolution failed; fan-in may be incomplete',
|
|
13
|
+
'files_skipped' => 'one or more files were skipped during analysis',
|
|
14
|
+
'git_error' => 'git command failed while computing temporal coupling; coupling is unavailable',
|
|
15
|
+
'git_not_found' => 'git was not found in PATH while computing temporal coupling; coupling is unavailable',
|
|
16
|
+
'js_depcruise_failed' => 'dependency-cruiser failed; JavaScript/TypeScript fan-in is unavailable',
|
|
17
|
+
'js_depcruise_no_config' => 'dependency-cruiser config failed; retried with --no-config, so ' \
|
|
18
|
+
'JavaScript/TypeScript fan-in may be undercounted',
|
|
19
|
+
'js_depcruise_timeout' => 'dependency-cruiser timed out; JavaScript/TypeScript fan-in is unavailable',
|
|
20
|
+
'js_eslint_failed' => 'ESLint failed for at least one batch; JavaScript/TypeScript complexity may be incomplete',
|
|
21
|
+
'js_eslint_malformed' => 'ESLint produced malformed JSON for at least one batch; JavaScript/TypeScript ' \
|
|
22
|
+
'complexity may be incomplete',
|
|
23
|
+
'js_eslint_missing' => 'ESLint was not found; JavaScript/TypeScript complexity is unavailable',
|
|
24
|
+
'js_eslint_timeout' => 'ESLint timed out for at least one batch; JavaScript/TypeScript complexity may be ' \
|
|
25
|
+
'incomplete',
|
|
26
|
+
'js_tools_missing' => 'Node.js or dependency-cruiser was not found; JavaScript/TypeScript fan-in is unavailable',
|
|
27
|
+
'js_ts_parser_missing' => 'TypeScript files were present but @typescript-eslint/parser was not found; ' \
|
|
28
|
+
'TypeScript complexity may be incomplete',
|
|
29
|
+
'shallow_clone_newness_disabled' => 'shallow git clone detected; newness rules disabled (use fetch-depth: 0)',
|
|
30
|
+
'shallow_clone_unshallow_failed' => 'auto-unshallow failed; evidence unavailable (use full git history)',
|
|
31
|
+
'small_repo' => 'repo has fewer files than --min-files; results may be noisy',
|
|
32
|
+
'temporal_coupling_bulk_commits_skipped' => 'one or more bulk commits were skipped while computing temporal ' \
|
|
33
|
+
'coupling',
|
|
34
|
+
'zero_churn_majority' => 'most files have zero churn in the selected window; churn may be less informative'
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
INSUFFICIENT_DISPERSION_MESSAGE = lambda do |signal|
|
|
38
|
+
"every file has the same non-zero raw #{signal} value, so its percentile-ranked contribution collapsed to 0.0"
|
|
39
|
+
end
|
|
40
|
+
private_constant :INSUFFICIENT_DISPERSION_MESSAGE
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def normalize(items)
|
|
45
|
+
Array(items).map { |item| normalize_one(item) }.uniq { |warning| warning.fetch(:code) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def normalize_one(item)
|
|
49
|
+
code, explicit_message = warning_parts(item)
|
|
50
|
+
{ code: code, message: message_for(code, explicit_message, item) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def message_for(code, explicit_message = nil, item = nil)
|
|
54
|
+
return explicit_message.to_s unless explicit_message.to_s.empty?
|
|
55
|
+
|
|
56
|
+
if code.start_with?('insufficient_dispersion_')
|
|
57
|
+
signal = code.delete_prefix('insufficient_dispersion_')
|
|
58
|
+
return INSUFFICIENT_DISPERSION_MESSAGE.call(signal)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if code == 'temporal_coupling_bulk_commits_skipped' && item.respond_to?(:fetch)
|
|
62
|
+
count = item.fetch(:count, nil) || item.fetch('count', nil)
|
|
63
|
+
max = item.fetch(:max_commit_files, nil) || item.fetch('max_commit_files', nil)
|
|
64
|
+
return "#{MESSAGES.fetch(code)} (#{count} skipped; max_commit_files=#{max})" if count && max
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
MESSAGES.fetch(code) { code.tr('_', ' ') }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def warning_parts(item)
|
|
71
|
+
if item.respond_to?(:fetch) && (item.key?(:code) || item.key?('code'))
|
|
72
|
+
code = item.fetch(:code, nil) || item.fetch('code')
|
|
73
|
+
message = item.fetch(:message, nil) || item.fetch('message', nil)
|
|
74
|
+
[code.to_s, message]
|
|
75
|
+
else
|
|
76
|
+
[item.to_s, nil]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
private_class_method :warning_parts
|
|
80
|
+
end
|
|
81
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stud-finder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bazfer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: csv
|
|
@@ -162,6 +162,7 @@ files:
|
|
|
162
162
|
- lib/stud_finder/edges.rb
|
|
163
163
|
- lib/stud_finder/fan_in.rb
|
|
164
164
|
- lib/stud_finder/file_collector.rb
|
|
165
|
+
- lib/stud_finder/gate.rb
|
|
165
166
|
- lib/stud_finder/js_complexity.rb
|
|
166
167
|
- lib/stud_finder/js_fan_in.rb
|
|
167
168
|
- lib/stud_finder/loc_counter.rb
|
|
@@ -171,6 +172,7 @@ files:
|
|
|
171
172
|
- lib/stud_finder/scorer.rb
|
|
172
173
|
- lib/stud_finder/temporal_coupling.rb
|
|
173
174
|
- lib/stud_finder/version.rb
|
|
175
|
+
- lib/stud_finder/warnings.rb
|
|
174
176
|
homepage: https://github.com/bazfer/stud-finder
|
|
175
177
|
licenses:
|
|
176
178
|
- MIT
|