1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//! Composite risk-score formulas.
//!
//! Two scores are offered. The default **weighted** score is a
//! log-scaled weighted sum with categorical multiplicative bumps; the
//! **percentile** score re-ranks every signal to its position within
//! the analyzed set and averages. Both are *ordinal*: only relative
//! ranks carry meaning, never the absolute magnitude.
//!
//! # Literature
//!
//! The term weights and thresholds are grounded in the defect- and
//! vulnerability-prediction literature synthesised on issue #328:
//!
//! - Recent churn and recent commit count carry the highest weight —
//! Nagappan & Ball's relative-churn measures and the just-in-time
//! defect-prediction line both find recent change activity the
//! strongest single signal (Firefox `LinesChanged` PD 85,
//! `NumChanges` PD 86).
//! - The author factor is multiplied by an ownership-dilution term
//! `(1 - ownership_top_share)`: diffuse ownership predicts defects
//! (Avelino DoA / truck-factor; Bird et al.).
//! - Categorical developer-count bumps encode the RHEL4 finding that
//! files touched by ≥9 developers were ~16× more likely to harbour a
//! vulnerability, with a softer bump at the 6-developer mark.
//! - A new-file bump reflects the Chromium observation that newly
//! added features carry elevated risk.
//! - Bug-fix and security-fix commit counts feed a log-scaled additive
//! term, security fixes double-weighted (Sentence-Level VFC studies;
//! PySecDB).
//! - File size enters as `ln1p(sloc)^2 / 100` with a full coefficient of
//! 1.0, so it is a meaningful contributor rather than a tiny tie-breaker:
//! ~0.33 at 300 SLOC, ~0.48 at 1k, ~0.85 at 10k, and >1.0 past ~50k SLOC.
//! Those magnitudes are comparable to the churn terms (a recency-churn
//! "point" is `0.30*ln1p(50)` ≈ 1.18) and dwarf the entropy (≤0.15) and
//! long-churn (≈0.27 at heavy churn) terms. Large files are only weakly
//! correlated with defects, but the squared-log scaling keeps size a
//! first-class additive signal across realistic file sizes.
//! - **v2** adds two recent-window process-entropy terms (issue #330):
//! *change entropy* (Hassan 2009; file-level Pearson 0.54 with defects
//! on Apache projects) and *co-change graph entropy* (arXiv 2504.18511,
//! 2025; combining the two improved AUROC in 82.5% of cases). Both
//! enter additively, weighted below recent churn/commits but on par
//! with the fix term — they are bounded per file and complement, rather
//! than restate, the v1 churn/commit signals.
//!
//! Bumping *either* formula in any way **must** increment
//! [`RISK_SCORE_VERSION`] so downstream consumers can detect the
//! change. Both the weighted sum ([`weighted`]) and the percentile
//! blend ([`apply_percentile`]) stamp and are cache-keyed on that one
//! constant, so it versions both — see its doc for the full contract.
use Stats;
/// Version of the risk-score computation, covering **both** formulas.
/// A single `risk_score_version` is stamped on every file's output
/// regardless of the active [`super::RiskFormula`] (the weighted sum or
/// the percentile blend), and the persistent VCS cache keys reuse on it
/// (see [`super::cache`]). Increment on any change that can alter an
/// emitted `risk_score`, including:
///
/// - the weighted formula ([`weighted`]): its term set, weights, or
/// categorical bumps below; and
/// - the percentile blend ([`apply_percentile`]): its extractor set,
/// mid-rank scaling, or normalization.
///
/// `2` added the change- and co-change-entropy terms to both formulas
/// (issue #330).
pub const RISK_SCORE_VERSION: u32 = 2;
/// RHEL4 high-developer-count threshold (~16× vulnerability likelihood).
const HIGH_DEV_THRESHOLD: u32 = 9;
/// RHEL4 softer developer-count threshold.
const MID_DEV_THRESHOLD: u32 = 6;
/// Multiplicative bump applied at or above [`HIGH_DEV_THRESHOLD`].
const HIGH_DEV_BONUS: f64 = 0.35;
/// Multiplicative bump applied at or above [`MID_DEV_THRESHOLD`].
const MID_DEV_BONUS: f64 = 0.15;
/// Multiplicative bump for a file first seen within the recent window.
const NEW_FILE_BONUS: f64 = 0.15;
/// Absolute tolerance for treating two percentile signals as tied. All
/// signals are integer counts or ratios in `[0, 1]`, so `1e-9` is far
/// below any meaningful difference yet absorbs floating-point ratio drift.
const TIE_TOLERANCE: f64 = 1e-9;
/// Raw signals consumed by the weighted formula. Decoupled from
/// [`Stats`] so the formula is unit-testable on synthetic inputs
/// without constructing a full stats record.
/// Compute the weighted composite risk score for one file.
///
/// See the module docs for the term-by-term literature grounding. The
/// `f64` casts of count fields are exact for every realistic input
/// (counts never exceed 2^53) and the score is ordinal, so the lints
/// are allowed locally with that justification.
/// `ln(1 + x)` guarding against a negative argument from clock skew or
/// an upstream miscount; the domain of every caller is non-negative, so
/// the clamp is purely defensive. Shared with the JIT formula
/// ([`super::jit`]) so the two scores log-scale counts identically.
pub
/// Recompute every file's `risk_score` as the mean percentile rank of
/// its signals within the analyzed set (the `--risk-formula percentile`
/// mode).
///
/// Each signal is ranked independently using the *mid-rank* of ties
/// (so identical values share a percentile), scaled to `[0, 100]`, and
/// the per-file mean across signals becomes the score. With fewer than
/// two files percentiles are undefined, so the weighted score is left
/// in place.
///
/// The `u64 → f64` signal casts are exact for every realistic churn
/// count (well under 2^53) and the result is ordinal, so the precision
/// lint is allowed for the whole pass.
///
/// The resulting `risk_score` is stamped with and cache-keyed on
/// [`RISK_SCORE_VERSION`] (the same constant as the weighted formula),
/// so any change to this blend — the extractor set, the mid-rank
/// scaling, or the `/ signal_count * 100` normalization — **must**
/// increment that constant.