#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostic {
pub path: String,
pub kind: DiagnosticKind,
pub risk: RiskLevel,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticKind {
Coerced {
from: String,
to: String,
},
Defaulted {
field: String,
value: String,
},
Dropped {
field: String,
},
Preserved {
field: String,
},
ErrorDefaulted {
field: String,
error: String,
},
Overridden {
from_type: String,
to_type: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RiskLevel {
Info,
Warning,
Risky,
}
impl std::fmt::Display for DiagnosticKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticKind::Coerced { from, to } => write!(f, "coerced {from} → {to}"),
DiagnosticKind::Defaulted { field, value } => {
write!(f, "defaulted field '{field}' ({value})")
}
DiagnosticKind::Dropped { field } => write!(f, "dropped unknown field '{field}'"),
DiagnosticKind::Preserved { field } => {
write!(f, "preserved unknown field '{field}' in overflow")
}
DiagnosticKind::ErrorDefaulted { field, error } => {
write!(f, "field '{field}' failed ({error}), used default")
}
DiagnosticKind::Overridden { from_type, to_type } => {
write!(f, "overridden {from_type} → {to_type}")
}
}
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] at '{}': {}", self.risk, self.path, self.kind)?;
if let Some(ref suggestion) = self.suggestion {
write!(f, " — {suggestion}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
StopSequence,
Unknown(String),
}
impl std::fmt::Display for StopReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StopReason::EndTurn => write!(f, "end_turn"),
StopReason::ToolUse => write!(f, "tool_use"),
StopReason::MaxTokens => write!(f, "max_tokens"),
StopReason::StopSequence => write!(f, "stop_sequence"),
StopReason::Unknown(s) => write!(f, "{s}"),
}
}
}
pub trait DiagnosticSink {
fn receive(&mut self, diagnostic: &Diagnostic);
fn receive_all(&mut self, diagnostics: &[Diagnostic]) {
for d in diagnostics {
self.receive(d);
}
}
}
#[derive(Debug, Default)]
pub struct CollectSink {
pub diagnostics: Vec<Diagnostic>,
}
impl DiagnosticSink for CollectSink {
fn receive(&mut self, diagnostic: &Diagnostic) {
self.diagnostics.push(diagnostic.clone());
}
}
#[derive(Debug, Default)]
pub struct StderrSink;
impl DiagnosticSink for StderrSink {
fn receive(&mut self, diagnostic: &Diagnostic) {
eprintln!("{diagnostic}");
}
}
pub struct FilteredSink<S: DiagnosticSink> {
inner: S,
min_risk: RiskLevel,
}
impl<S: DiagnosticSink> FilteredSink<S> {
pub fn new(inner: S, min_risk: RiskLevel) -> Self {
Self { inner, min_risk }
}
}
impl<S: DiagnosticSink> DiagnosticSink for FilteredSink<S> {
fn receive(&mut self, diagnostic: &Diagnostic) {
if diagnostic.risk >= self.min_risk {
self.inner.receive(diagnostic);
}
}
}
#[derive(Debug, Default)]
pub struct NullSink;
impl DiagnosticSink for NullSink {
fn receive(&mut self, _diagnostic: &Diagnostic) {}
}
impl DiagnosticSink for Vec<Diagnostic> {
fn receive(&mut self, diagnostic: &Diagnostic) {
self.push(diagnostic.clone());
}
}
impl std::fmt::Display for RiskLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RiskLevel::Info => write!(f, "info"),
RiskLevel::Warning => write!(f, "warning"),
RiskLevel::Risky => write!(f, "risky"),
}
}
}