Skip to main content

perspt_coding/diag/
types.rs

1//! Structured diagnostics and their conversion to residual evidence
2//! (PSP-10 system 26).
3//!
4//! Language adapters preserve as much source structure as each tool
5//! provides; `EvidencePayload::raw` and `::structured` carry the actual
6//! evidence — the historic summary-only, score-1.0 residuals are gone.
7
8use perspt_sdk::{
9    EvidencePayload, ResidualClass, ResidualEvent, ResidualSeverity, SensorRef,
10    StructuredDiagnosticRef,
11};
12
13/// One diagnostic normalized from a tool's native output.
14#[derive(Debug, Clone, PartialEq)]
15pub struct StructuredDiagnostic {
16    pub class: ResidualClass,
17    pub code: Option<String>,
18    pub message: String,
19    pub path: Option<String>,
20    pub line: Option<u32>,
21    pub column: Option<u32>,
22    /// Whether the tool marks this as a primary diagnostic (cascade
23    /// children are secondary).
24    pub primary: bool,
25    /// Applicable fix suggestion, when the tool provides one.
26    pub suggestion: Option<String>,
27}
28
29impl StructuredDiagnostic {
30    pub fn to_ref(&self) -> StructuredDiagnosticRef {
31        StructuredDiagnosticRef {
32            code: self.code.clone(),
33            message: self.message.clone(),
34            path: self.path.clone(),
35            line: self.line,
36            column: self.column,
37        }
38    }
39}
40
41/// Build one cluster-level residual event carrying full evidence.
42#[allow(clippy::too_many_arguments)]
43pub fn cluster_residual(
44    node_id: &str,
45    generation: u32,
46    class: ResidualClass,
47    magnitude: f64,
48    sensor: SensorRef,
49    summary: &str,
50    raw: &str,
51    members: &[StructuredDiagnostic],
52) -> anyhow::Result<ResidualEvent> {
53    let mut event = ResidualEvent::new(
54        node_id,
55        generation,
56        class,
57        ResidualSeverity::Error,
58        magnitude,
59        sensor,
60    )
61    .map_err(|e| anyhow::anyhow!("{e}"))?;
62    event.evidence = EvidencePayload {
63        summary: summary.to_string(),
64        raw: Some(bounded(raw, 16 * 1024)),
65        structured: Some(serde_json::to_value(
66            members
67                .iter()
68                .map(StructuredDiagnostic::to_ref)
69                .collect::<Vec<_>>(),
70        )?),
71    };
72    event.affected_paths = members
73        .iter()
74        .filter_map(|member| member.path.clone())
75        .collect::<std::collections::BTreeSet<_>>()
76        .into_iter()
77        .collect();
78    Ok(event)
79}
80
81fn bounded(raw: &str, cap: usize) -> String {
82    if raw.len() <= cap {
83        return raw.to_string();
84    }
85    let mut end = cap;
86    while !raw.is_char_boundary(end) {
87        end -= 1;
88    }
89    format!("{}\n[truncated {} bytes]", &raw[..end], raw.len() - end)
90}