use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Diagnostic {
pub code: String,
pub severity: Severity,
pub subject: String,
pub message: String,
pub evidence: serde_json::Value,
pub supported_fixes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HashInfo {
pub algorithm: String,
pub hash: String,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidationSummary {
pub checks_passed: u32,
pub check_count: u32,
pub errors: u32,
pub warnings: u32,
pub quality_profile: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceSummary {
pub verified: bool,
pub repository: Option<String>,
pub revision: Option<String>,
pub reference_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Receipt {
pub command: String,
pub specification: HashInfo,
pub artifact: HashInfo,
pub validation: ValidationSummary,
pub evidence: Option<EvidenceSummary>,
pub diagnostics: Vec<Diagnostic>,
}
pub const AMBIGUOUS_SYMBOL_CODE: &str = "resolve/ambiguous-symbol";
#[must_use]
pub fn ambiguous_symbol_diagnostics(symbol: &str, candidates: &[String]) -> Vec<Diagnostic> {
let supported_fixes = candidates
.iter()
.map(|qn| format!("Use the qualified name `{qn}` instead of `{symbol}`"))
.collect();
vec![Diagnostic {
code: AMBIGUOUS_SYMBOL_CODE.to_string(),
severity: Severity::Error,
subject: symbol.to_string(),
message: format!(
"ambiguous symbol '{symbol}': {} candidates found",
candidates.len()
),
evidence: serde_json::json!({
"candidate_count": candidates.len(),
"candidates": candidates,
}),
supported_fixes,
}]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostic_serializes_exact_field_names() {
let d = Diagnostic {
code: "index/stale".to_string(),
severity: Severity::Warning,
subject: "demo".to_string(),
message: "index is stale".to_string(),
evidence: serde_json::json!({ "indexed_commit": "a", "current_head": "b" }),
supported_fixes: vec!["Re-run: codenexus index".to_string()],
};
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("\"code\":\"index/stale\""), "{json}");
assert!(json.contains("\"severity\":\"warning\""), "{json}");
assert!(json.contains("\"subject\""), "{json}");
assert!(json.contains("\"message\""), "{json}");
assert!(json.contains("\"evidence\""), "{json}");
assert!(json.contains("\"supported_fixes\""), "{json}");
}
#[test]
fn severity_serializes_and_orders_by_seriousness() {
let json_warn = serde_json::to_string(&Severity::Warning).unwrap();
let json_err = serde_json::to_string(&Severity::Error).unwrap();
assert_eq!(json_warn, "\"warning\"");
assert_eq!(json_err, "\"error\"");
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn receipt_serializes_full_shape() {
let receipt = Receipt {
command: "diagram".to_string(),
specification: HashInfo {
algorithm: "blake3".to_string(),
hash: "aa".to_string(),
bytes: 1,
},
artifact: HashInfo {
algorithm: "blake3".to_string(),
hash: "bb".to_string(),
bytes: 2,
},
validation: ValidationSummary {
checks_passed: 7,
check_count: 7,
errors: 0,
warnings: 0,
quality_profile: "standard".to_string(),
},
evidence: None,
diagnostics: vec![],
};
let json = serde_json::to_string(&receipt).unwrap();
for field in [
"\"command\"",
"\"specification\"",
"\"artifact\"",
"\"validation\"",
"\"evidence\"",
"\"diagnostics\"",
"\"checks_passed\"",
"\"quality_profile\"",
] {
assert!(json.contains(field), "missing {field} in {json}");
}
assert!(json.contains("\"evidence\":null"), "{json}");
assert!(json.contains("\"algorithm\":\"blake3\""), "{json}");
}
#[test]
fn ambiguous_symbol_diagnostics_lists_candidates_as_fixes() {
let candidates = vec!["demo.a.handler".to_string(), "demo.b.handler".to_string()];
let diags = ambiguous_symbol_diagnostics("handler", &candidates);
assert_eq!(diags.len(), 1);
let d = &diags[0];
assert_eq!(d.code, "resolve/ambiguous-symbol");
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.subject, "handler");
assert_eq!(d.evidence["candidate_count"], 2);
assert_eq!(d.evidence["candidates"][0], "demo.a.handler");
assert_eq!(d.supported_fixes.len(), 2);
assert!(d.supported_fixes[0].contains("demo.a.handler"));
assert!(d.supported_fixes[1].contains("demo.b.handler"));
}
#[test]
fn ambiguous_symbol_diagnostics_empty_candidates_still_emits_receipt() {
let diags = ambiguous_symbol_diagnostics("ghost", &[]);
assert_eq!(diags.len(), 1);
assert!(diags[0].supported_fixes.is_empty());
assert_eq!(diags[0].evidence["candidate_count"], 0);
}
}