software_engineering/space_framework.rs
1//! # The SPACE Framework
2//!
3//! SPACE measures developer productivity across five dimensions instead of
4//! one: **Satisfaction and well-being**, **Performance**, **Activity**,
5//! **Communication and collaboration**, and **Efficiency and flow**. No
6//! single dimension is meant to stand alone; the framework's real
7//! contribution is the discipline of holding all five in view together, so a
8//! team cannot look productive on one axis while quietly damaging another.
9//!
10//! ## Formula
11//!
12//! ```text
13//! SPACE dimensions (five, not a computed ratio):
14//! S = Satisfaction and well-being
15//! P = Performance
16//! A = Activity
17//! C = Communication and collaboration
18//! E = Efficiency and flow
19//!
20//! Minimum balanced composition:
21//! at least one metric from at least 3 of the 5 dimensions,
22//! mixing objective instrumentation with subjective survey data
23//! ```
24//!
25//! ## Why it matters
26//!
27//! Single-number developer productivity metrics — lines of code, commit
28//! count, story points — are trivially gamed and routinely mislead. A team
29//! can be highly active while performing poorly, or perform well in the
30//! short term while satisfaction craters, a leading indicator of the
31//! attrition and quality collapse that shows up months later. A
32//! single-dimension metric set is a known anti-pattern: the book insists on
33//! covering multiple dimensions together, mixing subjective (survey) and
34//! objective (instrumented) data sources.
35//!
36//! ## Example
37//!
38//! ```rust
39//! use software_engineering::space_framework::{
40//! SpaceDimension, covers_all_dimensions, missing_dimensions,
41//! };
42//!
43//! // A team that tracks only Activity is the book's classic anti-pattern.
44//! let activity_only = [SpaceDimension::Activity];
45//! assert!(!covers_all_dimensions(&activity_only));
46//! assert_eq!(missing_dimensions(&activity_only).len(), 4);
47//!
48//! // A team that tracks all five is fully covered.
49//! let all = [
50//! SpaceDimension::Satisfaction,
51//! SpaceDimension::Performance,
52//! SpaceDimension::Activity,
53//! SpaceDimension::Communication,
54//! SpaceDimension::Efficiency,
55//! ];
56//! assert!(covers_all_dimensions(&all));
57//! assert!(missing_dimensions(&all).is_empty());
58//! ```
59//!
60//! ## Pitfalls
61//!
62//! - **Adopting SPACE in name only** while remaining activity-dominated in
63//! practice defeats the framework's entire purpose.
64//! - **Applying SPACE dimensions to individual scorecards** misapplies a
65//! framework validated for team and system-level insight, not individual
66//! performance.
67//! - **Reviewing dimensions in isolation** rather than watching for
68//! cross-dimensional trade-offs misses the pattern SPACE is specifically
69//! designed to catch.
70//! - **Treating a single satisfaction survey score as sufficient** without
71//! objective data loses the balance the framework calls for.
72//!
73//! ## Sources
74//!
75//! - Chapter 3.1, The SPACE framework.
76//! - Forsgren, Storey, Maddila, Zimmermann, Houck, and Butler, "The SPACE of
77//! Developer Productivity," *ACM Queue* (2021).
78//!
79//! Topic doc: software-engineering-metrics/locales/en-001/chapters/03-01-the-space-framework.md
80
81/// One of the five SPACE dimensions.
82///
83/// Canonical order follows the acronym: Satisfaction, Performance, Activity,
84/// Communication, Efficiency.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum SpaceDimension {
87 /// Satisfaction and well-being.
88 Satisfaction,
89 /// Performance (outcomes, not output).
90 Performance,
91 /// Activity (commits, pull requests, and similar counts) — the
92 /// dimension most prone to misuse as a standalone proxy.
93 Activity,
94 /// Communication and collaboration.
95 Communication,
96 /// Efficiency and flow (absence of friction, sustained focus).
97 Efficiency,
98}
99
100/// The five canonical SPACE dimensions, in acronym order.
101const ALL_DIMENSIONS: [SpaceDimension; 5] = [
102 SpaceDimension::Satisfaction,
103 SpaceDimension::Performance,
104 SpaceDimension::Activity,
105 SpaceDimension::Communication,
106 SpaceDimension::Efficiency,
107];
108
109/// Whether a measured set of dimensions covers all five SPACE dimensions.
110///
111/// The book's recommended minimum is at least three of five dimensions with
112/// mixed subjective and objective sources; this function checks full
113/// coverage (all five), which `missing_dimensions` can help diagnose
114/// against that minimum.
115///
116/// # Arguments
117///
118/// * `measured` — the SPACE dimensions currently measured by a team.
119///
120/// # Returns
121///
122/// `true` iff every one of the five canonical dimensions appears in
123/// `measured` (duplicates are ignored).
124///
125/// # Examples
126///
127/// ```rust
128/// use software_engineering::space_framework::{SpaceDimension, covers_all_dimensions};
129///
130/// let activity_only = [SpaceDimension::Activity];
131/// assert!(!covers_all_dimensions(&activity_only));
132/// ```
133#[must_use]
134pub fn covers_all_dimensions(measured: &[SpaceDimension]) -> bool {
135 ALL_DIMENSIONS.iter().all(|d| measured.contains(d))
136}
137
138/// The SPACE dimensions not present in a measured set, in canonical order.
139///
140/// # Arguments
141///
142/// * `measured` — the SPACE dimensions currently measured by a team.
143///
144/// # Returns
145///
146/// A vector of the missing dimensions, in the order Satisfaction,
147/// Performance, Activity, Communication, Efficiency. Empty when `measured`
148/// already covers all five.
149///
150/// # Examples
151///
152/// ```rust
153/// use software_engineering::space_framework::{SpaceDimension, missing_dimensions};
154///
155/// // A single-dimension metric set is the book's classic anti-pattern.
156/// let activity_only = [SpaceDimension::Activity];
157/// let missing = missing_dimensions(&activity_only);
158/// assert_eq!(missing.len(), 4);
159/// assert!(missing.contains(&SpaceDimension::Satisfaction));
160/// ```
161#[must_use]
162pub fn missing_dimensions(measured: &[SpaceDimension]) -> Vec<SpaceDimension> {
163 ALL_DIMENSIONS
164 .iter()
165 .copied()
166 .filter(|d| !measured.contains(d))
167 .collect()
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 // "SPACE proposes measuring across five dimensions instead: Satisfaction
175 // and well-being, Performance, Activity, Communication and
176 // collaboration, and Efficiency and flow."
177 #[test]
178 fn all_five_dimensions_are_covered_when_all_are_measured() {
179 let all = [
180 SpaceDimension::Satisfaction,
181 SpaceDimension::Performance,
182 SpaceDimension::Activity,
183 SpaceDimension::Communication,
184 SpaceDimension::Efficiency,
185 ];
186 assert!(covers_all_dimensions(&all));
187 assert!(missing_dimensions(&all).is_empty());
188 }
189
190 // "A metric set drawn entirely from one dimension or one data type is
191 // not really using SPACE." — activity-only is the book's named
192 // anti-pattern.
193 #[test]
194 fn activity_only_metric_set_is_not_full_coverage() {
195 let activity_only = [SpaceDimension::Activity];
196 assert!(!covers_all_dimensions(&activity_only));
197 let missing = missing_dimensions(&activity_only);
198 assert_eq!(missing.len(), 4);
199 assert!(!missing.contains(&SpaceDimension::Activity));
200 assert!(missing.contains(&SpaceDimension::Satisfaction));
201 assert!(missing.contains(&SpaceDimension::Performance));
202 assert!(missing.contains(&SpaceDimension::Communication));
203 assert!(missing.contains(&SpaceDimension::Efficiency));
204 }
205
206 // "At least one metric from at least three dimensions ... is the
207 // minimum for a balanced picture."
208 #[test]
209 fn three_dimensions_still_leaves_two_missing() {
210 let three = [
211 SpaceDimension::Satisfaction,
212 SpaceDimension::Performance,
213 SpaceDimension::Efficiency,
214 ];
215 assert!(!covers_all_dimensions(&three));
216 assert_eq!(missing_dimensions(&three).len(), 2);
217 }
218
219 // Missing dimensions are reported in canonical acronym order.
220 #[test]
221 fn missing_dimensions_preserve_canonical_order() {
222 let empty: [SpaceDimension; 0] = [];
223 let missing = missing_dimensions(&empty);
224 assert_eq!(
225 missing,
226 vec![
227 SpaceDimension::Satisfaction,
228 SpaceDimension::Performance,
229 SpaceDimension::Activity,
230 SpaceDimension::Communication,
231 SpaceDimension::Efficiency,
232 ]
233 );
234 }
235}