stud-finder 0.3.0 → 0.4.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 +7 -1
- data/README.md +15 -11
- data/SIGNALS.md +1 -1
- data/lib/stud_finder/cli.rb +4 -2
- data/lib/stud_finder/scorer.rb +9 -7
- data/lib/stud_finder/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c203b1ef550bd6c05b1f07211cbad55d55520dcbf081ec59f24ab955c3a34d8
|
|
4
|
+
data.tar.gz: 10c285f7512708afda1af1a3c8f0b629eccdddc45aee9ac1cd2869c44af86f64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d68892673ccaed71bde8f223ae5d1098033aeee65ca89bb7b21c301edf7a3eb7a7745cd5e70df24bbdb779a70d6f44ddbe4ccab324ff4731fba47f8c3a01cef
|
|
7
|
+
data.tar.gz: 704e0964915bbe5dce1628d082e5eccb6e1376bdd9919ace07251000892432373f94e61d1f2cebb91442feda1cef50358aeda31c2e30ed95e5414529ce5479ce
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.4.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- BREAKING: Classification thresholds are now percentile-based, not score-value-based. `--trunk-threshold 85` and `--branch-threshold 50` now mean "top 15% and top 50% of files by composite score" — not "score value ≥ 0.85 / 0.50". This is a semantic change to the same flag values; the numbers you pass stay the same but their meaning inverts. Previously, trunk was unreachable at defaults (max observed composite score ~0.717 in real repos, zero trunks); now every repo of meaningful size produces some trunks and branches. Absolute floors (raw complexity ≥ 15, raw fan_in ≥ 25) still escalate to `branch` regardless of percentile.
|
|
13
|
+
|
|
8
14
|
## [0.3.0] - 2026-07-11
|
|
9
15
|
|
|
10
16
|
### Fixed
|
|
@@ -38,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
38
44
|
|
|
39
45
|
### Added
|
|
40
46
|
|
|
41
|
-
- Initial
|
|
47
|
+
- Initial RubyGems release: a CLI that ranks files by five risk signals: fan-in (blast radius), fan-out, cyclomatic complexity, git churn, and test coverage.
|
|
42
48
|
- Temporal coupling analysis for identifying files that change together.
|
|
43
49
|
- Diff mode for scoring only files changed in a pull request while preserving repo-relative scores.
|
|
44
50
|
- Ruby and JavaScript/TypeScript support.
|
data/README.md
CHANGED
|
@@ -9,12 +9,12 @@ $ bundle exec bin/stud-finder ./my-rails-app
|
|
|
9
9
|
|
|
10
10
|
Ruby
|
|
11
11
|
rank language file score evidence class new age_days escalation ...
|
|
12
|
-
1 ruby app/models/proficiency.rb 0.7304 1.0000
|
|
13
|
-
2 ruby app/services/payment_service.rb 0.6890 1.0000
|
|
12
|
+
1 ruby app/models/proficiency.rb 0.7304 1.0000 trunk false 842 ...
|
|
13
|
+
2 ruby app/services/payment_service.rb 0.6890 1.0000 trunk false 611 ...
|
|
14
14
|
3 ruby app/controllers/orders_ctlr.rb 0.5721 0.6667 branch false 520 ...
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
*Scores and evidence are illustrative. Row 3 has no coverage data (evidence capped at `0.6667 = (1.0 + 1.0 + 0.0) / 3`); rows 1–2 have full coverage (evidence `1.0000`).
|
|
17
|
+
*Scores and evidence are illustrative. Row 3 has no coverage data (evidence capped at `0.6667 = (1.0 + 1.0 + 0.0) / 3`); rows 1–2 have full coverage (evidence `1.0000`). The `class` column reflects percentile rank of composite score within the repo — see Classification.*
|
|
18
18
|
|
|
19
19
|
The full table adds `fan_in`, `fan_out`, `instability`, `complexity`, `churn_commits`, `churn_lines`, `churn_pct`, `loc`, `loc_pct`, `max_coupling`, `max_coupling_partner`, `coupling_partners`, `coupling_pct`, and `coverage`. Use `--output json` for machine-readable output including a `warnings` section and full `meta`.
|
|
20
20
|
|
|
@@ -117,15 +117,19 @@ Result is clamped to `[0.0, 1.0]` and rounded to four decimal places.
|
|
|
117
117
|
|
|
118
118
|
## Classification
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Files are classified into three tiers based on the **percentile rank of their composite score** within the repo:
|
|
121
121
|
|
|
122
|
-
- **trunk** —
|
|
123
|
-
- **branch** —
|
|
124
|
-
- **leaf** —
|
|
122
|
+
- **trunk** — top 15% by composite score (default `--trunk-threshold 85`). Load-bearing. High review bar, change with care.
|
|
123
|
+
- **branch** — top 50% but below top 15% (default `--branch-threshold 50`). Meaningful coupling.
|
|
124
|
+
- **leaf** — everything below the 50th percentile. Isolated. Move fast here.
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
This means every repo of meaningful size has trunks: a file at score 0.55 is trunk if the rest of the repo scores below it. The absolute floors below provide a safety net for tiny repos.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
**Absolute floors:** raw complexity ≥ 15 or raw fan-in ≥ 25 escalates a `leaf` to `branch` regardless of percentile. This ensures that structurally dangerous files in tiny repos (where percentile spread is minimal) still receive elevated attention.
|
|
129
|
+
|
|
130
|
+
**Tiny repos:** In repos with very few files and uniform scores, the percentile spread may place all files at the same score_pct (0.0), resulting in 0 trunks. The absolute floor is the escape hatch for dangerous files in this case.
|
|
131
|
+
|
|
132
|
+
Note: `--trunk-threshold 85` and `--branch-threshold 50` now mean "top 15% / top 50% of files by composite score" — not "score value ≥ 0.85 / 0.50". This is a BREAKING semantic change from 0.3.0. Trunk was previously unreachable at defaults (max observed composite score ~0.717 in real repos); this change restores the guarantee that some files are always classified trunk-tier relative to their codebase.
|
|
129
133
|
|
|
130
134
|
### Absolute floors
|
|
131
135
|
|
|
@@ -218,8 +222,8 @@ Each language gets its own ranking section in the output — Ruby and JS are not
|
|
|
218
222
|
| `--weights WEIGHTS` | Custom weights as fractions, e.g. `fan_in:F,fan_out:O,complexity:C,churn:H,coverage:V[,interaction:I][,coupling:P]`. The five base keys (`fan_in`, `fan_out`, `complexity`, `churn`, `coverage`) are required. `interaction` and `coupling` are optional: when omitted, `interaction` defaults to `0.0` (custom weights opt-in) and `coupling` defaults to `0.05`. Each value must be in `[0.0, 1.0]`. When no coverage data is provided, `coverage` must be `0.0`. |
|
|
219
223
|
| `--interaction-weight N` | Sugar flag for setting only the interaction weight. |
|
|
220
224
|
| `--coupling-weight N` | Sugar flag for setting only the coupling weight. Bounds-checked `[0.0, 1.0]`. |
|
|
221
|
-
| `--trunk-threshold N` |
|
|
222
|
-
| `--branch-threshold N` |
|
|
225
|
+
| `--trunk-threshold N` | composite-score percentile cutoff for trunk classification; top (100-N)% of files by score (default: 85) |
|
|
226
|
+
| `--branch-threshold N` | composite-score percentile cutoff for branch classification; top (100-N)% of files by score (default: 50) |
|
|
223
227
|
| `--exclude PATTERN` | Exclude glob pattern (repeatable). `spec/` and `test/` excluded by default. |
|
|
224
228
|
| `--top N` | Emit only the top N results |
|
|
225
229
|
| `--diff-base REF` | Score the whole repo but emit only the files changed on `HEAD` vs the merge-base with `REF` (e.g. `origin/staging`). Ranks and scores stay relative to the full repo. Ideal for per-PR runs. |
|
data/SIGNALS.md
CHANGED
|
@@ -88,7 +88,7 @@ Ties receive the same rank. Edge cases: a codebase with only one file gets `0.0`
|
|
|
88
88
|
Three pieces of output serve three purposes. Confusing them causes gate consumers to threshold the wrong number.
|
|
89
89
|
|
|
90
90
|
- **`score`** (0.0–1.0, four decimals) — the weighted composite. This is **evidence about the file's structural risk**, nothing more. Higher score means the signals concentrated more risk on this file.
|
|
91
|
-
- **`class`** (`leaf` / `branch` / `trunk`) — the decision label. Driven by the composite
|
|
91
|
+
- **`class`** (`leaf` / `branch` / `trunk`) — the decision label. Driven by the composite-score PERCENTILE across the codebase against configurable thresholds (defaults: `branch` at top 50%, `trunk` at top 15%). This guarantees that some files are always trunk-tier relative to their repo — a file scoring 0.55 can be trunk if the rest of the codebase scores below it. This is what a gate should threshold on for verdicts.
|
|
92
92
|
- **`evidence`** (0.0–1.0) — a metadata confidence value based on file age, commit count, and whether coverage data was explicitly provided. A high score with low evidence means "structural signals concentrated risk here, but we're not certain because the file is young or the history is thin." Gates should threshold `evidence` for confidence, not raw `score`.
|
|
93
93
|
|
|
94
94
|
### Absolute floors
|
data/lib/stud_finder/cli.rb
CHANGED
|
@@ -209,11 +209,13 @@ module StudFinder
|
|
|
209
209
|
@options[:rails_inference] = false
|
|
210
210
|
end
|
|
211
211
|
opts.on('--trunk-threshold N', Integer,
|
|
212
|
-
'score cutoff for trunk classification, integer 1-99
|
|
212
|
+
'score percentile cutoff for trunk classification, integer 1-99 ' \
|
|
213
|
+
'(default: 85 = top 15% of files by score)') do |value|
|
|
213
214
|
@options[:trunk_threshold] = value
|
|
214
215
|
end
|
|
215
216
|
opts.on('--branch-threshold N', Integer,
|
|
216
|
-
'score cutoff for branch classification, integer 1-99
|
|
217
|
+
'score percentile cutoff for branch classification, integer 1-99 ' \
|
|
218
|
+
'(default: 50 = top 50% of files by score)') do |value|
|
|
217
219
|
@options[:branch_threshold] = value
|
|
218
220
|
end
|
|
219
221
|
opts.on('--exclude PATTERN', 'Exclude glob pattern (repeatable)') do |value|
|
data/lib/stud_finder/scorer.rb
CHANGED
|
@@ -52,9 +52,11 @@ module StudFinder
|
|
|
52
52
|
}
|
|
53
53
|
@warnings = insufficient_dispersion_warnings(pcts)
|
|
54
54
|
|
|
55
|
+
scores = @files.to_h { |file| [file, weighted_score(file, pcts)] }
|
|
56
|
+
score_pcts = Normalizer.percentile_rank(scores, @files)
|
|
57
|
+
|
|
55
58
|
rows = @files.each_with_index.map do |file, index|
|
|
56
|
-
|
|
57
|
-
[index, result_row(file, score, pcts)]
|
|
59
|
+
[index, result_row(file, scores.fetch(file), pcts, score_pcts.fetch(file))]
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
rows.sort_by { |index, row| [-row[:score], index] }
|
|
@@ -123,12 +125,12 @@ module StudFinder
|
|
|
123
125
|
end
|
|
124
126
|
end
|
|
125
127
|
|
|
126
|
-
def result_row(file, score, pcts)
|
|
128
|
+
def result_row(file, score, pcts, score_pct)
|
|
127
129
|
fi = @fan_in.fetch(file, 0).to_i
|
|
128
130
|
fo = @fan_out.fetch(file, 0).to_i
|
|
129
131
|
rounded_score = score.round(4)
|
|
130
132
|
complexity = @complexity.fetch(file, 0).to_i
|
|
131
|
-
floored_class, floor_escalation = floored_classification(classification(
|
|
133
|
+
floored_class, floor_escalation = floored_classification(classification(score_pct), complexity, fi)
|
|
132
134
|
{
|
|
133
135
|
path: file,
|
|
134
136
|
score: rounded_score,
|
|
@@ -242,9 +244,9 @@ module StudFinder
|
|
|
242
244
|
end
|
|
243
245
|
end
|
|
244
246
|
|
|
245
|
-
def classification(
|
|
246
|
-
return 'trunk' if
|
|
247
|
-
return 'branch' if
|
|
247
|
+
def classification(score_pct)
|
|
248
|
+
return 'trunk' if score_pct >= @trunk_threshold / 100.0
|
|
249
|
+
return 'branch' if score_pct >= @branch_threshold / 100.0
|
|
248
250
|
|
|
249
251
|
'leaf'
|
|
250
252
|
end
|
data/lib/stud_finder/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stud-finder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bazfer
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-07-11 00:00:00.000000000 Z
|
|
@@ -180,7 +180,7 @@ metadata:
|
|
|
180
180
|
changelog_uri: https://github.com/bazfer/stud-finder/blob/main/CHANGELOG.md
|
|
181
181
|
bug_tracker_uri: https://github.com/bazfer/stud-finder/issues
|
|
182
182
|
rubygems_mfa_required: 'true'
|
|
183
|
-
post_install_message:
|
|
183
|
+
post_install_message:
|
|
184
184
|
rdoc_options: []
|
|
185
185
|
require_paths:
|
|
186
186
|
- lib
|
|
@@ -195,8 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
195
195
|
- !ruby/object:Gem::Version
|
|
196
196
|
version: '0'
|
|
197
197
|
requirements: []
|
|
198
|
-
rubygems_version: 3.
|
|
199
|
-
signing_key:
|
|
198
|
+
rubygems_version: 3.4.10
|
|
199
|
+
signing_key:
|
|
200
200
|
specification_version: 4
|
|
201
201
|
summary: Rank files by structural risk in Ruby and JavaScript/TypeScript codebases.
|
|
202
202
|
test_files: []
|