Skip to main content

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}