Skip to main content

mcp_trace_validator/report/
human.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. (https://github.com/tomtom215)
3
4//! The terminal-oriented rendering of a [`Report`].
5//!
6//! Split from the data model so the report's shape and the way it is printed
7//! can be read separately: everything here is presentation, and nothing here
8//! decides an outcome.
9
10use core::fmt::Write as _;
11
12use super::{Outcome, Report};
13
14impl Report {
15    /// Renders the human-readable form.
16    #[must_use]
17    pub fn render_human(&self) -> String {
18        let mut out = String::new();
19        let _ = writeln!(out, "MCP trace validation — revision {}", self.revision);
20        self.write_revision_mismatch(&mut out);
21        for row in &self.requirements {
22            let marker = match row.outcome {
23                Outcome::Pass => "PASS",
24                Outcome::Fail => "FAIL",
25                Outcome::Warn => "WARN",
26                Outcome::Excluded => "EXCL",
27                Outcome::Unsupported => "UNSUP",
28                Outcome::NotApplicable => "N/A",
29                Outcome::NotObserved => "NOBS",
30            };
31            let _ = writeln!(out, "  {marker:<5} {} ({})", row.id, row.level);
32            for finding in &row.findings {
33                match finding.seq {
34                    Some(seq) => {
35                        let _ = writeln!(out, "        seq {seq}: {}", finding.detail);
36                    }
37                    None => {
38                        let _ = writeln!(out, "        {}", finding.detail);
39                    }
40                }
41            }
42            if let Some(exclusion) = &row.exclusion {
43                let _ = writeln!(out, "        excluded: {exclusion}");
44            }
45            for check in &row.missing_checks {
46                let _ = writeln!(out, "        unsupported check: {check}");
47            }
48            if let Some(capability) = &row.capability {
49                let _ = writeln!(
50                    out,
51                    "        not applicable: capability {capability} was not declared in this session"
52                );
53            }
54            if row.outcome == Outcome::NotObserved {
55                let _ = writeln!(
56                    out,
57                    "        not observed: the session carried none of the traffic this clause binds to"
58                );
59            }
60        }
61        // Every outcome is named, so the counts sum to the registry's size — a
62        // reader can check the arithmetic, and `Totals`' own exhaustive
63        // destructuring is what keeps that true as outcomes are added.
64        let _ = writeln!(out, "totals: {}", self.totals);
65        let _ = writeln!(out, "verdict: {}", self.verdict());
66        self.write_revision_mismatch(&mut out);
67        out
68    }
69
70    /// Writes the revision-disagreement note, if there is one.
71    ///
72    /// Rendered twice — under the header and under the verdict — because both
73    /// are where a reader looks, and a note that scrolls past a hundred rows of
74    /// findings is a note nobody reads. It is short enough that repeating it
75    /// costs less than missing it.
76    fn write_revision_mismatch(&self, out: &mut String) {
77        let Some(declared) = &self.revision_mismatch else {
78            return;
79        };
80        let subject = if declared.len() == 1 {
81            "revision"
82        } else {
83            "revisions"
84        };
85        let suggestion = declared.last().map_or("<revision>", String::as_str);
86        let _ = writeln!(
87            out,
88            "  NOTE  this session declares protocol {subject} {}, not {}.",
89            declared.join(", "),
90            self.revision
91        );
92        let _ = writeln!(
93            out,
94            "        Every outcome here judges it against rules it was not playing by;"
95        );
96        let _ = writeln!(
97            out,
98            "        re-run with `--revision {suggestion}` to judge it against its own."
99        );
100    }
101}