Skip to main content

software_engineering/
escaped_defects.rs

1//! # Escaped Defect Rate and Quality Escapes
2//!
3//! An **escaped defect** is one that reaches production rather than being
4//! caught before release. The escaped defect rate compares how many defects
5//! escaped against how many were found in total (pre- and post-release
6//! combined), giving a direct read on how well internal quality practices
7//! are catching problems before customers do. A raw count understates the
8//! picture: weighting by severity, using a consistent, documented scale,
9//! stops a spike in minor issues from visually swamping a smaller but far
10//! more consequential rise in critical ones.
11//!
12//! ## Formula
13//!
14//! ```text
15//! Escaped defect rate (%) = escaped defects / (escaped defects + caught defects) × 100
16//! Severity-weighted score  = critical × 5 + major × 3 + minor × 1
17//! ```
18//!
19//! ## Why it matters
20//!
21//! Classifying every escaped defect on a fixed severity scale, based on
22//! actual customer or business impact, and tracking a severity-weighted
23//! trend rather than just a raw count, is what keeps a handful of critical
24//! escapes from being buried under a much larger count of cosmetic ones.
25//! Standardizing classification criteria across teams matters just as much:
26//! left to classify independently, teams drift toward different standards,
27//! making cross-team comparison meaningless and creating an incentive to
28//! classify generously downward to flatter a team's own numbers.
29//!
30//! ## Example
31//!
32//! ```rust
33//! use software_engineering::escaped_defects::{
34//!     escaped_defect_rate_percent, severity_weighted_escaped_defect_score,
35//! };
36//!
37//! // 4 defects escaped to production out of 40 found in total.
38//! let rate = escaped_defect_rate_percent(4.0, 36.0).unwrap();
39//! assert_eq!(rate, 10.0);
40//!
41//! // 1 critical escape outweighs 4 minor ones under severity weighting.
42//! let one_critical = severity_weighted_escaped_defect_score(1.0, 0.0, 0.0);
43//! let four_minor = severity_weighted_escaped_defect_score(0.0, 0.0, 4.0);
44//! assert!(one_critical > four_minor);
45//! // But 10 minors already outweigh a single critical.
46//! let ten_minor = severity_weighted_escaped_defect_score(0.0, 0.0, 10.0);
47//! assert!(ten_minor > one_critical);
48//! // And 3 criticals outweigh those same 10 minors.
49//! let three_critical = severity_weighted_escaped_defect_score(3.0, 0.0, 0.0);
50//! assert!(three_critical > ten_minor);
51//! ```
52//!
53//! ## Pitfalls
54//!
55//! - **Tracking a raw escaped-defect count** instead of a severity-weighted
56//!   trend — lets a spike in minor issues visually swamp a smaller, more
57//!   consequential rise in critical ones.
58//! - **Letting teams classify severity independently**, without a
59//!   documented, audited scale — produces cross-team comparisons that are
60//!   meaningless at best and gamed at worst.
61//! - **Tracking count and severity without root cause** — misses the
62//!   systemic pattern (a testing gap, a missed edge case, an
63//!   environment difference) that would point at a specific, fixable
64//!   process gap.
65//! - **Framing defect classification as an individual-blame exercise** —
66//!   creates a strong incentive to under-report or misclassify downward.
67//!
68//! ## Sources
69//!
70//! - Chapter 5.1, Escaped defect rate and quality escapes.
71//!
72//! Topic doc: software-engineering-metrics/locales/en-001/chapters/05-01-escaped-defect-rate-and-quality-escapes.md
73
74/// Weight applied to a critical-severity escaped defect in
75/// [`severity_weighted_escaped_defect_score`].
76///
77/// A common, documented convention, not a universal constant — teams should
78/// adapt the scale to their own context, per the chapter's "consistent,
79/// documented scale" guidance.
80pub const CRITICAL_WEIGHT: f64 = 5.0;
81
82/// Weight applied to a major-severity escaped defect in
83/// [`severity_weighted_escaped_defect_score`]. See [`CRITICAL_WEIGHT`].
84pub const MAJOR_WEIGHT: f64 = 3.0;
85
86/// Weight applied to a minor-severity escaped defect in
87/// [`severity_weighted_escaped_defect_score`]. See [`CRITICAL_WEIGHT`].
88pub const MINOR_WEIGHT: f64 = 1.0;
89
90/// Escaped defect rate: the percentage of all found defects that escaped to
91/// production rather than being caught first.
92///
93/// `escaped_defects / (escaped_defects + caught_defects) × 100`.
94///
95/// # Arguments
96///
97/// * `escaped_defects` — count of defects found in production.
98/// * `caught_defects` — count of defects found before release.
99///
100/// # Returns
101///
102/// The escaped defect rate as a percentage, or `None` if both counts are
103/// zero.
104///
105/// # Examples
106///
107/// ```rust
108/// use software_engineering::escaped_defects::escaped_defect_rate_percent;
109///
110/// assert_eq!(escaped_defect_rate_percent(4.0, 36.0), Some(10.0));
111/// assert_eq!(escaped_defect_rate_percent(0.0, 0.0), None);
112/// ```
113#[must_use]
114pub fn escaped_defect_rate_percent(escaped_defects: f64, caught_defects: f64) -> Option<f64> {
115    let total = escaped_defects + caught_defects;
116    if total == 0.0 {
117        return None;
118    }
119    Some((escaped_defects / total) * 100.0)
120}
121
122/// A severity-weighted escaped-defect score, so a spike in minor issues
123/// cannot visually swamp a smaller rise in critical ones.
124///
125/// `critical × `[`CRITICAL_WEIGHT`]` + major × `[`MAJOR_WEIGHT`]` + minor ×
126/// `[`MINOR_WEIGHT`].
127///
128/// # Arguments
129///
130/// * `critical` — count of critical-severity escaped defects.
131/// * `major` — count of major-severity escaped defects.
132/// * `minor` — count of minor-severity escaped defects.
133///
134/// # Returns
135///
136/// The severity-weighted score.
137///
138/// # Examples
139///
140/// ```rust
141/// use software_engineering::escaped_defects::severity_weighted_escaped_defect_score;
142///
143/// // A single critical escape outweighs 4 minor ones.
144/// let critical = severity_weighted_escaped_defect_score(1.0, 0.0, 0.0);
145/// let minors = severity_weighted_escaped_defect_score(0.0, 0.0, 4.0);
146/// assert!(critical > minors);
147/// ```
148#[must_use]
149pub fn severity_weighted_escaped_defect_score(critical: f64, major: f64, minor: f64) -> f64 {
150    critical * CRITICAL_WEIGHT + major * MAJOR_WEIGHT + minor * MINOR_WEIGHT
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    // "escaped defect rate" as a share of all defects found, pre- and
158    // post-release combined.
159    #[test]
160    fn escaped_defect_rate_computes_percentage_of_total_found() {
161        let rate = escaped_defect_rate_percent(4.0, 36.0).unwrap();
162        assert!((rate - 10.0).abs() < 1e-9);
163    }
164
165    #[test]
166    fn escaped_defect_rate_is_none_when_nothing_was_found() {
167        assert_eq!(escaped_defect_rate_percent(0.0, 0.0), None);
168    }
169
170    // "Track a severity-weighted trend, not just a raw count, so that a
171    // spike in minor issues does not visually swamp a smaller but far more
172    // consequential increase in critical ones."
173    #[test]
174    fn a_handful_of_criticals_outweighs_many_minors() {
175        let ten_minor = severity_weighted_escaped_defect_score(0.0, 0.0, 10.0);
176        let three_critical = severity_weighted_escaped_defect_score(3.0, 0.0, 0.0);
177        assert!(three_critical > ten_minor);
178    }
179
180    #[test]
181    fn severity_weighted_score_is_zero_with_no_escapes() {
182        let score = severity_weighted_escaped_defect_score(0.0, 0.0, 0.0);
183        assert!((score - 0.0).abs() < 1e-9);
184    }
185}