Skip to main content

agent_plugins/
diag.rs

1//! Diagnostics: the spec's "SHOULD report" made concrete.
2//!
3//! Agent Plugins is deliberately forgiving — most failures narrow to the
4//! smallest boundary (§4.1, §11.3) and loading continues. What must not
5//! happen is *silence*. Every non-fatal decision the loader takes on the
6//! plugin's behalf is recorded as a [`Diagnostic`]: which [`Rule`] fired,
7//! where ([`Origin`]), and a human-readable why.
8
9use std::fmt;
10
11/// The closed set of non-fatal report conditions defined by the specification.
12///
13/// Each variant maps to the spec section that mandates the report, available
14/// via [`Rule::section`], and to a stable machine-readable [`Rule::code`].
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum Rule {
18    /// An unknown top-level manifest field was reported and ignored (§5.2).
19    UnknownManifestField,
20    /// A non-object `extensions` field was reported and ignored (§8.1).
21    ExtensionsIgnored,
22    /// A fixed component location exists but is unusable (§6.2, §4.1 rule 2).
23    ComponentInvalid,
24    /// A discovered skill was skipped (§7.1, §4.1 rule 3).
25    SkillSkipped,
26    /// An individual MCP server entry was skipped (§7.2.2 rules 3–4).
27    ServerSkipped,
28    /// MCP was disabled for the whole plugin (§7.2.2 rule 2).
29    McpDisabled,
30}
31
32impl Rule {
33    /// Stable kebab-case identifier, shared with the TypeScript implementation
34    /// and the cross-language conformance fixtures.
35    pub fn code(self) -> &'static str {
36        match self {
37            Self::UnknownManifestField => "unknown-manifest-field",
38            Self::ExtensionsIgnored => "extensions-ignored",
39            Self::ComponentInvalid => "component-invalid",
40            Self::SkillSkipped => "skill-skipped",
41            Self::ServerSkipped => "server-skipped",
42            Self::McpDisabled => "mcp-disabled",
43        }
44    }
45
46    /// The specification section that defines this report condition.
47    pub fn section(self) -> &'static str {
48        match self {
49            Self::UnknownManifestField => "§5.2",
50            Self::ExtensionsIgnored => "§8.1",
51            Self::ComponentInvalid => "§6.2",
52            Self::SkillSkipped => "§7.1",
53            Self::ServerSkipped => "§7.2.2",
54            Self::McpDisabled => "§7.2.2",
55        }
56    }
57}
58
59impl fmt::Display for Rule {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{} ({})", self.code(), self.section())
62    }
63}
64
65/// Where in the package a diagnostic points.
66#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum Origin {
69    /// The `plugin.json` manifest.
70    Manifest,
71    /// The `skills/` component location.
72    Skills,
73    /// One skill directory under `skills/`, by directory name.
74    Skill(String),
75    /// The `mcp.json` component location.
76    Mcp,
77    /// One MCP server entry, by its `mcpServers` member name.
78    Server(String),
79}
80
81impl fmt::Display for Origin {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Manifest => f.write_str("plugin.json"),
85            Self::Skills => f.write_str("skills/"),
86            Self::Skill(dir) => write!(f, "skills/{dir}/"),
87            Self::Mcp => f.write_str("mcp.json"),
88            Self::Server(name) => write!(f, "mcp.json#{name}"),
89        }
90    }
91}
92
93/// One reported, non-fatal loading decision.
94#[derive(Debug, Clone, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct Diagnostic {
97    /// Which report condition fired.
98    pub rule: Rule,
99    /// Where in the package it fired.
100    pub origin: Origin,
101    /// Human-readable explanation.
102    pub message: String,
103}
104
105impl Diagnostic {
106    pub(crate) fn new(rule: Rule, origin: Origin, message: impl Into<String>) -> Self {
107        Self { rule, origin, message: message.into() }
108    }
109}
110
111impl fmt::Display for Diagnostic {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{} at {}: {}", self.rule, self.origin, self.message)
114    }
115}