software_engineering/maturity_model.rs
1//! # Maturity Model for Engineering Metrics Programs
2//!
3//! A metrics program's overall maturity is scored across five dimensions:
4//! **governance**, **instrumentation**, **outcome balance**, **cultural
5//! trust**, and **continuous improvement**, each independently on a 1–5
6//! level scale (Level 1, Initiate, through Level 5, Orchestrate). The
7//! chapter's central recommendation is to take the *minimum* across
8//! dimensions as the honest overall score, resisting the temptation to
9//! average them into a more flattering composite.
10//!
11//! ## Formula
12//!
13//! ```text
14//! Honest overall score = minimum(governance, instrumentation, outcome balance,
15//! cultural trust, continuous improvement)
16//! (Average is computed alongside it only to make the gap visible.)
17//! ```
18//!
19//! ## Why it matters
20//!
21//! A programme with excellent instrumentation (Level 4) but weak cultural
22//! trust (Level 1) is not, in any meaningful sense, a Level 2 or 3
23//! programme; the weak dimension actively undermines the value of the
24//! strong ones, since untrustworthy data corrupted by fear-driven gaming is
25//! not rescued by having been collected with excellent instrumentation.
26//! Reporting the minimum, even though it produces a less flattering overall
27//! picture than an average would, is what keeps the assessment honest.
28//!
29//! ## Example
30//!
31//! ```rust
32//! use software_engineering::maturity_model::{
33//! MaturityDimension, minimum_maturity_level, average_maturity_level,
34//! };
35//!
36//! // Instrumentation through continuous improvement score well, but
37//! // cultural trust lags badly.
38//! let scored: [(MaturityDimension, u8); 5] = [
39//! (MaturityDimension::Governance, 4),
40//! (MaturityDimension::Instrumentation, 4),
41//! (MaturityDimension::OutcomeBalance, 4),
42//! (MaturityDimension::CulturalTrust, 1),
43//! (MaturityDimension::ContinuousImprovement, 4),
44//! ];
45//! let scores: Vec<u8> = scored.iter().map(|(_, level)| *level).collect();
46//!
47//! let honest_score = minimum_maturity_level(&scores).unwrap();
48//! let flattering_average = average_maturity_level(&scores).unwrap();
49//! assert_eq!(honest_score, 1);
50//! assert!((flattering_average - 3.4).abs() < 1e-9);
51//! assert!((flattering_average as f64) > (honest_score as f64));
52//! ```
53//!
54//! ## Pitfalls
55//!
56//! - **Averaging the five dimension scores** into a single, more flattering
57//! composite, instead of reporting the minimum — hides exactly the weak
58//! dimension that undermines the rest.
59//! - **Assessing only aspirationally**, based on stated policy rather than
60//! concrete evidence for each dimension.
61//! - **Reassessing only after a crisis** forces the question reactively,
62//! rather than on a fixed, regular cadence.
63//! - **Treating a low score as a verdict to feel bad about**, rather than
64//! the diagnostic starting point for a targeted investment plan.
65//!
66//! ## Sources
67//!
68//! - Chapter 8.4, Maturity model for engineering metrics programs.
69//! - *Capability Maturity Model Integration (CMMI)*, Software Engineering
70//! Institute (structural inspiration).
71//!
72//! Topic doc: software-engineering-metrics/locales/en-001/chapters/08-04-maturity-model-for-engineering-metrics-programs.md
73
74/// One of the five maturity-model dimensions, in the chapter's own order.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum MaturityDimension {
77 /// Whether every consequential metric has a named owner and a
78 /// documented charter.
79 Governance,
80 /// Whether metrics come from automated sources rather than self-report
81 /// wherever possible.
82 Instrumentation,
83 /// The actual ratio of outcome-weighted to output-weighted metrics on
84 /// primary dashboards.
85 OutcomeBalance,
86 /// Whether rollout history has ever included a mishandled, punitive use
87 /// of a metric, and how it was addressed.
88 CulturalTrust,
89 /// Whether the organization has a documented history of retiring
90 /// metrics that stopped earning their keep.
91 ContinuousImprovement,
92}
93
94/// The minimum score across a set of per-dimension maturity levels — the
95/// chapter's recommended honest overall score.
96///
97/// Each score is expected to be in `1..=5`, but this function itself simply
98/// takes the minimum of whatever values are given; validating the range is
99/// the caller's responsibility.
100///
101/// # Arguments
102///
103/// * `scores` — one maturity level per dimension assessed.
104///
105/// # Returns
106///
107/// The minimum level, or `None` if `scores` is empty.
108///
109/// # Examples
110///
111/// ```rust
112/// use software_engineering::maturity_model::minimum_maturity_level;
113///
114/// assert_eq!(minimum_maturity_level(&[4, 4, 4, 1, 4]), Some(1));
115/// assert_eq!(minimum_maturity_level(&[]), None);
116/// ```
117#[must_use]
118pub fn minimum_maturity_level(scores: &[u8]) -> Option<u8> {
119 scores.iter().copied().min()
120}
121
122/// The arithmetic mean across a set of per-dimension maturity levels — the
123/// more flattering composite the chapter explicitly warns against using as
124/// the *overall* score. Kept here so callers can compute it alongside
125/// [`minimum_maturity_level`] and see the gap between the two.
126///
127/// # Arguments
128///
129/// * `scores` — one maturity level per dimension assessed.
130///
131/// # Returns
132///
133/// The mean level as an `f64`, or `None` if `scores` is empty.
134///
135/// # Examples
136///
137/// ```rust
138/// use software_engineering::maturity_model::average_maturity_level;
139///
140/// let average = average_maturity_level(&[4, 4, 4, 1, 4]).unwrap();
141/// assert!((average - 3.4).abs() < 1e-9);
142/// assert_eq!(average_maturity_level(&[]), None);
143/// ```
144#[must_use]
145pub fn average_maturity_level(scores: &[u8]) -> Option<f64> {
146 if scores.is_empty() {
147 return None;
148 }
149 let sum: u32 = scores.iter().map(|&level| u32::from(level)).sum();
150 // Maturity levels are always small integers (1..=5), so this cast is
151 // always exact.
152 #[allow(clippy::cast_precision_loss)]
153 let count = scores.len() as f64;
154 Some(f64::from(sum) / count)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn minimum_maturity_level_is_none_for_empty_scores() {
163 assert_eq!(minimum_maturity_level(&[]), None);
164 }
165
166 #[test]
167 fn average_maturity_level_is_none_for_empty_scores() {
168 assert_eq!(average_maturity_level(&[]), None);
169 }
170
171 // "Take the minimum across dimensions as your honest overall score...
172 // A programme with excellent instrumentation (Level 4) but weak
173 // cultural trust (Level 1) is not, in any meaningful sense, a Level 2
174 // or 3 programme."
175 #[test]
176 fn minimum_reveals_the_weak_dimension_the_average_hides() {
177 let scores = [4, 4, 4, 1, 4];
178 let honest_score = minimum_maturity_level(&scores).unwrap();
179 let flattering_average = average_maturity_level(&scores).unwrap();
180 assert_eq!(honest_score, 1);
181 assert!((flattering_average - 3.4).abs() < 1e-9);
182 assert!(f64::from(honest_score) < flattering_average);
183 }
184
185 #[test]
186 fn a_uniformly_scored_programme_has_matching_minimum_and_average() {
187 let scores = [3, 3, 3, 3, 3];
188 let minimum = minimum_maturity_level(&scores).unwrap();
189 let average = average_maturity_level(&scores).unwrap();
190 assert_eq!(minimum, 3);
191 assert!((average - 3.0).abs() < 1e-9);
192 }
193}