stud-finder 0.4.0 → 0.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c203b1ef550bd6c05b1f07211cbad55d55520dcbf081ec59f24ab955c3a34d8
4
- data.tar.gz: 10c285f7512708afda1af1a3c8f0b629eccdddc45aee9ac1cd2869c44af86f64
3
+ metadata.gz: 6e14a94cc20570cd7eb6b592b62b0cbf7268d8be02f546a3f1f343146f6b3c6f
4
+ data.tar.gz: a49e0535ae4dc3f97958e2758773a04ceb7b486e7f7582a3048562dafbcfeaf4
5
5
  SHA512:
6
- metadata.gz: 2d68892673ccaed71bde8f223ae5d1098033aeee65ca89bb7b21c301edf7a3eb7a7745cd5e70df24bbdb779a70d6f44ddbe4ccab324ff4731fba47f8c3a01cef
7
- data.tar.gz: 704e0964915bbe5dce1628d082e5eccb6e1376bdd9919ace07251000892432373f94e61d1f2cebb91442feda1cef50358aeda31c2e30ed95e5414529ce5479ce
6
+ metadata.gz: d036d77feaec9b455cf6abe8d5f2578f25a91f7948bb91d7a97150d94b007c38fd4daa4b4ea0bce21c41168b31f3f70cb03779657af4c156a7c4225d17fa6020
7
+ data.tar.gz: ff082727ae14f9dbd27a0a11528a4f3223df541218ab58f4ac7420adae66b405eef05ae541c20da3ad3a97cd738c2104bc5b916ad1846ac6066f361576e2f3a2
data/CHANGELOG.md CHANGED
@@ -5,7 +5,32 @@ 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
8
+ ## [0.5.1] - Unreleased
9
+
10
+ ### Added
11
+
12
+ - Auto-unshallow now emits a stderr progress line before fetching, so users aren't surprised by the silent clone mutation. `--no-auto-unshallow` still skips the whole attempt.
13
+
14
+ ### Fixed
15
+
16
+ - Auto-unshallow no longer leaks the child `git fetch` process on timeout. `Open3.popen3` + explicit `TERM`/`KILL` reap replaces the prior `Open3.capture3` + `Timeout.timeout` pattern that unwound the parent stack while leaving the child orphaned.
17
+
18
+ ## [0.5.0] - 2026-07-12
19
+
20
+ ### Changed
21
+
22
+ - BREAKING: Evidence formula changed — coverage is now a bonus, not a required third signal. The new formula is `max(history_only, with_coverage)` where `history_only = (age + commits) / 2`. Mature files without a coverage report now produce `evidence: 1.0` instead of the old ceiling of `0.6667`. Files with coverage data can only equal or exceed the history-only baseline, never fall below it.
23
+
24
+ ### Fixed
25
+
26
+ - Shallow-clone evidence null in CI: when Stud Finder detects a depth-1 clone (GitHub Actions default), it now automatically runs `git fetch --unshallow` (45 second timeout) before computing newness metadata. Repos that used the default `actions/checkout` without `fetch-depth: 0` now get real evidence values instead of `null` on every row.
27
+
28
+ ### Added
29
+
30
+ - `--no-auto-unshallow` flag: opt out of the automatic `git fetch --unshallow` on shallow clones. When set, Stud Finder behaves as before — shallow clone disables newness and emits only `shallow_clone_newness_disabled`.
31
+ - `shallow_clone_unshallow_failed` warning code: fires when auto-unshallow was attempted but failed (network error, timeout, or non-zero exit). Appears alongside `shallow_clone_newness_disabled` so consumers can distinguish "unshallow was never tried" from "unshallow was tried and failed".
32
+
33
+ ## [0.4.0] - 2026-07-11
9
34
 
10
35
  ### Changed
11
36
 
data/README.md CHANGED
@@ -11,10 +11,10 @@ Ruby
11
11
  rank language file score evidence class new age_days escalation ...
12
12
  1 ruby app/models/proficiency.rb 0.7304 1.0000 trunk false 842 ...
13
13
  2 ruby app/services/payment_service.rb 0.6890 1.0000 trunk false 611 ...
