Skip to main content

devscout_rs/truth/
capability.rs

1//! The machine-readable capability matrix: one state per (profile, axis)
2//! pair, across five capability axes tracked independently.
3//!
4//! `Passing` is constructible only from an [`ExecutedObligation`] witness
5//! -- and that witness is constructible only when at least one obligation
6//! actually ran -- so a profile with an unexecuted obligation cannot hold
7//! `Passing` by construction, not merely by convention.
8
9use crate::query::json::J;
10
11use super::profile::REGISTERED_PROFILES;
12
13/// The five capability axes tracked independently per profile.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum CapabilityAxis {
16    /// Whether a usable build context can be acquired at all.
17    ContextAcquisition,
18    /// Whether the analyzer's binding decisions conform to the language's
19    /// own semantics.
20    SemanticConformance,
21    /// Whether an application framework's own conventions are modeled.
22    FrameworkModeling,
23    /// Whether repository-derived evidence (files, projects) is complete.
24    RepositoryEvidence,
25    /// Whether a run against a live host actually executed and produced
26    /// evidence.
27    RuntimeEvidence,
28}
29
30impl CapabilityAxis {
31    /// Every registered axis, in declaration order.
32    pub const ALL: [CapabilityAxis; 5] = [
33        CapabilityAxis::ContextAcquisition,
34        CapabilityAxis::SemanticConformance,
35        CapabilityAxis::FrameworkModeling,
36        CapabilityAxis::RepositoryEvidence,
37        CapabilityAxis::RuntimeEvidence,
38    ];
39
40    /// The wire label for this axis.
41    pub fn label(self) -> &'static str {
42        match self {
43            CapabilityAxis::ContextAcquisition => "context-acquisition",
44            CapabilityAxis::SemanticConformance => "semantic-conformance",
45            CapabilityAxis::FrameworkModeling => "framework-modeling",
46            CapabilityAxis::RepositoryEvidence => "repository-evidence",
47            CapabilityAxis::RuntimeEvidence => "runtime-evidence",
48        }
49    }
50}
51
52/// Proof that at least one obligation actually executed. The only public
53/// constructor refuses a zero count, so a caller can never manufacture a
54/// witness for work that did not run.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ExecutedObligation {
57    /// How many obligations actually ran.
58    pub executed: u32,
59}
60
61impl ExecutedObligation {
62    /// Records a witness for `executed` completed obligations, refusing a
63    /// zero count.
64    pub fn record(executed: u32) -> Option<ExecutedObligation> {
65        if executed == 0 {
66            None
67        } else {
68            Some(ExecutedObligation { executed })
69        }
70    }
71}
72
73/// The state one profile holds on one axis. `Passing` carries the witness
74/// that earned it; there is no variant that claims a pass with no
75/// obligation behind it.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum CapabilityState {
78    /// Registered as intended future coverage; nothing has run.
79    Planned,
80    /// Registered but cannot be executed offline from pinned inputs.
81    Unavailable,
82    /// Probed once, outside the obligation's own scored run.
83    SmokeTested,
84    /// The obligation executed and passed; carries the witness that
85    /// proves it.
86    Passing(ExecutedObligation),
87    /// The obligation executed and failed.
88    Failing,
89}
90
91impl CapabilityState {
92    /// The wire label for this state.
93    pub fn label(self) -> &'static str {
94        match self {
95            CapabilityState::Planned => "planned",
96            CapabilityState::Unavailable => "unavailable",
97            CapabilityState::SmokeTested => "smoke-tested",
98            CapabilityState::Passing(_) => "passing",
99            CapabilityState::Failing => "failing",
100        }
101    }
102}
103
104/// One (profile, axis) cell: the state it holds, and the denominator/
105/// exclusion counts a run against it reported.
106///
107/// A profile never registered against an axis simply has no entry --
108/// absence, not a zero disguised as a measurement.
109#[derive(Debug, Clone)]
110pub struct MatrixEntry {
111    /// The profile this cell belongs to.
112    pub profile_id: &'static str,
113    /// The capability axis this cell belongs to.
114    pub axis: CapabilityAxis,
115    /// The state this cell holds.
116    pub state: CapabilityState,
117    /// How many obligations were requested for this cell.
118    pub denominator: u32,
119    /// How many requested obligations were excluded from scoring.
120    pub excluded: u32,
121}
122
123/// The full profile-by-axis matrix.
124#[derive(Debug, Clone)]
125pub struct CapabilityMatrix {
126    /// One entry per registered (profile, axis) pair.
127    pub entries: Vec<MatrixEntry>,
128}
129
130/// The state one profile's registered ceiling caps a given axis at.
131///
132/// The registry's demonstrated evidence is a narrow, static smoke fixture
133/// under one pinned host: that backs context acquisition, semantic
134/// conformance, and repository evidence, but a successful static fixture
135/// is not a runtime or full project-system support claim -- so
136/// `runtime-evidence` and `framework-modeling` never rise above `planned`
137/// (or stay `unavailable`) purely because the profile's own ceiling is
138/// stronger. Each axis's state is therefore actually tracked per axis, not
139/// smeared uniformly from one ceiling value.
140fn axis_ceiling(ceiling: CapabilityState, axis: CapabilityAxis) -> CapabilityState {
141    match axis {
142        CapabilityAxis::RuntimeEvidence | CapabilityAxis::FrameworkModeling => match ceiling {
143            CapabilityState::Unavailable => CapabilityState::Unavailable,
144            _ => CapabilityState::Planned,
145        },
146        CapabilityAxis::ContextAcquisition
147        | CapabilityAxis::SemanticConformance
148        | CapabilityAxis::RepositoryEvidence => ceiling,
149    }
150}
151
152/// Builds the registry-only matrix: every registered profile, every axis,
153/// each capped at that profile's registered ceiling AND that axis's own
154/// narrower cap (see [`axis_ceiling`]).
155///
156/// No obligation is executed by this function -- it reads static registry
157/// data only -- so no entry it produces ever holds `Passing`.
158pub fn build_matrix() -> CapabilityMatrix {
159    let mut entries = Vec::new();
160    for profile in REGISTERED_PROFILES {
161        for axis in CapabilityAxis::ALL {
162            entries.push(MatrixEntry {
163                profile_id: profile.id,
164                axis,
165                state: axis_ceiling(profile.ceiling, axis),
166                denominator: 0,
167                excluded: 0,
168            });
169        }
170    }
171    CapabilityMatrix { entries }
172}
173
174/// Deterministic JSON for the matrix: entries sorted by profile id then
175/// axis declaration order, so two builds from the same registry produce
176/// byte-identical output.
177pub fn matrix_to_json(matrix: &CapabilityMatrix) -> String {
178    let mut sorted = matrix.entries.clone();
179    sorted.sort_by(|a, b| {
180        a.profile_id
181            .cmp(b.profile_id)
182            .then_with(|| axis_order(a.axis).cmp(&axis_order(b.axis)))
183    });
184    J::Obj(vec![(
185        "entries",
186        J::Arr(
187            sorted
188                .iter()
189                .map(|e| {
190                    J::Obj(vec![
191                        ("profile", J::Str(e.profile_id.to_string())),
192                        ("axis", J::Str(e.axis.label().to_string())),
193                        ("state", J::Str(e.state.label().to_string())),
194                        ("denominator", J::UInt(u64::from(e.denominator))),
195                        ("excluded", J::UInt(u64::from(e.excluded))),
196                    ])
197                })
198                .collect(),
199        ),
200    )])
201    .to_json_string()
202}
203
204fn axis_order(axis: CapabilityAxis) -> usize {
205    CapabilityAxis::ALL
206        .iter()
207        .position(|a| *a == axis)
208        .unwrap_or(usize::MAX)
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn zero_executions_cannot_produce_a_witness() {
217        assert_eq!(ExecutedObligation::record(0), None);
218        assert!(ExecutedObligation::record(1).is_some());
219    }
220
221    #[test]
222    fn the_registry_only_matrix_holds_no_unexecuted_passing_entry() {
223        let matrix = build_matrix();
224        assert!(!matrix.entries.is_empty());
225        for entry in &matrix.entries {
226            assert!(
227                !matches!(entry.state, CapabilityState::Passing(_)),
228                "{} / {} claims passing with nothing executed",
229                entry.profile_id,
230                entry.axis.label()
231            );
232        }
233    }
234
235    #[test]
236    fn matrix_json_is_byte_identical_across_two_builds() {
237        let a = matrix_to_json(&build_matrix());
238        let b = matrix_to_json(&build_matrix());
239        assert_eq!(a, b);
240    }
241
242    #[test]
243    fn runtime_and_framework_axes_never_claim_more_than_planned_from_a_static_ceiling() {
244        let matrix = build_matrix();
245        for entry in matrix
246            .entries
247            .iter()
248            .filter(|e| e.profile_id == "csharp-net8.0-sdk")
249        {
250            match entry.axis {
251                CapabilityAxis::RuntimeEvidence | CapabilityAxis::FrameworkModeling => {
252                    assert_eq!(
253                        entry.state,
254                        CapabilityState::Planned,
255                        "{:?} must not inherit the profile's smoke-tested ceiling",
256                        entry.axis
257                    );
258                }
259                CapabilityAxis::ContextAcquisition
260                | CapabilityAxis::SemanticConformance
261                | CapabilityAxis::RepositoryEvidence => {
262                    assert_eq!(entry.state, CapabilityState::SmokeTested);
263                }
264            }
265        }
266    }
267
268    #[test]
269    fn an_unavailable_profile_stays_unavailable_on_every_axis() {
270        let matrix = build_matrix();
271        for entry in matrix
272            .entries
273            .iter()
274            .filter(|e| e.profile_id == "csharp-netcoreapp2.x-sdk")
275        {
276            assert_eq!(entry.state, CapabilityState::Unavailable);
277        }
278    }
279
280    #[test]
281    fn matrix_json_is_sorted_by_profile_then_axis() {
282        let json = matrix_to_json(&build_matrix());
283        let first = json.find("csharp-net40-framework").unwrap();
284        let later = json.find("csharp-netstandard2.1-library").unwrap();
285        assert!(first < later, "profiles must sort lexically");
286    }
287}