Skip to main content

wasm4pm_compat/
dfcm.rs

1//! DfCM — Design-for-Combinatorial-Maximality Matrix (Vision 2030 §7).
2//!
3//! Provides the [`DfCmMatrix`] type that tracks test coverage across the full
4//! Cartesian product of process-evidence dimensions, together with a
5//! [`Standing`] enum that records the verdict for each cell.
6
7use serde::{Deserialize, Serialize};
8
9// ── Standing ─────────────────────────────────────────────────────────────────
10
11/// Verdict for a single DfCM cell.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub enum Standing {
14    Unknown,
15    Refused,
16    Admitted,
17    Planned,
18    Executed,
19    Impossible,
20}
21
22impl Default for Standing {
23    fn default() -> Self {
24        Standing::Unknown
25    }
26}
27
28// ── DfCmAxis ─────────────────────────────────────────────────────────────────
29
30/// One dimension of a DfCM matrix.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DfCmAxis {
33    pub name: String,
34    pub description: Option<String>,
35    pub variants: Vec<String>,
36}
37
38// ── DfCmCell ─────────────────────────────────────────────────────────────────
39
40/// A single cell in the DfCM matrix identified by its coordinate tuple.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DfCmCell {
43    pub coords: Vec<String>,
44    pub expected_standing: Standing,
45    pub actual_standing: Standing,
46    pub fixture: Option<String>,
47    pub is_impossible: bool,
48    pub refusal_reason: Option<String>,
49    pub manufacture_witness: Option<String>,
50}
51
52impl DfCmCell {
53    /// Create a new cell at the given coordinates with all fields defaulted.
54    pub fn new(coords: Vec<String>) -> Self {
55        Self {
56            coords,
57            expected_standing: Standing::Unknown,
58            actual_standing: Standing::Unknown,
59            fixture: None,
60            is_impossible: false,
61            refusal_reason: None,
62            manufacture_witness: None,
63        }
64    }
65
66    /// Create a cell that is pre-marked as impossible.
67    pub fn impossible(coords: Vec<String>) -> Self {
68        Self {
69            coords,
70            expected_standing: Standing::Impossible,
71            actual_standing: Standing::Refused,
72            fixture: None,
73            is_impossible: true,
74            refusal_reason: None,
75            manufacture_witness: None,
76        }
77    }
78
79    /// Returns `true` when the cell's verdict is satisfactory.
80    pub fn passes(&self) -> bool {
81        self.expected_standing == self.actual_standing
82            || (self.is_impossible && self.actual_standing == Standing::Refused)
83    }
84}
85
86// ── DfCmMatrix ───────────────────────────────────────────────────────────────
87
88/// The full DfCM matrix: a named set of axes and the cells that span them.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DfCmMatrix {
91    pub name: String,
92    pub axes: Vec<DfCmAxis>,
93    pub cells: Vec<DfCmCell>,
94}
95
96impl DfCmMatrix {
97    /// Create an empty matrix with the given axes.
98    pub fn new(name: impl Into<String>, axes: Vec<DfCmAxis>) -> Self {
99        Self {
100            name: name.into(),
101            axes,
102            cells: Vec::new(),
103        }
104    }
105
106    /// Populate `cells` with one [`DfCmCell`] per point in the Cartesian product
107    /// of all axis variants. Existing cells are replaced.
108    pub fn expand_cartesian(&mut self) {
109        let variant_slices: Vec<&[String]> =
110            self.axes.iter().map(|a| a.variants.as_slice()).collect();
111        let combos = cartesian_product(&variant_slices);
112        self.cells = combos.into_iter().map(DfCmCell::new).collect();
113    }
114
115    /// Total number of cells.
116    pub fn total(&self) -> usize {
117        self.cells.len()
118    }
119
120    /// Number of *evaluated* cells where [`DfCmCell::passes`] is `true`.
121    ///
122    /// A cell whose `actual_standing` is still [`Standing::Unknown`] has not
123    /// been evaluated yet and must not count as passing merely because its
124    /// (also-default) `expected_standing` happens to equal it — otherwise
125    /// unevaluated cells would trivially inflate `pass_rate` above the true
126    /// coverage-weighted rate.
127    pub fn passing(&self) -> usize {
128        self.cells
129            .iter()
130            .filter(|c| c.actual_standing != Standing::Unknown && c.passes())
131            .count()
132    }
133
134    /// Number of cells whose `actual_standing` is not [`Standing::Unknown`].
135    pub fn evaluated(&self) -> usize {
136        self.cells
137            .iter()
138            .filter(|c| c.actual_standing != Standing::Unknown)
139            .count()
140    }
141
142    /// Fraction of cells that have been evaluated (0.0–1.0).
143    pub fn coverage(&self) -> f64 {
144        let t = self.total();
145        if t == 0 {
146            return 0.0;
147        }
148        self.evaluated() as f64 / t as f64
149    }
150
151    /// Fraction of evaluated cells that pass (0.0–1.0). Returns 0.0 when none
152    /// have been evaluated.
153    pub fn pass_rate(&self) -> f64 {
154        let e = self.evaluated();
155        if e == 0 {
156            return 0.0;
157        }
158        self.passing() as f64 / e as f64
159    }
160
161    /// Find a cell by its coordinate tuple.
162    pub fn find_cell(&self, coords: &[&str]) -> Option<&DfCmCell> {
163        self.cells.iter().find(|c| {
164            c.coords.len() == coords.len()
165                && c.coords.iter().zip(coords.iter()).all(|(a, b)| a == b)
166        })
167    }
168
169    /// Find a cell mutably by its coordinate tuple.
170    pub fn find_cell_mut(&mut self, coords: &[&str]) -> Option<&mut DfCmCell> {
171        self.cells.iter_mut().find(|c| {
172            c.coords.len() == coords.len()
173                && c.coords.iter().zip(coords.iter()).all(|(a, b)| a == b)
174        })
175    }
176
177    /// Validate the matrix and return a list of human-readable error strings.
178    ///
179    /// Checks:
180    /// - Every cell has the same number of coordinates as there are axes.
181    /// - No two cells share the same coordinate tuple.
182    /// - Every coordinate value at position `i` is a declared variant of axis `i`.
183    pub fn validate(&self) -> Vec<String> {
184        let mut errors = Vec::new();
185        let axis_count = self.axes.len();
186
187        for (idx, cell) in self.cells.iter().enumerate() {
188            if cell.coords.len() != axis_count {
189                errors.push(format!(
190                    "cell[{}]: expected {} coordinates, got {}",
191                    idx,
192                    axis_count,
193                    cell.coords.len()
194                ));
195                continue;
196            }
197            for (dim, (coord, axis)) in cell.coords.iter().zip(self.axes.iter()).enumerate() {
198                if !axis.variants.contains(coord) {
199                    errors.push(format!(
200                        "cell[{}] dim {}: coordinate {:?} not in axis {:?} variants",
201                        idx, dim, coord, axis.name
202                    ));
203                }
204            }
205        }
206
207        // Duplicate check.
208        for i in 0..self.cells.len() {
209            for j in (i + 1)..self.cells.len() {
210                if self.cells[i].coords == self.cells[j].coords {
211                    errors.push(format!(
212                        "cells[{}] and cells[{}] share duplicate coords {:?}",
213                        i, j, self.cells[i].coords
214                    ));
215                }
216            }
217        }
218
219        errors
220    }
221}
222
223// ── DfCmReport / DfCmFailure ─────────────────────────────────────────────────
224
225/// A failure record extracted from a DfCM matrix.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct DfCmFailure {
228    pub coords: Vec<String>,
229    pub expected: Standing,
230    pub actual: Standing,
231    pub reason: Option<String>,
232}
233
234/// Summary report generated from a [`DfCmMatrix`].
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct DfCmReport {
237    pub matrix_name: String,
238    pub total: usize,
239    pub evaluated: usize,
240    pub passing: usize,
241    pub coverage: f64,
242    pub pass_rate: f64,
243    pub failures: Vec<DfCmFailure>,
244}
245
246impl DfCmReport {
247    /// Build a report from a matrix snapshot.
248    pub fn from_matrix(matrix: &DfCmMatrix) -> Self {
249        let failures = matrix
250            .cells
251            .iter()
252            .filter(|c| !c.passes() && c.actual_standing != Standing::Unknown)
253            .map(|c| DfCmFailure {
254                coords: c.coords.clone(),
255                expected: c.expected_standing,
256                actual: c.actual_standing,
257                reason: c.refusal_reason.clone(),
258            })
259            .collect();
260
261        Self {
262            matrix_name: matrix.name.clone(),
263            total: matrix.total(),
264            evaluated: matrix.evaluated(),
265            passing: matrix.passing(),
266            coverage: matrix.coverage(),
267            pass_rate: matrix.pass_rate(),
268            failures,
269        }
270    }
271}
272
273// ── cartesian_product ────────────────────────────────────────────────────────
274
275fn cartesian_product(axes: &[&[String]]) -> Vec<Vec<String>> {
276    if axes.is_empty() {
277        return vec![vec![]];
278    }
279    let mut result = vec![vec![]];
280    for axis in axes {
281        let mut next = Vec::with_capacity(result.len() * axis.len());
282        for existing in &result {
283            for variant in *axis {
284                let mut combo = existing.clone();
285                combo.push(variant.clone());
286                next.push(combo);
287            }
288        }
289        result = next;
290    }
291    result
292}
293
294// ── tests ────────────────────────────────────────────────────────────────────
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    fn two_axis_matrix() -> DfCmMatrix {
301        let axes = vec![
302            DfCmAxis {
303                name: "format".into(),
304                description: None,
305                variants: vec!["xes".into(), "ocel".into()],
306            },
307            DfCmAxis {
308                name: "size".into(),
309                description: None,
310                variants: vec!["small".into(), "large".into()],
311            },
312        ];
313        let mut m = DfCmMatrix::new("test", axes);
314        m.expand_cartesian();
315        m
316    }
317
318    #[test]
319    fn cartesian_expands_correctly() {
320        let m = two_axis_matrix();
321        assert_eq!(m.total(), 4);
322        assert!(m.find_cell(&["xes", "small"]).is_some());
323        assert!(m.find_cell(&["ocel", "large"]).is_some());
324    }
325
326    #[test]
327    fn validate_clean_matrix() {
328        let m = two_axis_matrix();
329        assert!(m.validate().is_empty());
330    }
331
332    #[test]
333    fn coverage_and_pass_rate() {
334        let mut m = two_axis_matrix();
335        {
336            let c = m.find_cell_mut(&["xes", "small"]).unwrap();
337            c.expected_standing = Standing::Admitted;
338            c.actual_standing = Standing::Admitted;
339        }
340        {
341            let c = m.find_cell_mut(&["xes", "large"]).unwrap();
342            c.expected_standing = Standing::Admitted;
343            c.actual_standing = Standing::Refused;
344        }
345        assert_eq!(m.evaluated(), 2);
346        assert_eq!(m.passing(), 1);
347        assert!((m.coverage() - 0.5).abs() < f64::EPSILON);
348        assert!((m.pass_rate() - 0.5).abs() < f64::EPSILON);
349    }
350
351    #[test]
352    fn impossible_cell_passes() {
353        let c = DfCmCell::impossible(vec!["xes".into(), "small".into()]);
354        assert!(c.passes());
355    }
356
357    #[test]
358    fn report_captures_failures() {
359        let mut m = two_axis_matrix();
360        {
361            let c = m.find_cell_mut(&["xes", "large"]).unwrap();
362            c.expected_standing = Standing::Admitted;
363            c.actual_standing = Standing::Refused;
364        }
365        let r = DfCmReport::from_matrix(&m);
366        assert_eq!(r.failures.len(), 1);
367        assert_eq!(r.failures[0].coords, vec!["xes", "large"]);
368    }
369}