use serde::{Deserialize, Serialize};
use crate::diagnostics::shape::ShapeExpr;
use crate::diagnostics::types::{MetadataValue, TensorDtype};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Matches,
OptionalExtras,
ProfileMismatch,
UnknownArchitecture,
}
impl Verdict {
pub fn exit_code(self) -> i32 {
match self {
Verdict::Matches | Verdict::OptionalExtras => 0,
Verdict::ProfileMismatch => 2,
Verdict::UnknownArchitecture => 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum ProfileRef {
Builtin { name: String, extends: Option<String> },
File { path: String, name: String },
Pairwise { against: String },
None,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResolvedSymbol {
pub name: String,
pub value: u64,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TensorPattern {
pub name: String,
pub per_layer_count: Option<u32>,
#[serde(default)]
pub layers: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MissingTensor {
pub pattern: TensorPattern,
pub expected_shape: Option<Vec<ShapeExpr>>,
pub optional: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnexpectedTensor {
pub pattern: TensorPattern,
pub shape: Vec<usize>,
pub dtype: TensorDtype,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShapeMismatch {
pub pattern: TensorPattern,
pub expected_shape: Vec<ShapeExpr>,
pub actual_shape: Vec<usize>,
pub resolved_expected: Vec<Option<u64>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShapeComparisonSkipped {
pub pattern: TensorPattern,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetadataDelta {
pub key: String,
pub value: Option<MetadataValue>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Hypothesis {
pub id: u32,
pub name: Option<String>,
pub triggered_by: HypothesisTriggers,
pub message: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct HypothesisTriggers {
#[serde(default)]
pub missing: Vec<String>,
#[serde(default)]
pub unexpected: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Warning {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct MetadataDeltas {
#[serde(default)]
pub unexpected: Vec<MetadataDelta>,
#[serde(default)]
pub missing_required: Vec<MetadataDelta>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FormatInfo {
pub kind: String, pub label: String,
pub tensor_count: u32,
pub metadata_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArchitectureInfo {
pub declared: Option<String>,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Summary {
pub required_missing: u32,
pub optional_missing: u32,
pub unexpected_patterns: u32,
pub shape_mismatches: u32,
pub hypotheses: u32,
pub warnings: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DiagnosticReport {
pub schema_version: u32,
pub file: String,
pub format: FormatInfo,
pub architecture: ArchitectureInfo,
pub profile: ProfileRef,
pub verdict: Verdict,
#[serde(default)]
pub symbols: Vec<ResolvedSymbol>,
#[serde(default)]
pub missing_tensors: Vec<MissingTensor>,
#[serde(default)]
pub unexpected_tensors: Vec<UnexpectedTensor>,
#[serde(default)]
pub shape_mismatches: Vec<ShapeMismatch>,
#[serde(default)]
pub shape_comparisons_skipped: Vec<ShapeComparisonSkipped>,
#[serde(default)]
pub metadata_deltas: MetadataDeltas,
#[serde(default)]
pub hypotheses: Vec<Hypothesis>,
#[serde(default)]
pub warnings: Vec<Warning>,
pub summary: Summary,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_exit_codes() {
assert_eq!(Verdict::Matches.exit_code(), 0);
assert_eq!(Verdict::OptionalExtras.exit_code(), 0);
assert_eq!(Verdict::ProfileMismatch.exit_code(), 2);
assert_eq!(Verdict::UnknownArchitecture.exit_code(), 3);
}
#[test]
fn verdict_round_trips_as_snake_case_json() {
for v in [
Verdict::Matches,
Verdict::OptionalExtras,
Verdict::ProfileMismatch,
Verdict::UnknownArchitecture,
] {
let json = serde_json::to_string(&v).unwrap();
let back: Verdict = serde_json::from_str(&json).unwrap();
assert_eq!(v, back);
}
let s = serde_json::to_string(&Verdict::ProfileMismatch).unwrap();
assert_eq!(s, r#""profile_mismatch""#);
}
#[test]
fn profile_ref_round_trips() {
let cases = [
ProfileRef::Builtin {
name: "qwen3".into(),
extends: Some("llama".into()),
},
ProfileRef::File {
path: "/tmp/x.toml".into(),
name: "x".into(),
},
ProfileRef::Pairwise {
against: "/tmp/other.gguf".into(),
},
ProfileRef::None,
];
for p in cases {
let json = serde_json::to_string(&p).unwrap();
let back: ProfileRef = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
}
#[test]
fn report_round_trips_through_json() {
let report = DiagnosticReport {
schema_version: SCHEMA_VERSION,
file: "/tmp/x.gguf".into(),
format: FormatInfo {
kind: "gguf".into(),
label: "GGUF v3".into(),
tensor_count: 10,
metadata_count: 5,
},
architecture: ArchitectureInfo {
declared: Some("qwen3moe".into()),
source: "general.architecture".into(),
},
profile: ProfileRef::Builtin {
name: "qwen3moe".into(),
extends: None,
},
verdict: Verdict::ProfileMismatch,
symbols: vec![ResolvedSymbol {
name: "hidden".into(),
value: 4096,
source: "metadata:qwen3moe.embedding_length".into(),
}],
missing_tensors: vec![MissingTensor {
pattern: TensorPattern {
name: "blk.{layer}.ffn_norm.weight".into(),
per_layer_count: Some(48),
layers: (0..48).collect(),
},
expected_shape: Some(vec![ShapeExpr::from_str("hidden")]),
optional: false,
}],
unexpected_tensors: vec![],
shape_mismatches: vec![],
shape_comparisons_skipped: vec![],
metadata_deltas: MetadataDeltas::default(),
hypotheses: vec![Hypothesis {
id: 1,
name: Some("rename".into()),
triggered_by: HypothesisTriggers {
missing: vec!["blk.*.ffn_norm.weight".into()],
unexpected: vec![],
},
message: "FFN norm renamed.".into(),
}],
warnings: vec![],
summary: Summary {
required_missing: 1,
optional_missing: 0,
unexpected_patterns: 0,
shape_mismatches: 0,
hypotheses: 1,
warnings: 0,
},
};
let json = serde_json::to_string(&report).unwrap();
let back: DiagnosticReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, back);
assert!(json.contains(r#""schema_version":1"#));
}
}