stud-finder 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 187e021ca929ec91327d22576d8e834b085dfcddcf68915c14ada124b1b99d5d
4
+ data.tar.gz: 840844ff17652ab19c769a52f8bc5a3162de63bb4d47827664f4e9ca58433520
5
+ SHA512:
6
+ metadata.gz: 43b47e35c51c450555ef66ae9f879979242fe8e5cb24119de98ea1b13f12b3befefe8d5f28a4443b35ead8691138eff6214fd6a36a5a5a851f0c4f64a3affae0
7
+ data.tar.gz: '049bd829d40c7f63417c251875670df9f6f1544b53d71d484adb5d0c129b64fdeb6074a6b4fe2fcc79c3cff3972bdd5dfa44827a36ac37d574dd2684f2c66fc9'
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-07-03
9
+
10
+ ### Added
11
+
12
+ - Initial RubyGems release of `stud-finder`, a CLI that ranks files by five risk signals: fan-in (blast radius), fan-out, cyclomatic complexity, git churn, and test coverage.
13
+ - Temporal coupling analysis for identifying files that change together.
14
+ - Diff mode for scoring only files changed in a pull request while preserving repo-relative scores.
15
+ - Ruby and JavaScript/TypeScript support.
16
+ - Table, JSON, CSV, and Markdown output formats.
17
+ - Lexical constant resolution for Ruby fan-in analysis, including the Task 1 fix from PR #33.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fernando Baz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/PRODUCT.md ADDED
@@ -0,0 +1,172 @@
1
+ # stud-finder
2
+
3
+ **Find the files that will hurt you before they do.**
4
+
5
+ ---
6
+
7
+ ## The Problem
8
+
9
+ Every codebase has load-bearing walls. Files that dozens of other files depend on. Files that change every sprint. Files whose complexity means one wrong edit cascades into a day of debugging.
10
+
11
+ Most teams discover these files the hard way — after the incident.
12
+
13
+ stud-finder surfaces them before you touch them.
14
+
15
+ ---
16
+
17
+ ## What It Does
18
+
19
+ stud-finder analyzes a codebase and produces a ranked list of every file, scored by structural risk. Run it before a sprint, before a refactor, before a code review. Know which files deserve extra attention before anyone writes a line.
20
+
21
+ ```
22
+ $ stud-finder ./my-rails-app
23
+
24
+ FILE SCORE LABEL FAN_IN COMPLEXITY CHURN_COMMITS CHURN_LINES CHURN_PCT COVERAGE
25
+ app/models/user.rb 0.91 trunk 0.97 0.42 0.88 0.91 0.89 0.14
26
+ app/services/payment_service.rb 0.84 trunk 0.78 0.91 0.71 0.68 0.69 0.22
27
+ app/controllers/orders_controller 0.73 branch 0.61 0.65 0.74 0.77 0.75 0.31
28
+ ...
29
+ ```
30
+
31
+ Three labels, one decision framework:
32
+
33
+ - **Trunk** — load-bearing. Change with care. High review bar.
34
+ - **Branch** — meaningful coupling. Worth a second look.
35
+ - **Leaf** — isolated. Lower risk. Move fast here.
36
+
37
+ ---
38
+
39
+ ## The Five Signals
40
+
41
+ Each file is scored on up to five independently measured signals, each grounded in decades of software engineering research. M7 introduced scored `fan_out`, so the composite now considers both incoming blast radius and outgoing coupling burden.
42
+
43
+ ### 1. Fan-in — Blast Radius
44
+
45
+ *"How many files depend on this one?"*
46
+
47
+ Rooted in Robert Martin's **afferent coupling (Ca)** metric (1994) and graph theory in-degree analysis. A file with fan_in 60 means 60 other files break if it breaks. The Stable Dependencies Principle says: high-coupling files must be treated as infrastructure.
48
+
49
+ stud-finder builds the dependency graph via static analysis — Zeitwerk constant mapping for Rails, falling back to AST scanning. No runtime instrumentation required.
50
+
51
+ **Weight: 25% of total score**
52
+
53
+ ### 2. Fan-out — Coupling Burden
54
+
55
+ *"How many files does this one depend on?"*
56
+
57
+ Rooted in Robert Martin's **efferent coupling (Ce)** metric. A high `fan_out` file has more direct dependencies to understand, coordinate, and mock in tests. In M7, fan-out moved from an informational column into the scored model.
58
+
59
+ **Weight: 10% of total score**
60
+
61
+ ### 3. Complexity — Cognitive Load
62
+
63
+ *"How hard is this file to reason about?"*
64
+
65
+ Cyclomatic complexity, measured as the **maximum across any single method** in the file. A file with one function of complexity 12 is riskier than a file with ten functions of complexity 3 each — the hardest function determines how deep you have to go.
66
+
67
+ Computed via RuboCop's static analysis engine. No manual annotation.
68
+
69
+ **Weight: 25% of total score**
70
+
71
+ ### 4. Churn — Change Velocity
72
+
73
+ *"How often is this file being touched, and how much?"*
74
+
75
+ A composite signal: 50% commit frequency + 50% lines changed, both percentile-ranked across the full codebase. A file touched in 40 commits but only for small fixes is different from a file touched in 40 commits with major rewrites each time.
76
+
77
+ Computed from git history over a configurable window (default: 180 days). Language-agnostic.
78
+
79
+ **Weight: 25% of total score**
80
+
81
+ ### 5. Coverage — Safety Net
82
+
83
+ *"If this file breaks, will tests catch it?"*
84
+
85
+ Low coverage on a high-risk file is compounded danger — no blast-radius detection, no complexity safety net, no test catch. Coverage is measured as an inverse (0% coverage = maximum penalty), and files absent from the coverage report are handled via coverage fallback rather than penalized falsely.
86
+
87
+ Supports Cobertura XML (RSpec + SimpleCov), LCOV (Jest, lcov), and SimpleCov JSON resultsets. Auto-detected by file extension.
88
+
89
+ **Weight: 15% of total score** (optional — runs as 4-factor model when no coverage report provided)
90
+
91
+ ---
92
+
93
+ ## The Score
94
+
95
+ Each signal is percentile-ranked across the full codebase — so scores are always relative to the project itself, not an external benchmark. A file at the 90th percentile of fan_in has more incoming dependencies than 90% of its peers.
96
+
97
+ The composite score (0.0–1.0) weights the signals and produces the ranked output. Classification thresholds are configurable.
98
+
99
+ **4-factor formula (no coverage):**
100
+ ```
101
+ score = 0.2941 × fan_in_pct + 0.1176 × fan_out_pct + 0.2941 × complexity_pct + 0.2941 × churn_pct
102
+ ```
103
+
104
+ **5-factor formula (with coverage):**
105
+ ```
106
+ score = 0.25 × fan_in_pct + 0.10 × fan_out_pct + 0.25 × complexity_pct + 0.25 × churn_pct + 0.15 × (1 − coverage)
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Use Cases
112
+
113
+ **Pre-sprint risk assessment** — before planning, run stud-finder against the files your team is about to touch. Trunk files get more review time budgeted.
114
+
115
+ **Refactor prioritization** — you have ten candidates for cleanup. stud-finder tells you which ones have the highest blast radius if the refactor goes wrong.
116
+
117
+ **Onboarding** — new engineer joining the team. Here's the trunk map. These are the files you ask before changing.
118
+
119
+ **PR review triage** — reviewer bandwidth is finite. Direct it at the files that matter.
120
+
121
+ **Architecture health monitoring** — run stud-finder weekly. Watch if trunk is growing or shrinking. Trunk growth is a coupling smell.
122
+
123
+ ---
124
+
125
+ ## Technical Foundation
126
+
127
+ - **Language:** Ruby gem, zero runtime instrumentation
128
+ - **Static analysis:** RuboCop (complexity), Zeitwerk + custom AST (fan_in), git log (churn)
129
+ - **Coverage formats:** Cobertura XML, LCOV, SimpleCov JSON — auto-detected
130
+ - **Output formats:** table (default), JSON, CSV, Markdown
131
+ - **Configuration:** CLI flags for weights, thresholds, excludes, churn window
132
+ - **Requires:** Ruby, RuboCop, git. Nothing else for Ruby analysis.
133
+
134
+ ---
135
+
136
+ ## Roadmap
137
+
138
+ **M1–M3 — Complete**
139
+ Initial composite score (Ruby + JS/TS). `--diff-base` / `--only` filter for per-PR output. Per-PR CircleCI integration — stud-finder runs on every PR, posts ranked artifact and PR comment. Non-blocking.
140
+
141
+ **M4 — Complete: fan-out, instability, `stud-finder edges`**
142
+ Fan-out (efferent coupling) and instability (`fan_out / (fan_in + fan_out)`) added to every row in the core output. New `stud-finder edges FILE` subcommand emits the actual dependency edge list for a specific file — dependents and dependencies, both sorted by risk score. Shifts the output from "this file scores high" to "here are the specific files in the blast radius."
143
+
144
+ **M5 — Sentry integration**
145
+ Connect to the Sentry REST API. Parse production stack traces, aggregate error frequency by source file. A runtime signal: not structural approximation but observed failure in production. `--sentry-token`, `--sentry-org`, `--sentry-project` flags. Percentile-ranked and added to the composite score.
146
+
147
+ **M6 — Temporal coupling**
148
+ Co-change frequency from git history: file pairs that change together more often than expected by chance. Surfaces hidden coupling that static analysis cannot see — implicit contracts, shared state, callback side effects. Observed behavior, not structural approximation.
149
+
150
+ **Pinned — Producer-consumer dependency mapping**
151
+ Explicitly surfacing which components consume data produced by other components, flagging pairs with high temporal coupling but low static coupling as candidates for explicit contract documentation.
152
+
153
+ **M7 — Complete: scored fan-out + rankings**
154
+ Scored fan-out introduced as the fifth risk signal with a 10% default weight.
155
+
156
+ **M7 follow-up — Merge-to-staging S3 timeline (lowest priority)**
157
+ Full stud-finder run on each merge to the mainline branch → JSON → S3, keyed by timestamp + commit SHA. Durable risk-over-time feed for trend analysis.
158
+
159
+ **Future — Toward a validated risk estimator**
160
+ Calibrated weights back-tested against bug history. Historical bug density as a direct input metric. Change-scope awareness (per-PR risk = file-risk × change-magnitude × change-type). Test quality beyond line coverage. See `VISION.md` for the full analysis.
161
+
162
+ ---
163
+
164
+ ## Why stud-finder?
165
+
166
+ In construction, a stud finder locates the load-bearing structure inside a wall before you drill. You don't guess — you know exactly where the structure is.
167
+
168
+ Same principle. Before you refactor, before you sprint, before you review — know where the load-bearing code is.
169
+
170
+ ---
171
+
172
+ *Built by Artífice. Ruby gem. Open to collaboration.*
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # stud-finder
2
+
3
+ **Find the files that will hurt you before they do.**
4
+
5
+ A code risk scoring CLI for Ruby and JavaScript/TypeScript codebases. Ranks every file by structural risk so you know where to put your senior review effort, your refactoring time, and your test coverage — *before* the incident.
6
+
7
+ ```
8
+ $ bundle exec bin/stud-finder ./my-rails-app
9
+
10
+ RANK LANGUAGE FILE SCORE CLASS FAN_IN FAN_OUT COMPLEXITY CHURN_COMMITS MAX_COUPLING COUPLING_PARTNERS COVERAGE
11
+ 1 ruby app/models/proficiency.rb 0.91 trunk 223 4 85 11 0.62 3 0.99
12
+ 2 ruby app/services/payment_service.rb 0.84 trunk 78 12 91 42 0.71 5 0.22
13
+ 3 ruby app/controllers/orders_controller 0.73 branch 61 9 65 74 0.48 2 0.31
14
+ 4 js src/components/Dashboard.tsx 0.68 branch 44 18 56 18 0.00 0 —
15
+ ...
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Install
21
+
22
+ After the gem is published:
23
+
24
+ ```bash
25
+ gem install stud-finder
26
+ ```
27
+
28
+ Or add it to your Gemfile:
29
+
30
+ ```ruby
31
+ gem 'stud-finder'
32
+ ```
33
+
34
+ Then run `bundle install`.
35
+
36
+ For edge or unreleased changes, install from git:
37
+
38
+ ```ruby
39
+ gem 'stud-finder', git: 'https://github.com/bazfer/stud-finder'
40
+ ```
41
+
42
+ Or clone and run directly.
43
+
44
+ **Requirements:** Ruby >= 3.2. For JavaScript support, install `dependency-cruiser` and `eslint` in the target project (`npm install -D dependency-cruiser eslint`).
45
+
46
+ ---
47
+
48
+ ## Usage
49
+
50
+ The path is positional. Everything else is optional flags.
51
+
52
+ ```bash
53
+ bundle exec bin/stud-finder PATH [options]
54
+ ```
55
+
56
+ ### Common runs
57
+
58
+ ```bash
59
+ # Basic: rank every file in the project
60
+ bundle exec bin/stud-finder ./my-rails-app
61
+
62
+ # CSV output for spreadsheet review
63
+ bundle exec bin/stud-finder ./my-rails-app --output csv > risk.csv
64
+
65
+ # Top 50 highest-risk files, markdown for a PR comment
66
+ bundle exec bin/stud-finder ./my-rails-app --top 50 --output markdown
67
+
68
+ # With coverage signals (5-factor scoring)
69
+ bundle exec bin/stud-finder ./my-rails-app \
70
+ --ruby-coverage ./coverage/resultset.json \
71
+ --js-coverage ./coverage/lcov.info
72
+ ```
73
+
74
+ ---
75
+
76
+ ## The Five Signals
77
+
78
+ Each file is scored on up to five independently measured signals. See [PRODUCT.md](PRODUCT.md) for the full theory and weighting math.
79
+
80
+ | Signal | What it measures | Weight |
81
+ |--------|------------------|--------|
82
+ | **fan_in** | How many other files depend on this one (blast radius) | 25% |
83
+ | **fan_out** | How many other files this one depends on (its own coupling burden) | 10% |
84
+ | **complexity** | Cyclomatic complexity of the hardest method in the file | 25% |
85
+ | **churn** | Commit frequency + line volume over a 180-day window | 25% |
86
+ | **coverage** | Inverse of line coverage (lower coverage = higher risk) | 15% |
87
+
88
+ When coverage isn't available, the remaining four signals (fan_in, fan_out, complexity, churn) re-normalize to 100% automatically (4-factor mode).
89
+
90
+ ### Informational columns (not scored)
91
+
92
+ These ride alongside the score to give reviewers extra context, but do not contribute to it:
93
+
94
+ - **instability** / **instability_pct** — `fan_out / (fan_in + fan_out)`, and its percentile rank across the repo. High instability = depends on a lot while little depends on it.
95
+ - **max_coupling** / **max_coupling_partner** / **coupling_partners** / **coupling_pct** — temporal coupling from git history: the strongest co-change ratio with any partner file, the path of that strongest partner, how many partners cross the threshold, and the percentile rank of `max_coupling`. The analysis produces co-change pairs; each file's row keeps the strongest pair's ratio (`max_coupling`), that partner's path (`max_coupling_partner`), and the count of pairs (`coupling_partners`). On ties the strongest partner is chosen deterministically: highest coupling, then highest co-change count, then alphabetical path; `max_coupling_partner` is an empty string when a file has no qualifying partners. Computed once over the full file set in the main scan (one extra `git log` pass), so cross-language co-change is captured. Same thresholds as the `edges` subcommand (`--coupling-threshold`, `--coupling-min-commits`).
96
+
97
+ Files are classified into three labels based on their **fan_in percentile** (not the total score):
98
+
99
+ - **trunk** — fan_in in the top 15% (default `trunk_threshold: 85`). Load-bearing. High review bar, change with care.
100
+ - **branch** — fan_in between the 50th and 85th percentile (default `branch_threshold: 50`). Meaningful coupling.
101
+ - **leaf** — everything below the 50th percentile. Isolated. Move fast here.
102
+
103
+ The total score still drives the ranking. The class label is a separate coupling-based signal.
104
+
105
+ ---
106
+
107
+ ## Language Support
108
+
109
+ **Ruby:**
110
+ - fan_in via Zeitwerk constant mapping (Rails-aware), AST fallback
111
+ - complexity via RuboCop
112
+ - coverage: SimpleCov resultset JSON, Cobertura XML
113
+
114
+ **JavaScript / TypeScript (.js, .jsx, .ts, .tsx):**
115
+ - fan_in via `dependency-cruiser` (must be installed in the target project)
116
+ - complexity via `eslint` (`--rule '{"complexity":["error",0]}'`)
117
+ - coverage: LCOV (`.info` format)
118
+
119
+ Each language gets its own ranking section in the output — Ruby and JS are not pooled.
120
+
121
+ ---
122
+
123
+ ## Flag Reference
124
+
125
+ | Flag | Description |
126
+ |------|-------------|
127
+ | `--output FORMAT` | `table` (default), `json`, `markdown`, `csv` |
128
+ | `--ruby-coverage PATH` | Ruby coverage report (SimpleCov `.json` or Cobertura `.xml`) |
129
+ | `--js-coverage PATH` | JavaScript coverage report (LCOV `.info`) |
130
+ | `--coverage PATH` | Deprecated alias for `--ruby-coverage` |
131
+ | `--js-timeout N` | dependency-cruiser timeout in seconds (default: 60) |
132
+ | `--churn-days N` | Commit lookback window in days (default: 180) |
133
+ | `--weights WEIGHTS` | Custom weights as fractions, e.g. `fan_in:0.25,fan_out:0.10,complexity:0.25,churn:0.25,coverage:0.15`. Defaults shown. All five keys are required. |
134
+ | `--trunk-threshold N` | fan_in percentile cutoff for trunk classification (default: 85) |
135
+ | `--branch-threshold N` | fan_in percentile cutoff for branch classification (default: 50) |
136
+ | `--exclude PATTERN` | Exclude glob pattern (repeatable). `spec/` and `test/` excluded by default. |
137
+ | `--top N` | Emit only the top N results |
138
+ | `--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. |
139
+ | `--only PATHS` | Emit only these comma-separated repo-relative paths. Like `--diff-base` but with an explicit list instead of a git diff. Mutually exclusive with `--diff-base`. |
140
+ | `--min-files N` | Advisory minimum file count to trust percentiles (default: 20) |
141
+ | `--verbose` | Print suppressed per-file warnings to stderr |
142
+ | `--version`, `--help` | Self-explanatory |
143
+
144
+ ---
145
+
146
+ ## Output Formats
147
+
148
+ - `table` — human-readable, aligned columns
149
+ - `csv` — spreadsheet-friendly, pipe to a file
150
+ - `json` — machine-readable with `meta`, `warnings`, `ruby`, `javascript` sections
151
+ - `markdown` — drop directly into a PR comment or issue
152
+
153
+ ---
154
+
155
+ ## What It's For
156
+
157
+ Run it:
158
+ - Before a sprint, to see what the team is about to touch
159
+ - Before a major refactor, to identify the load-bearing walls
160
+ - Before a code review, to know which PRs deserve extra scrutiny
161
+ - On every PR in CI, as a risk-tagged diff context
162
+
163
+ Don't run it as a gate — risk isn't a binary blocker. Run it as input to human judgment.
164
+
165
+ ---
166
+
167
+ ## Documentation
168
+
169
+ - **[PRODUCT.md](PRODUCT.md)** — theory, formulas, and the research behind each signal
170
+ - **[VISION.md](VISION.md)** — project vision and positioning
171
+
172
+ ---
173
+
174
+ ## License
175
+
176
+ MIT. See [LICENSE](LICENSE).
data/VISION.md ADDED
@@ -0,0 +1,151 @@
1
+ # stud-finder — Vision & Roadmap to Risk Estimator
2
+
3
+ ## What stud-finder is today
4
+
5
+ A **triage and orientation tool**. It surfaces which files deserve attention before you touch them, based on four structural signals: fan-in (blast radius), complexity (cognitive load), churn (change velocity), and coverage (safety net). Scores are percentile-ranked across the full codebase — so the output is always relative to the project itself.
6
+
7
+ Legitimate uses today:
8
+ - Bootstrapping orientation in an unfamiliar codebase — fast
9
+ - Prioritizing which modules get a formal stabilization review first
10
+ - Making implicit architectural risk explicit ("everyone knows employee.rb is risky" — now there's a number)
11
+ - Directing reviewer bandwidth at the files that matter
12
+
13
+ ---
14
+
15
+ ## The honest limits of the current model
16
+
17
+ **1. Coupling ≠ correctness.**
18
+ Fan-in measures blast radius, not bug probability. High fan-in files (`employee.rb`, `objective_template.rb`) tend to get the most attention, the most tests, the most experienced eyes. They can be riskier to change, but they may also be the best-maintained files in the codebase. High structural score does not mean high bug rate.
19
+
20
+ **2. The weights are invented.**
21
+ `fan_in: 0.35, complexity: 0.25, churn: 0.25, coverage: 0.15` — these were chosen on first principles. They haven't been back-tested against actual bug outcomes. Without fitting, the score is a ranking, not an estimate.
22
+
23
+ **3. Bugs live at interfaces, not in files.**
24
+ The dominant class of production bug — traced across 30 real incidents — is not "a single high-risk file was wrong." It's an implicit contract between a producer and a consumer that was never explicitly defined, then violated by a refactor or a lifecycle change. No individual file scores high; the interface between them is broken. File-level scoring misses this entire class.
25
+
26
+ **4. File-risk ≠ change-risk.**
27
+ A one-line comment edit to `employee.rb` is not the same risk as a 400-line refactor. The current score is on the file, not on the change. Without change-scope awareness, the score can't distinguish.
28
+
29
+ **5. Coverage measures execution, not correctness.**
30
+ Line coverage tells you which lines run during tests. It doesn't tell you whether the tests assert the invariants that matter. Files with 90%+ coverage have produced serious production bugs because the broken invariant was never tested.
31
+
32
+ **6. No runtime signal.**
33
+ Static analysis is backward-looking about structure. Where code is actually failing in production right now is a stronger signal than where it looks risky structurally.
34
+
35
+ ---
36
+
37
+ ## Why similar tools don't fully exist
38
+
39
+ CodeClimate measures complexity + churn. Structure101 measures coupling. Danger and CodeOwners operate on change shape. Nobody has combined all of these into a single validated risk score for review prioritization — because:
40
+
41
+ - Signal-to-noise at the file level is low without calibration against outcomes
42
+ - Teams that need precision use formal methods (property-based testing, TLA+, invariant documentation) on specific risky subsystems — not file-level rankings
43
+ - The actuators (review depth, staging gate) are hard to connect to file topology alone
44
+
45
+ This is not a reason not to build it. It's a reason to be honest about what validation is required before calling it an estimator.
46
+
47
+ ---
48
+
49
+ ## The path to a genuine risk estimator
50
+
51
+ ### Already built (M1–M3)
52
+ - 4-signal composite score (fan_in, complexity, churn, coverage)
53
+ - `--diff-base` / `--only` filter — per-PR output scoped to touched files, full-repo scoring preserved
54
+ - Per-PR CircleCI job — stud-finder runs on every PR, posts markdown + CSV artifact and PR comment
55
+
56
+ ### M4 — Fan-out, instability, and `stud-finder edges`
57
+ Retain the dependency graph that fan_in already builds internally (and discards). Two deliverables:
58
+
59
+ **Fan-out + instability in the core output (every row):**
60
+ - `fan_out` — raw count of files this file depends on (efferent coupling)
61
+ - `instability` — Robert Martin's metric: `fan_out / (fan_in + fan_out)`. Bounded [0, 1]. A file with fan_in=100, fan_out=10 → instability=0.09 (stable). A file with fan_in=2, fan_out=50 → instability=0.96 (fragile consumer).
62
+
63
+ Fan-out captures a different failure mode than fan-in. Several production bugs in the Covalent 2026 incident set were fan-out failures: a consumer depended on a fragile implicit contract, not on a high-blast-radius file. Fan-in alone would not have flagged them.
64
+
65
+ Instability is not yet added to the composite score — first validate it against known fan-out bugs (CO-21367 is the reference case), then calibrate the weight.
66
+
67
+ **`stud-finder edges FILE [PATH]` subcommand:**
68
+ Drill-down for a specific file. Emits:
69
+ - Dependents — files that depend on this file (incoming edges), sorted by score desc
70
+ - Dependencies — files this file depends on (outgoing edges), sorted by score desc
71
+
72
+ ### M5 — Sentry integration
73
+ Query the Sentry REST API for production issues, parse stack trace frames, aggregate by source file. `sentry_events[file]` = distinct production errors that touched this file. Percentile-ranked as a scored signal.
74
+
75
+ This is the only runtime signal in the stack — not "this file looks risky structurally" but "this file is actually in the stack when things break in production." Stronger than any static approximation.
76
+
77
+ CLI: `--sentry-token`, `--sentry-org`, `--sentry-project`. Main implementation challenge: path normalization (Sentry frame paths → repo-relative paths).
78
+
79
+ ### M6 — Temporal coupling
80
+ Co-change frequency from git history: file pairs that change together in the same commit more often than expected by chance. This captures hidden coupling that static analysis cannot see — implicit contracts, shared state, callback side effects that always require coordinated edits.
81
+
82
+ This is the most empirically defensible structural metric in the roadmap — observed behavior in real production git history, not a theoretical approximation. Files that always change together have a hidden dependency. If that dependency is not explicit, it's a risk.
83
+
84
+ ### Pinned — Producer-consumer dependency mapping
85
+ Explicitly mapping what data each component consumes and who produces it. The interface between a data producer (a clone/publish flow, an import pipeline) and a data consumer (a blueprint serializer, a dashboard query) is where the hardest-to-detect bugs live. These interfaces are not visible to fan-in analysis because they operate through data shape (a join table schema, a JSON payload structure) rather than constant references.
86
+
87
+ This is a design artifact — an explicit contract document — not just a metric. The tooling question: can stud-finder surface candidate producer-consumer pairs (files with high temporal coupling but low static coupling) and flag them for explicit contract documentation?
88
+
89
+ ### What's still missing to reach "validated estimator"
90
+
91
+ These five items are the gap between "plausible ranking" and "calibrated risk estimator":
92
+
93
+ **1. Calibrated weights (back-tested against bug history).**
94
+ Run stud-finder against git history at the introducing commit of each known production bug. What were the scores of the introducing files? Fit the weights so the score would have ranked those files highly before the bug surfaced. The seed data for this already exists: a 57-ticket CSV of true production issues with introducing PRs traced. This is a supervised fitting problem — the labels (bug/no-bug) and the features (fan_in, complexity, churn) are both available.
95
+
96
+ **2. Historical bug density as a direct input metric.**
97
+ Count production bugs introduced per file over a trailing window (e.g., 2 years). This is the strongest single signal available — it is the outcome variable itself, used as a predictor. Files that score high structurally AND have prior bug density are high-confidence risk. Files that score high structurally but have zero prior bugs may be well-maintained trunks. Combined with structural signals, this dramatically improves precision.
98
+
99
+ **3. Change-scope awareness (delta-risk = file-risk × change-magnitude × change-type).**
100
+ File-risk × lines changed is a trivially computable multiplier already partially available in the per-PR job. Change-type (touching a public interface vs. an internal method vs. a query) is harder — static analysis cannot distinguish reliably. LLM-based semantic classification of the diff is the natural extension here: classify the change type, feed it into a per-PR risk score that is change-specific, not file-specific.
101
+
102
+ **4. Test quality beyond line coverage.**
103
+ Mutation score — does killing a line cause a test failure? — is expensive to compute but dramatically more signal-rich than line coverage. Even assertion density (assertions per tested line) would improve on raw coverage. The goal: distinguish "this file has 90% coverage with meaningful assertions" from "this file has 90% coverage with happy-path-only tests that missed the broken invariant."
104
+
105
+ **5. Runtime signals (Sentry / error rates per file).**
106
+ Where code is actually failing in production is a stronger signal than where it looks structurally risky. A file with moderate structural score but high Sentry event rate is more dangerous than a high-score file that has never produced an error. Sentry API integration — mapping error events back to source files — would make stud-finder empirical at the risk-in-production level, not just structural.
107
+
108
+ ---
109
+
110
+ ## What each milestone unlocks
111
+
112
+ | Milestone | What it enables |
113
+ |-----------|----------------|
114
+ | M4 `--explain` | Actionable blast-radius view per file; fan-out as a new signal class |
115
+ | M5 temporal coupling | Empirical hidden coupling detection; surfaces implicit contracts |
116
+ | Pinned producer-consumer | Framework for explicit interface contracts; feeds stabilization review docs |
117
+ | Calibrated weights | Score becomes an estimate, not just a ranking |
118
+ | Historical bug density | Strongest predictor; validates structural signals against outcomes |
119
+ | Change-scope awareness | Per-PR risk score, not per-file; connects to actual review decisions |
120
+ | Test quality | Coverage signal becomes meaningful rather than gameable |
121
+ | Runtime signals | Ground truth: where code is failing right now |
122
+
123
+ **Immediately actionable (data already in hand):** calibrated weights + historical bug density. The 57-ticket CSV with introducing PRs is the training set.
124
+
125
+ **Near-term with existing infrastructure:** change-scope awareness (lines diff is in the per-PR job; LLM classification via Covy is a natural extension).
126
+
127
+ **Longer-term:** mutation score, Sentry integration.
128
+
129
+ ---
130
+
131
+ ## The ceiling without validation
132
+
133
+ Without calibrated weights and historical bug density (items 1 and 2 above), stud-finder identifies the right *neighborhoods* to be suspicious of but cannot say how suspicious to be about a specific change. It remains a triage tool — valuable, but not an estimator.
134
+
135
+ With items 1 and 2, stud-finder becomes a validated estimator: the score has a known relationship to bug probability, not just a plausible structural theory. With item 3 (change-scope), it becomes actionable at the PR level — where the review and staging gate decisions actually happen.
136
+
137
+ ---
138
+
139
+ ## Updated milestone roadmap
140
+
141
+ | Milestone | Status | Description |
142
+ |-----------|--------|-------------|
143
+ | M1 | Done | 4-signal composite score, Ruby |
144
+ | M2 | Done | JS/TS support, `--diff-base` / `--only` filter |
145
+ | M3 | Done (PR open) | Per-PR CircleCI job — artifact + PR comment |
146
+ | M4 | Next | Fan-out + instability in core output; `stud-finder edges FILE` subcommand |
147
+ | M5 | Queued | Sentry integration — runtime error frequency as a scored signal |
148
+ | M6 | Queued | Temporal coupling — co-change frequency from git history |
149
+ | Pinned | Queued | Producer-consumer dependency mapping |
150
+ | M7 | Lowest prio | Merge-to-staging S3 timeline producer |
151
+ | Future | Backlog | Calibrated weights, historical bug density, change-scope LLM classification, mutation score |
data/bin/stud-finder ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/stud_finder/cli'
5
+
6
+ exit StudFinder::CLI.start(ARGV)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stud_finder'