14
- 3 ruby app/controllers/orders_ctlr.rb 0.5721 0.6667 branch false 520 ...
14
+ 3 ruby app/controllers/orders_ctlr.rb 0.5721 1.0000 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`). The `class` column reflects percentile rank of composite score within the repo — see Classification.*
17
+ *Scores and evidence are illustrative. Row 3 has full history but no coverage data (evidence `1.0000` coverage is a bonus, not a requirement); 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
 
@@ -147,7 +147,7 @@ A file is considered new when its first commit is within `--new-file-days` days
147
147
 
148
148
  A stronger rule runs first: if a new file depends on a structurally `trunk` file through its fan-out edges, it escalates to `trunk` with `escalation=trunk_adjacent`. This highlights new code consuming critical interfaces, where contract-violation risk is highest. Use `--no-newness` to disable both newness rules.
149
149
 
150
- **CI usage:** newness rules require full git history. In GitHub Actions, set `fetch-depth: 0` before running Stud Finder. If Stud Finder detects a shallow clone, it auto-disables both newness rules and emits `shallow_clone_newness_disabled` in `warnings` so classifications match `--no-newness` instead of misclassifying mature files as new.
150
+ **CI usage:** newness rules require full git history. In GitHub Actions, set `fetch-depth: 0` before running Stud Finder. If Stud Finder detects a shallow clone it automatically runs `git fetch --unshallow` (45 second timeout) to recover full history before computing metadata. If the fetch fails (network error, timeout, offline runner), Stud Finder falls back to disabled newness and emits both `shallow_clone_newness_disabled` and `shallow_clone_unshallow_failed` in `warnings`. Pass `--no-auto-unshallow` to skip the fetch attempt entirely — on shallow clones only `shallow_clone_newness_disabled` fires and classifications match `--no-newness`.
151
151
 
152
152
  ---
153
153
 
@@ -155,9 +155,9 @@ A stronger rule runs first: if a new file depends on a structurally `trunk` file
155
155
 
156
156
  Every row carries an `evidence` value (0.0–1.0) alongside `score`. Score is the weighted signal composite. Evidence is a metadata confidence: how much history + coverage-data backing does that score have?
157
157
 
158
- Evidence combines 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." A high score with high evidence is a strong claim.
158
+ Evidence combines file age and commit count as its base, with coverage data as a bonus signal. The formula is `max(history_only, with_coverage)`, where `history_only = (age + commits) / 2` and `with_coverage = (age + commits + 1.0) / 3`. A mature file with full history always reaches `1.0` regardless of whether a coverage report is present — coverage can only raise evidence above the history baseline, never suppress it. 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." A high score with high evidence is a strong claim.
159
159
 
160
- In shallow clones (git fetch-depth < full history), evidence is `null` on every row because file-metadata history is unavailable. The `shallow_clone_newness_disabled` warning also fires. See the CI note above.
160
+ In shallow clones where auto-unshallow fails (or `--no-auto-unshallow` is set), evidence is `null` on every row because file-metadata history is unavailable. See the CI note above.
161
161
 
162
162
  **Gate consumers should threshold `class` for verdicts and `evidence` for confidence, not raw `score` alone.** Output is sorted by `(class_rank, score)` so trunks group above branches above leaves, and `--top N` no longer drops newness-escalated `trunk_adjacent` files behind high-score branches.
163
163
 
@@ -167,7 +167,8 @@ In shallow clones (git fetch-depth < full history), evidence is `null` on every
167
167
 
168
168
  `analysis.warnings` (available in JSON output) surfaces conditions the run detected that a consumer should know about:
169
169
 
170
- - **`shallow_clone_newness_disabled`** — shallow git clone detected; newness rules auto-disabled.
170
+ - **`shallow_clone_newness_disabled`** — shallow git clone detected; newness rules auto-disabled (auto-unshallow also failed, or `--no-auto-unshallow` was passed).
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.
171
172
  - **`insufficient_dispersion_<signal>`** — every file in the codebase has the same non-zero raw value for `<signal>`, so its percentile-ranked contribution collapsed to `0.0`. The score is unchanged; the warning flags that the signal is silently uninformative rather than genuinely absent. One per affected signal: `fan_in`, `fan_out`, `complexity`, `churn`, `coverage`, `interaction`, `coupling`.
172
173
  - Language-specific warnings such as `js_depcruise_no_config` when the JS pipeline had to fall back.
173
174
 
data/SIGNALS.md CHANGED
@@ -89,7 +89,7 @@ Three pieces of output serve three purposes. Confusing them causes gate consumer
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
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
- - **`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`.
92
+ - **`evidence`** (0.0–1.0) — a metadata confidence value based on file age, commit count, and optionally coverage presence. The formula is `max(history_only, with_coverage)`, where `history_only = (age + commits) / 2` and `with_coverage = (age + commits + 1.0) / 3` (only included when coverage data is present). Coverage is a bonus: a mature file with full history always reaches `1.0` even without a coverage report. 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
95
95
 
@@ -63,6 +63,7 @@ module StudFinder
63
63
  newness: true,
64
64
  new_file_days: StudFinder::Newness::DEFAULT_DAYS,
65
65
  new_file_min_commits: StudFinder::Newness::DEFAULT_MIN_COMMITS,
66
+ auto_unshallow: true,
66
67
  cli_warnings: []
67
68
  }.freeze
68
69
 
@@ -259,6 +260,9 @@ module StudFinder
259
260
  opts.on('--no-newness', 'Disable new-file classification rules') do
260
261
  @options[:newness] = false
261
262
  end
263
+ opts.on('--no-auto-unshallow', 'Disable automatic git fetch --unshallow on shallow clones') do
264
+ @options[:auto_unshallow] = false
265
+ end
262
266
  opts.on('--verbose', 'Print suppressed per-file warnings to stderr') do
263
267
  @options[:verbose] = true
264
268
  end
@@ -511,8 +515,12 @@ module StudFinder
511
515
 
512
516
  def apply_newness(files, edges, rows, coverage_result = nil)
513
517
  metadata = if @options[:newness] && Newness.shallow_repository?(@repo_path)
514
- @options[:cli_warnings] << Newness::SHALLOW_CLONE_WARNING
515
- Newness.disabled_metadata(files)
518
+ if @options[:auto_unshallow]
519
+ attempt_unshallow(files)
520
+ else
521
+ @options[:cli_warnings] << Newness::SHALLOW_CLONE_WARNING
522
+ Newness.disabled_metadata(files)
523
+ end
516
524
  elsif @options[:newness]
517
525
  Newness.new(repo_path: @repo_path, files: files, days: @options[:new_file_days],
518
526
  min_commits: @options[:new_file_min_commits]).call
@@ -522,6 +530,25 @@ module StudFinder
522
530
  Newness.apply(rows: rows, edges: edges, metadata: metadata, coverage: coverage_result)
523
531
  end
524
532
 
533
+ def attempt_unshallow(files)
534
+ progress('auto-unshallow: fetching full history to compute evidence (use --no-auto-unshallow to skip)')
535
+ Open3.popen3('git', '-C', @repo_path, 'fetch', '--unshallow') do |_stdin, _stdout, _stderr, wait_thread|
536
+ unless wait_thread.join(45)
537
+ Process.kill('TERM', wait_thread.pid)
538
+ Process.kill('KILL', wait_thread.pid) unless wait_thread.join(5)
539
+ raise 'unshallow timed out'
540
+ end
541
+ raise 'unshallow failed' unless wait_thread.value.success?
542
+
543
+ Newness.new(repo_path: @repo_path, files: files, days: @options[:new_file_days],
544
+ min_commits: @options[:new_file_min_commits]).call
545
+ end
546
+ rescue StandardError
547
+ @options[:cli_warnings] << Newness::SHALLOW_CLONE_WARNING
548
+ @options[:cli_warnings] << Newness::UNSHALLOW_FAILED_WARNING
549
+ Newness.disabled_metadata(files)
550
+ end
551
+
525
552
  def loc_percentiles_by_language(files, loc, language_by_file)
526
553
  files.group_by { |file| language_by_file.fetch(file) }.values.reduce({}) do |pct, language_files|
527
554
  pct.merge(Normalizer.percentile_rank(loc, language_files))
@@ -14,6 +14,10 @@ module StudFinder
14
14
  code: 'shallow_clone_newness_disabled',
15
15
  message: 'shallow git clone detected; newness rules disabled (use fetch-depth: 0 in CI for full newness behavior)'
16
16
  }.freeze
17
+ UNSHALLOW_FAILED_WARNING = {
18
+ code: 'shallow_clone_unshallow_failed',
19
+ message: 'auto-unshallow failed; evidence unavailable (use fetch-depth: 0 in CI or pass --no-auto-unshallow)'
20
+ }.freeze
17
21
 
18
22
  History = Struct.new(:first_commit_epoch, :total_commits, keyword_init: true)
19
23
 
@@ -104,9 +108,11 @@ module StudFinder
104
108
 
105
109
  age_component = [metadata.fetch(:age_days, 0) / 30.0, 1.0].min
106
110
  commits_component = [metadata.fetch(:total_commits, 0) / 3.0, 1.0].min
107
- coverage_component = explicit_coverage ? 1.0 : 0.0
108
111
 
109
- ((age_component + commits_component + coverage_component) / 3.0).round(4)
112
+ history_only = (age_component + commits_component) / 2.0
113
+ with_coverage = ((age_component + commits_component + 1.0) / 3.0) if explicit_coverage
114
+
115
+ [history_only, with_coverage].compact.max.round(4)
110
116
  end
111
117
 
112
118
  def self.git_shallow_file?(repo_path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StudFinder
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.1'
5
5
  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.0
4
+ version: 0.5.1
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 00:00:00.000000000 Z
11
+ date: 2026-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: csv