software_engineering/pull_request_metrics.rs
1//! # Pull Request and Code Review Metrics
2//!
3//! Code review is usually the single largest wait-time contributor inside
4//! the cycle-time breakdown, and it is also the stage most directly under a
5//! team's own control to improve. This module covers two of that stage's
6//! core metrics: **time to first review**, the dominant wait-time lever, and
7//! **reviewer load concentration**, a way to surface an otherwise-invisible
8//! bottleneck and bus-factor risk in who does the reviewing.
9//!
10//! ## Formula
11//!
12//! ```text
13//! Time to first review = t(first substantive comment or approval) - t(opened)
14//! Reviewer load concentration = max(reviews per reviewer) / mean(reviews per reviewer)
15//! ```
16//!
17//! ## Why it matters
18//!
19//! Most delay in the review stage comes from a pull request waiting to be
20//! looked at, not from the review conversation taking long once it starts —
21//! which is why time to first review, instrumented automatically from the
22//! version control platform, "typically produces the largest single
23//! improvement to overall cycle time available to a team." Separately,
24//! reviewer load is commonly concentrated on a small number of people
25//! without anyone measuring it directly: an enterprise example in the book
26//! found "a handful of principal engineers were completing over 40% of all
27//! code reviews across a two-hundred-person organization." That
28//! concentration is both a bottleneck, since those engineers' availability
29//! caps the whole team's review throughput, and a burnout risk.
30//!
31//! ## Example
32//!
33//! ```rust
34//! use software_engineering::pull_request_metrics::{
35//! time_to_first_review, reviewer_load_concentration_ratio,
36//! };
37//!
38//! // A pull request opened at hour 0 gets its first review comment at hour 5.
39//! assert_eq!(time_to_first_review(0.0, 5.0), 5.0);
40//!
41//! // Five reviewers complete 40, 10, 10, 10, and 10 reviews in a quarter:
42//! // one reviewer is doing 2.5x the average review load.
43//! let reviews = [40.0, 10.0, 10.0, 10.0, 10.0];
44//! let ratio = reviewer_load_concentration_ratio(&reviews).unwrap();
45//! assert!((ratio - 2.5).abs() < 1e-9);
46//! ```
47//!
48//! ## Pitfalls
49//!
50//! - **Optimizing time to first review without a paired quality guardrail**
51//! invites rubber-stamp approval that defeats review's purpose; a fast
52//! approval with no real scrutiny is worse than a slower, genuine one.
53//! - **Reviewer load concentration is a diagnostic system signal for
54//! spotting bottleneck and bus-factor risk — never an individual
55//! performance scorecard.** The book is explicit that review-related
56//! counts are "more often a system or communication signal than a
57//! personal one," and warns directly against "the evaluative drift"
58//! of treating them as a judgement on any one reviewer or author. Use a
59//! high ratio to prompt rotation and knowledge-sharing, not to rank or
60//! evaluate the individuals involved.
61//! - **Ignoring reviewer load concentration** leaves it invisible until it
62//! surfaces as a bottleneck (the concentrated reviewers' availability caps
63//! throughput) or a burnout event.
64//!
65//! ## Sources
66//!
67//! - Chapter 2.9, Pull request and code review metrics.
68//!
69//! Topic doc: software-engineering-metrics/locales/en-001/chapters/02-09-pull-request-and-code-review-metrics.md
70
71/// Time to first review: the interval from a pull request being opened to a
72/// reviewer's first substantive comment or approval.
73///
74/// The book identifies this as "usually the dominant wait-time contributor"
75/// within the review stage, and improving it typically produces the largest
76/// single improvement to overall cycle time available to a team.
77///
78/// # Arguments
79///
80/// * `opened_at` — the time the pull request was opened (any consistent time
81/// unit, e.g. hours since epoch).
82/// * `first_response_at` — the time of the reviewer's first substantive
83/// comment or approval, in the same unit.
84///
85/// # Returns
86///
87/// The elapsed time between opening and first review response, in the same
88/// unit as the inputs.
89///
90/// # Examples
91///
92/// ```rust
93/// use software_engineering::pull_request_metrics::time_to_first_review;
94///
95/// // Opened at hour 10, first reviewed at hour 34: an 18-hour wait.
96/// assert_eq!(time_to_first_review(10.0, 34.0), 24.0);
97/// ```
98#[must_use]
99pub fn time_to_first_review(opened_at: f64, first_response_at: f64) -> f64 {
100 first_response_at - opened_at
101}
102
103/// Reviewer load concentration ratio: the busiest reviewer's review count
104/// divided by the mean review count across all reviewers.
105///
106/// This is a **diagnostic system signal**, not an individual performance
107/// scorecard. The book's own worked example, "a handful of principal
108/// engineers were completing over 40% of all code reviews across a
109/// two-hundred-person organization," is precisely the pattern this ratio is
110/// meant to surface: a bottleneck (the concentrated reviewers' availability
111/// caps team-wide review throughput) and a burnout risk, not evidence that
112/// any individual reviewer is doing something wrong. Use a high ratio to
113/// prompt review rotation and knowledge-sharing — never to rank or evaluate
114/// individual reviewers.
115///
116/// # Arguments
117///
118/// * `reviews_per_reviewer` — completed review counts for each reviewer over
119/// a rolling window.
120///
121/// # Returns
122///
123/// `Some(ratio)` where `ratio` is the maximum value divided by the mean of
124/// `reviews_per_reviewer`; `None` when the slice is empty or the mean is
125/// zero (ratio undefined). A ratio near `1.0` indicates evenly distributed
126/// review load; a high ratio indicates concentration on a small number of
127/// people.
128///
129/// # Examples
130///
131/// ```rust
132/// use software_engineering::pull_request_metrics::reviewer_load_concentration_ratio;
133///
134/// // Evenly distributed load: ratio is 1.0.
135/// let even = [10.0, 10.0, 10.0, 10.0];
136/// assert!((reviewer_load_concentration_ratio(&even).unwrap() - 1.0).abs() < 1e-9);
137///
138/// // Concentrated load: one reviewer far above the mean.
139/// let concentrated = [40.0, 10.0, 10.0, 10.0, 10.0];
140/// assert!((reviewer_load_concentration_ratio(&concentrated).unwrap() - 2.5).abs() < 1e-9);
141///
142/// assert_eq!(reviewer_load_concentration_ratio(&[]), None);
143/// ```
144#[must_use]
145pub fn reviewer_load_concentration_ratio(reviews_per_reviewer: &[f64]) -> Option<f64> {
146 if reviews_per_reviewer.is_empty() {
147 return None;
148 }
149 let sum: f64 = reviews_per_reviewer.iter().sum();
150 // Review counts never approach f64's precision limit, so this cast
151 // never loses precision in practice.
152 #[allow(clippy::cast_precision_loss)]
153 let count = reviews_per_reviewer.len() as f64;
154 let mean = sum / count;
155 if mean == 0.0 {
156 return None;
157 }
158 let max = reviews_per_reviewer
159 .iter()
160 .copied()
161 .fold(f64::MIN, f64::max);
162 Some(max / mean)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 // "Measure the interval from a pull request being opened to a
170 // reviewer's first substantive comment or approval."
171 #[test]
172 fn time_to_first_review_is_first_response_minus_opened() {
173 assert!((time_to_first_review(10.0, 34.0) - 24.0).abs() < 1e-9);
174 assert!((time_to_first_review(0.0, 5.0) - 5.0).abs() < 1e-9);
175 }
176
177 // Evenly distributed review load produces a concentration ratio of 1.0.
178 #[test]
179 fn even_review_load_has_ratio_of_one() {
180 let even = [10.0, 10.0, 10.0, 10.0];
181 assert!((reviewer_load_concentration_ratio(&even).unwrap() - 1.0).abs() < 1e-9);
182 }
183
184 // "A handful of principal engineers were completing over 40% of all
185 // code reviews across a two-hundred-person organization" — a
186 // worked example of concentrated review load, here as one reviewer
187 // doing 2.5 times the team's average.
188 #[test]
189 fn concentrated_review_load_has_high_ratio() {
190 let concentrated = [40.0, 10.0, 10.0, 10.0, 10.0];
191 let ratio = reviewer_load_concentration_ratio(&concentrated).unwrap();
192 assert!((ratio - 2.5).abs() < 1e-9);
193 }
194
195 // Undefined for an empty slice or an all-zero slice (mean is zero).
196 #[test]
197 fn ratio_is_none_for_empty_or_zero_mean_input() {
198 assert_eq!(reviewer_load_concentration_ratio(&[]), None);
199 assert_eq!(reviewer_load_concentration_ratio(&[0.0, 0.0, 0.0]), None);
200 }
201}