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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! Just-in-time (commit-level) defect-induction risk scoring (issue #331).
//!
//! Where [`score`](crate::vcs::score) ranks *files* at a ref, this module
//! scores a single *commit* at check-in time — the unit a CI gate
//! actually reviews. It is the static, rule-based counterpart to the
//! machine-learning just-in-time (JIT) defect-prediction models in the
//! literature; no model is trained or persisted, so there is nothing to
//! re-fit as a project ages.
//!
//! # Why static rules rather than a model
//!
//! The JIT defect-prediction literature (Kamei et al., *A Large-Scale
//! Empirical Study of Just-in-Time Quality Assurance*, IEEE TSE 39(6),
//! 2013; the systematic survey by Zhao et al. in *ACM Computing
//! Surveys* 55(4), 2022) is mature and consistently finds commit-level
//! prediction high-value at check-in. The survey's key tooling caveat is
//! that trained JIT models **lose predictive power within about a year**
//! and must be re-trained on recent data, so a rule-based scorer with no
//! model to drift is the most maintainable starting point. The signed
//! direction of every term below is taken from that literature, not
//! fitted, so the score needs no retraining.
//!
//! # Features (the Kamei change measures)
//!
//! Grouped exactly as Kamei et al. group them, with the open-source
//! replications [Commit Guru (Rosen, Grawi & Shihab, FSE 2015 tool
//! demo)] and [McIntosh & Kamei, *Are Fix-Inducing Changes a Moving
//! Target?*, IEEE TSE 44(5), 2018] confirming the directions on
//! independent corpora:
//!
//! - **Size** — lines added/deleted, files touched, diff hunks. Larger
//! changes are more defect-prone (Kamei `LA`/`LD`/`NF`).
//! - **Diffusion** — distinct subsystems and directories touched, plus
//! the within-commit change entropy. Scattered changes are riskier
//! (Kamei `NS`/`ND`/`Entropy`).
//! - **History** — the touched files' priors: prior change count,
//! distinct prior authors, prior bug- and security-fix counts, and the
//! composite file-level [`risk_score`](crate::vcs::Stats::risk_score).
//! Files with turbulent history induce more defects (Kamei
//! `NDEV`/`NUC`; the file priors fold in the #328 composite).
//! - **Experience** — the author's prior commit count, long and recent.
//! This term is **negatively** signed: experienced authors induce
//! *fewer* defects (Kamei `EXP`/`REXP`, the one robustly protective
//! signal in their models).
//! - **Purpose** — whether the commit is a fix (itself defect-prone in
//! Kamei's `FIX`), a security fix (weighted higher here), or a revert
//! (corrective, so dampened).
//!
//! # The score is ordinal
//!
//! [`score`] returns a non-negative composite plus its per-group
//! [`JitContributions`] (so a consumer sees *why* a commit scored as it
//! did). Like the file-level risk score it is **ordinal**: rank commits
//! by it, compare a commit against a project's own distribution, but do
//! not read the absolute magnitude as a probability. Any change to the
//! term set or weights **must** bump [`JIT_SCORE_VERSION`].
//!
//! # Scope
//!
//! [`score`] / [`JitReport`] cover a real commit (all five groups).
//! Scoring an arbitrary unprovenanced diff (`bca vcs jit --diff <file>`)
//! is supported as a deliberately *partial* path (issue #580): a bare
//! diff carries no author, parent, or file history, so only the size and
//! diffusion groups are computable. That path produces a distinct
//! [`JitDiffReport`] whose unavailable groups are **absent from the type**
//! (not present as zero), and whose [`partial_risk_score`](JitDiffReport::partial_risk_score)
//! is **not comparable** to a commit score — see [`JitDiffReport`].
//! ML-based JIT and server-side hook integration remain out of scope per
//! issues #331 / #580.
use Serialize;
use ln1p;
/// Version of the composite JIT formula. Increment on any change to the
/// term set, weights, or bumps in [`score`]. Separate from the
/// file-level [`RISK_SCORE_VERSION`](crate::vcs::score::RISK_SCORE_VERSION)
/// so the two scores version independently.
pub const JIT_SCORE_VERSION: u32 = 1;
/// Output-shape version for a [`JitReport`]. Bump on any change to the
/// serialized field set.
///
/// `2`: added the `source` discriminator to [`JitReport`] (issue #642), so
/// commit-mode reports now self-identify like [`JitDiffReport`] already
/// did.
///
/// `3`: renamed the per-commit score key `score` → `risk_score` and the
/// per-diff `partial_score` → `partial_risk_score` (issue #591), aligning
/// the JIT vocabulary with the per-file `risk_score`.
pub const JIT_SCHEMA_VERSION: u32 = 3;
/// Security fixes weigh twice a plain bug fix in the history term, matching
/// the file-level formula's double weight on security-fix history.
const SECURITY_FIX_WEIGHT: f64 = 2.0;
/// File-level `risk_score` magnitudes land in roughly `[0, 15]`; dividing
/// by this keeps the file-prior term on par with the `ln1p` count terms.
const FILE_RISK_SCALE: f64 = 10.0;
/// Size of the change (Kamei `LA`/`LD`/`NF`, plus diff hunks).
/// How widely the change is spread (Kamei `NS`/`ND`/`Entropy`).
/// Priors of the touched files, measured from history *before* the scored
/// commit (Kamei `NDEV`/`NUC` plus the #328 file composite).
/// The author's prior activity (Kamei `EXP`/`REXP`). Higher means more
/// experience, which **lowers** the score.
/// Keyword classification of the commit message (Kamei `FIX`, plus the
/// security and revert refinements this crate already detects).
/// Every numeric feature of one commit, grouped as Kamei groups them. The
/// score's *purpose* term is supplied separately ([`JitPurpose`]) so this
/// struct is the pure numeric feature vector.
/// Per-group contributions to the composite score. They sum to the score
/// before the non-negative floor (so `experience` is typically negative);
/// surfaced so a consumer can see which group drove the result.
/// Structural facts about the scored commit.
/// The full result of scoring one commit: the resolved commit, its
/// features, the per-group contributions, and the composite score.
///
/// Field order keeps every top-level scalar before the nested tables so
/// the report serializes cleanly to TOML (which requires values to
/// precede tables); JSON / YAML readers are order-insensitive.
/// Compute the composite JIT risk score and its per-group breakdown.
///
/// The term weights and signs come from the JIT literature (see the
/// module docs): size, diffusion, and history are positively signed;
/// experience is negative (experienced authors induce fewer defects);
/// purpose adds for fixes and subtracts for reverts. The contributions
/// sum to the score before the final non-negative floor.
///
/// The `f64` casts of count fields are exact for every realistic input
/// (counts never approach 2^53) and the score is ordinal, so the
/// precision lint is allowed locally.
/// The result of scoring an arbitrary unified diff (issue #580).
///
/// A bare diff carries **no author, parent, or file history**, so only the
/// *size* and *diffusion* feature groups are computable. The *history*,
/// *experience*, and *purpose* groups have no input and are therefore
/// **absent from this type entirely** — not present as zero. This is the
/// whole point of a distinct report shape: a consumer cannot read an
/// unavailable group as "low risk", because there is no field to read (the
/// failure mode #580 warns about).
///
/// # Not comparable to a commit score
///
/// [`partial_risk_score`](JitDiffReport::partial_risk_score) sums only the
/// size and diffusion contributions (both ≥ 0) and **omits** history,
/// experience, and purpose. The full [`JitReport::risk_score`] folds those
/// in, but the experience group is *negatively* signed (an experienced
/// author subtracts from the total), so the full commit score is **not**
/// strictly greater than the partial diff score — an experienced author can
/// push it below the partial value. The two scores are therefore **not
/// ordered or comparable**: they live on different scales. Rank diffs
/// against other *diffs*, never against commit scores.
/// The `source` field is a permanent `"diff"` marker so a serialized
/// report is self-identifying.
///
/// Field order keeps every top-level scalar before the nested tables so
/// the report serializes cleanly to TOML.
/// Which input a JIT report was scored from. Serializes to a lowercase
/// string (`"commit"` / `"diff"`) so consumers can branch on it.
/// The contributions available from a bare diff: size and diffusion only.
/// History, experience, and purpose are omitted (no input), so — unlike
/// [`JitContributions`] — there is no zero-valued field a consumer could
/// misread as "this group is low risk".
/// Compute the partial (size + diffusion only) JIT score for an arbitrary
/// diff, reusing the *same* [`score`] math as a commit so the two terms are
/// computed by one code path. The history, experience, and purpose terms
/// are left at their zero defaults (no input), and their zero contributions
/// are discarded — only size and diffusion survive into the returned
/// [`JitDiffContributions`].
///
/// The returned score is **not comparable** to a commit score; see
/// [`JitDiffReport`].
/// Additive adjustments for `FIX` / security / revert (Kamei `FIX` is
/// itself defect-prone, so fixes add; a revert is corrective, so it
/// subtracts).