Skip to main content

powerio_pkg/
diagnostics.rs

1//! Structured diagnostics.
2//!
3//! A free-form `Vec<String>` warning is useful for a human but opaque to CI, an
4//! agent, or a downstream solver. Every finding a frontend, lowering pass, or
5//! backend records carries a stable [`DiagnosticCode`], a [`DiagnosticSeverity`],
6//! the [`DiagnosticStage`] it came from, a human message, and (where known) the
7//! element path and [`SourceRef`] it refers to. Human-readable warnings should
8//! be rendered from these, not the other way around.
9
10use serde::{Deserialize, Serialize};
11
12use crate::provenance::SourceRef;
13
14/// A stable, dotted diagnostic code, e.g. `EMIT.PSSE.DROP_ANGLE_LIMITS`.
15///
16/// The leading segment is the namespace and names the stage family:
17/// `PARSE`, `READ`, `IR`, `VALIDATE`, `FIDELITY`, `LOWER`, `EMIT`, `BINDING`,
18/// `PARTNER`, `PERF`. The conventional shape is `NAMESPACE.SOURCE_OR_TARGET.SPECIFIC`.
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct DiagnosticCode(pub String);
22
23impl DiagnosticCode {
24    pub fn new(code: impl Into<String>) -> Self {
25        Self(code.into())
26    }
27
28    /// The leading dotted segment (the namespace), e.g. `EMIT` for
29    /// `EMIT.PSSE.DROP_ANGLE_LIMITS`.
30    pub fn namespace(&self) -> &str {
31        self.0.split('.').next().unwrap_or("")
32    }
33
34    pub fn as_str(&self) -> &str {
35        &self.0
36    }
37}
38
39impl std::fmt::Display for DiagnosticCode {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str(&self.0)
42    }
43}
44
45impl From<&str> for DiagnosticCode {
46    fn from(s: &str) -> Self {
47        Self(s.to_owned())
48    }
49}
50
51impl From<String> for DiagnosticCode {
52    fn from(s: String) -> Self {
53        Self(s)
54    }
55}
56
57/// Severity, ordered worst-last so [`Ord`] gives the dominant severity of a set.
58#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum DiagnosticSeverity {
61    /// Useful in development; normally hidden.
62    Debug,
63    /// A provenance or normalization event worth recording.
64    Info,
65    /// Usable, but semantics were defaulted, approximated, lost, or the target
66    /// is incomplete.
67    Warning,
68    /// The package exists but the model is not valid for the intended use
69    /// without repair.
70    Error,
71    /// The package could not be produced.
72    Fatal,
73}
74
75/// The compiler stage that emitted a diagnostic.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78#[non_exhaustive]
79pub enum DiagnosticStage {
80    Parse,
81    Read,
82    Canonicalize,
83    Validate,
84    Lower,
85    Emit,
86    Bind,
87    Partner,
88}
89
90/// One structured finding.
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92pub struct StructuredDiagnostic {
93    pub code: DiagnosticCode,
94    pub severity: DiagnosticSeverity,
95    pub stage: DiagnosticStage,
96    pub message: String,
97    /// JSON pointer (or best-effort locator) of the element the finding is about.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub element_path: Option<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub source_ref: Option<SourceRef>,
102    /// Code-specific structured payload, e.g. `{"dropped_fields": ["angmin"]}`.
103    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
104    pub details: serde_json::Map<String, serde_json::Value>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub suggested_action: Option<String>,
107    /// Workflows for which this finding is safe to ignore, e.g.
108    /// `["power_flow", "opf"]`. Empty means "no such assurance".
109    #[serde(default, skip_serializing_if = "Vec::is_empty")]
110    pub safe_to_ignore: Vec<String>,
111}
112
113impl StructuredDiagnostic {
114    /// A minimal finding; fill the optional locators with the builder methods.
115    pub fn new(
116        code: impl Into<DiagnosticCode>,
117        severity: DiagnosticSeverity,
118        stage: DiagnosticStage,
119        message: impl Into<String>,
120    ) -> Self {
121        Self {
122            code: code.into(),
123            severity,
124            stage,
125            message: message.into(),
126            element_path: None,
127            source_ref: None,
128            details: serde_json::Map::new(),
129            suggested_action: None,
130            safe_to_ignore: Vec::new(),
131        }
132    }
133
134    #[must_use]
135    pub fn with_element_path(mut self, path: impl Into<String>) -> Self {
136        self.element_path = Some(path.into());
137        self
138    }
139
140    #[must_use]
141    pub fn with_source_ref(mut self, source_ref: SourceRef) -> Self {
142        self.source_ref = Some(source_ref);
143        self
144    }
145
146    #[must_use]
147    pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
148        self.suggested_action = Some(action.into());
149        self
150    }
151}