Skip to main content

contextgraph_conformance/
report.rs

1//! The typed conformance report (`SPEC.md` §11). Each check
2//! carries a pass/fail/skip status and an evidence string, so "not
3//! conformant" always says *why*. Serde-derivable so `contextgraph-inspect --json`
4//! and CI can consume it.
5
6use serde::{Deserialize, Serialize};
7
8/// The verdict for a single conformance check.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CheckStatus {
12    Pass,
13    Fail,
14    /// Not applicable to this provider/transport (e.g. a wire-level probe
15    /// against an in-process provider).
16    Skipped,
17}
18
19/// One check's outcome: which check, its verdict, and human-readable evidence.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct CheckResult {
22    pub name: String,
23    pub status: CheckStatus,
24    pub evidence: String,
25}
26
27impl CheckResult {
28    pub fn pass(name: impl Into<String>, evidence: impl Into<String>) -> Self {
29        Self {
30            name: name.into(),
31            status: CheckStatus::Pass,
32            evidence: evidence.into(),
33        }
34    }
35
36    pub fn fail(name: impl Into<String>, evidence: impl Into<String>) -> Self {
37        Self {
38            name: name.into(),
39            status: CheckStatus::Fail,
40            evidence: evidence.into(),
41        }
42    }
43
44    pub fn skip(name: impl Into<String>, evidence: impl Into<String>) -> Self {
45        Self {
46            name: name.into(),
47            status: CheckStatus::Skipped,
48            evidence: evidence.into(),
49        }
50    }
51
52    /// Build a pass or fail from a boolean — the common "check this predicate"
53    /// shape.
54    pub fn from_bool(name: impl Into<String>, passed: bool, evidence: impl Into<String>) -> Self {
55        if passed {
56            Self::pass(name, evidence)
57        } else {
58            Self::fail(name, evidence)
59        }
60    }
61}
62
63/// The result of a conformance run: every check, against a described target.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct ConformanceReport {
66    /// Human description of the provider under test.
67    pub target: String,
68    pub checks: Vec<CheckResult>,
69}
70
71impl ConformanceReport {
72    /// True when no check failed (skips don't fail a run). This is the
73    /// "Context Graph Protocol conformant for your declared capability set" verdict (SPEC.md §11).
74    pub fn passed(&self) -> bool {
75        !self
76            .checks
77            .iter()
78            .any(|check| check.status == CheckStatus::Fail)
79    }
80
81    /// The checks that failed, in order.
82    pub fn failures(&self) -> impl Iterator<Item = &CheckResult> {
83        self.checks
84            .iter()
85            .filter(|check| check.status == CheckStatus::Fail)
86    }
87
88    /// `(passed, failed, skipped)` tallies.
89    pub fn tally(&self) -> (usize, usize, usize) {
90        let mut passed = 0;
91        let mut failed = 0;
92        let mut skipped = 0;
93        for check in &self.checks {
94            match check.status {
95                CheckStatus::Pass => passed += 1,
96                CheckStatus::Fail => failed += 1,
97                CheckStatus::Skipped => skipped += 1,
98            }
99        }
100        (passed, failed, skipped)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn a_run_passes_only_when_nothing_failed() {
110        let report = ConformanceReport {
111            target: "in-process: t".into(),
112            checks: vec![
113                CheckResult::pass("handshake", "ok"),
114                CheckResult::skip("malformed-input-tolerance", "n/a"),
115            ],
116        };
117        assert!(report.passed());
118        assert_eq!(report.tally(), (1, 0, 1));
119
120        let broken = ConformanceReport {
121            target: "in-process: t".into(),
122            checks: vec![
123                CheckResult::pass("handshake", "ok"),
124                CheckResult::fail("budget-honesty", "over budget"),
125            ],
126        };
127        assert!(!broken.passed());
128        assert_eq!(broken.failures().count(), 1);
129        assert_eq!(broken.tally(), (1, 1, 0));
130    }
131
132    #[test]
133    fn report_is_serde_roundtrippable_for_json_output() {
134        let report = ConformanceReport {
135            target: "stdio: contextgraph-example-docs".into(),
136            checks: vec![CheckResult::from_bool(
137                "frame-validity",
138                true,
139                "3 frames ok",
140            )],
141        };
142        let json = serde_json::to_string(&report).unwrap();
143        let back: ConformanceReport = serde_json::from_str(&json).unwrap();
144        assert_eq!(back, report);
145    }
146}