devscout_rs/truth/
capability.rs1use crate::query::json::J;
10
11use super::profile::REGISTERED_PROFILES;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum CapabilityAxis {
16 ContextAcquisition,
18 SemanticConformance,
21 FrameworkModeling,
23 RepositoryEvidence,
25 RuntimeEvidence,
28}
29
30impl CapabilityAxis {
31 pub const ALL: [CapabilityAxis; 5] = [
33 CapabilityAxis::ContextAcquisition,
34 CapabilityAxis::SemanticConformance,
35 CapabilityAxis::FrameworkModeling,
36 CapabilityAxis::RepositoryEvidence,
37 CapabilityAxis::RuntimeEvidence,
38 ];
39
40 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ExecutedObligation {
57 pub executed: u32,
59}
60
61impl ExecutedObligation {
62 pub fn record(executed: u32) -> Option<ExecutedObligation> {
65 if executed == 0 {
66 None
67 } else {
68 Some(ExecutedObligation { executed })
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum CapabilityState {
78 Planned,
80 Unavailable,
82 SmokeTested,
84 Passing(ExecutedObligation),
87 Failing,
89}
90
91impl CapabilityState {
92 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#[derive(Debug, Clone)]
110pub struct MatrixEntry {
111 pub profile_id: &'static str,
113 pub axis: CapabilityAxis,
115 pub state: CapabilityState,
117 pub denominator: u32,
119 pub excluded: u32,
121}
122
123#[derive(Debug, Clone)]
125pub struct CapabilityMatrix {
126 pub entries: Vec<MatrixEntry>,
128}
129
130fn 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
152pub 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
174pub 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}