use std::fmt::Write;
use crate::diagnostics::report::{
DiagnosticReport, MissingTensor, ProfileRef, ShapeMismatch, TensorPattern,
UnexpectedTensor, Verdict,
};
use crate::diagnostics::shape::ShapeExpr;
pub const MAX_TENSOR_LIST: usize = 200;
pub fn render_text(report: &DiagnosticReport) -> String {
let mut out = String::new();
let _ = writeln!(&mut out, "=== Profile diagnostic: {} ===", report.file);
let _ = writeln!(&mut out);
let _ = writeln!(
&mut out,
"Format: {}, {} tensors, {} metadata entries",
report.format.label, report.format.tensor_count, report.format.metadata_count
);
let arch_line = match report.architecture.declared.as_deref() {
Some(a) => format!("{a} (declared via {})", report.architecture.source),
None => "<none>".into(),
};
let _ = writeln!(&mut out, "Architecture: {arch_line}");
let profile_line = match &report.profile {
ProfileRef::Builtin { name, extends } => {
if let Some(base) = extends {
format!("{name} (built-in; extends {base})")
} else {
format!("{name} (built-in)")
}
}
ProfileRef::File { path, name } => format!("{name} (file: {path})"),
ProfileRef::Pairwise { against } => format!("<pairwise diff against {against}>"),
ProfileRef::None => "<none>".into(),
};
let _ = writeln!(&mut out, "Profile: {profile_line}");
let _ = writeln!(&mut out, "Verdict: {}", verdict_label(report.verdict));
let _ = writeln!(&mut out);
if !report.symbols.is_empty() {
let _ = writeln!(&mut out, "Symbols resolved:");
let width = report
.symbols
.iter()
.map(|s| s.name.len())
.max()
.unwrap_or(0);
for s in &report.symbols {
let _ = writeln!(
&mut out,
" {:<width$} = {:<8} ({})",
s.name, s.value, s.source,
width = width
);
}
let _ = writeln!(&mut out);
}
let required_missing: Vec<&MissingTensor> = report
.missing_tensors
.iter()
.filter(|m| !m.optional)
.collect();
if !required_missing.is_empty() {
let total_entries: usize = required_missing
.iter()
.map(|m| m.pattern.layers.len().max(1))
.sum();
let _ = writeln!(
&mut out,
"Missing required tensors ({} pattern{}, {} entries):",
required_missing.len(),
if required_missing.len() == 1 { "" } else { "s" },
total_entries
);
render_missing_list(&mut out, &required_missing);
let _ = writeln!(&mut out);
}
let optional_missing: Vec<&MissingTensor> = report
.missing_tensors
.iter()
.filter(|m| m.optional)
.collect();
if !optional_missing.is_empty() {
let _ = writeln!(
&mut out,
"Missing optional tensors ({} pattern{}):",
optional_missing.len(),
if optional_missing.len() == 1 { "" } else { "s" },
);
render_missing_list(&mut out, &optional_missing);
let _ = writeln!(&mut out);
}
if !report.unexpected_tensors.is_empty() {
let total: usize = report
.unexpected_tensors
.iter()
.map(|u| u.pattern.layers.len().max(1))
.sum();
let _ = writeln!(
&mut out,
"Unexpected tensors ({} pattern{}, {} entries):",
report.unexpected_tensors.len(),
if report.unexpected_tensors.len() == 1 { "" } else { "s" },
total
);
render_unexpected_list(&mut out, &report.unexpected_tensors);
let _ = writeln!(&mut out);
}
if !report.shape_mismatches.is_empty() {
let _ = writeln!(
&mut out,
"Shape mismatches ({}):",
report.shape_mismatches.len()
);
render_shape_mismatches(&mut out, &report.shape_mismatches);
let _ = writeln!(&mut out);
}
if !report.shape_comparisons_skipped.is_empty() {
let _ = writeln!(
&mut out,
"Shape comparisons skipped ({}):",
report.shape_comparisons_skipped.len()
);
for s in &report.shape_comparisons_skipped {
let _ = writeln!(
&mut out,
" {} — {}",
format_pattern(&s.pattern),
s.reason
);
}
let _ = writeln!(&mut out);
}
let mdd = &report.metadata_deltas;
if !mdd.unexpected.is_empty() || !mdd.missing_required.is_empty() {
let _ = writeln!(&mut out, "Metadata deltas:");
for m in &mdd.unexpected {
let val = m
.value
.as_ref()
.map(|v| v.display_short())
.unwrap_or_else(|| "?".into());
let _ = writeln!(&mut out, " + {} = {val} (unexpected)", m.key);
}
for m in &mdd.missing_required {
let _ = writeln!(&mut out, " - {} (missing, required)", m.key);
}
let _ = writeln!(&mut out);
}
if !report.hypotheses.is_empty() {
let _ = writeln!(&mut out, "Hypotheses:");
for h in &report.hypotheses {
let name = h.name.as_deref().unwrap_or("hypothesis");
let _ = writeln!(&mut out, " [{}] {name}", h.id);
let triggers = {
let mut parts = Vec::new();
if !h.triggered_by.missing.is_empty() {
parts.push(format!(
"missing {}",
h.triggered_by.missing.join(" + missing ")
));
}
if !h.triggered_by.unexpected.is_empty() {
parts.push(format!(
"unexpected {}",
h.triggered_by.unexpected.join(" + unexpected ")
));
}
parts.join(" + ")
};
if !triggers.is_empty() {
let _ = writeln!(&mut out, " Triggered by: {triggers}");
}
let _ = writeln!(&mut out, " > {}", h.message);
let _ = writeln!(&mut out);
}
}
if !report.warnings.is_empty() {
let _ = writeln!(&mut out, "Warnings:");
for w in &report.warnings {
let _ = writeln!(&mut out, " [{}] {}", w.code, w.message);
}
let _ = writeln!(&mut out);
}
let summary = &report.summary;
let _ = writeln!(
&mut out,
"Summary: {} required missing, {} unexpected pattern{}, {} shape mismatch{}, {} hypothes{}.",
summary.required_missing,
summary.unexpected_patterns,
if summary.unexpected_patterns == 1 { "" } else { "s" },
summary.shape_mismatches,
if summary.shape_mismatches == 1 { "" } else { "es" },
summary.hypotheses,
if summary.hypotheses == 1 { "is" } else { "es" },
);
let _ = writeln!(
&mut out,
"Exit: {} ({}).",
report.verdict.exit_code(),
verdict_label(report.verdict)
);
out
}
pub fn render_json(report: &DiagnosticReport) -> serde_json::Value {
serde_json::to_value(report).unwrap_or(serde_json::Value::Null)
}
fn verdict_label(v: Verdict) -> &'static str {
match v {
Verdict::Matches => "MATCHES",
Verdict::OptionalExtras => "OPTIONAL EXTRAS",
Verdict::ProfileMismatch => "PROFILE MISMATCH",
Verdict::UnknownArchitecture => "UNKNOWN ARCHITECTURE",
}
}
fn format_pattern(pattern: &TensorPattern) -> String {
if pattern.layers.is_empty() {
pattern.name.clone()
} else if let Some(range) = contiguous_range(&pattern.layers) {
pattern.name.replace("{layer}", &format!("{{{}..{}}}", range.0, range.1))
} else {
let layers: Vec<String> = pattern.layers.iter().map(|l| l.to_string()).collect();
pattern
.name
.replace("{layer}", &format!("{{{}}}", layers.join(",")))
}
}
fn contiguous_range(layers: &[u32]) -> Option<(u32, u32)> {
if layers.is_empty() {
return None;
}
let first = layers[0];
let last = *layers.last().unwrap();
let len = layers.len() as u32;
if last.checked_sub(first).map(|d| d + 1) == Some(len)
&& layers.windows(2).all(|w| w[1] == w[0] + 1)
{
Some((first, last))
} else {
None
}
}
fn render_missing_list(out: &mut String, items: &[&MissingTensor]) {
for (i, m) in items.iter().enumerate() {
if i >= MAX_TENSOR_LIST {
let _ = writeln!(
out,
" ... ({} more; pass --json for full list)",
items.len() - MAX_TENSOR_LIST
);
break;
}
let _ = writeln!(out, " {}", format_pattern(&m.pattern));
if let Some(shape) = m.expected_shape.as_ref() {
let _ = writeln!(out, " expected shape [{}]", render_shape(shape));
}
}
}
fn render_unexpected_list(out: &mut String, items: &[UnexpectedTensor]) {
for (i, u) in items.iter().enumerate() {
if i >= MAX_TENSOR_LIST {
let _ = writeln!(
out,
" ... ({} more; pass --json for full list)",
items.len() - MAX_TENSOR_LIST
);
break;
}
let shape_str = u
.shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
out,
" {}\n shape [{}] dtype {}",
format_pattern(&u.pattern),
shape_str,
u.dtype.label()
);
}
}
fn render_shape_mismatches(out: &mut String, items: &[ShapeMismatch]) {
for m in items {
let expected = render_shape(&m.expected_shape);
let resolved: String = m
.resolved_expected
.iter()
.map(|r| match r {
Some(v) => v.to_string(),
None => "?".into(),
})
.collect::<Vec<_>>()
.join(", ");
let actual: String = m
.actual_shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
out,
" {}\n expected [{expected}] = [{resolved}]\n actual [{actual}]",
format_pattern(&m.pattern)
);
}
}
fn render_shape(shape: &[ShapeExpr]) -> String {
shape
.iter()
.map(|e| e.as_source().to_string())
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::report::{
ArchitectureInfo, DiagnosticReport, FormatInfo, Hypothesis, HypothesisTriggers,
MetadataDelta, MetadataDeltas, MissingTensor, ProfileRef, ResolvedSymbol,
ShapeMismatch, Summary, TensorPattern, UnexpectedTensor, Verdict, SCHEMA_VERSION,
};
use crate::diagnostics::shape::ShapeExpr;
use crate::diagnostics::types::{MetadataValue, TensorDtype};
fn matching_report() -> DiagnosticReport {
DiagnosticReport {
schema_version: SCHEMA_VERSION,
file: "/tmp/good.gguf".into(),
format: FormatInfo {
kind: "gguf".into(),
label: "GGUF v3".into(),
tensor_count: 5,
metadata_count: 10,
},
architecture: ArchitectureInfo {
declared: Some("llama".into()),
source: "general.architecture".into(),
},
profile: ProfileRef::Builtin {
name: "llama3".into(),
extends: None,
},
verdict: Verdict::Matches,
symbols: vec![ResolvedSymbol {
name: "hidden".into(),
value: 4096,
source: "metadata:llama.embedding_length".into(),
}],
missing_tensors: vec![],
unexpected_tensors: vec![],
shape_mismatches: vec![],
shape_comparisons_skipped: vec![],
metadata_deltas: MetadataDeltas::default(),
hypotheses: vec![],
warnings: vec![],
summary: Summary {
required_missing: 0,
optional_missing: 0,
unexpected_patterns: 0,
shape_mismatches: 0,
hypotheses: 0,
warnings: 0,
},
}
}
#[test]
fn text_renders_matches_header() {
let txt = render_text(&matching_report());
assert!(txt.contains("=== Profile diagnostic:"));
assert!(txt.contains("Verdict: MATCHES"));
assert!(txt.contains("hidden"));
assert!(txt.contains("Exit: 0 (MATCHES)"));
}
#[test]
fn text_renders_per_layer_range_when_contiguous() {
let mut r = matching_report();
r.verdict = Verdict::ProfileMismatch;
r.missing_tensors = vec![MissingTensor {
pattern: TensorPattern {
name: "blk.{layer}.ffn_norm.weight".into(),
per_layer_count: Some(4),
layers: vec![0, 1, 2, 3],
},
expected_shape: Some(vec![ShapeExpr::from_str("hidden")]),
optional: false,
}];
r.summary.required_missing = 1;
let txt = render_text(&r);
assert!(txt.contains("blk.{0..3}.ffn_norm.weight"), "{txt}");
assert!(txt.contains("expected shape [hidden]"), "{txt}");
}
#[test]
fn text_renders_explicit_layers_when_noncontiguous() {
let mut r = matching_report();
r.verdict = Verdict::ProfileMismatch;
r.missing_tensors = vec![MissingTensor {
pattern: TensorPattern {
name: "blk.{layer}.x.weight".into(),
per_layer_count: Some(4),
layers: vec![0, 2, 3],
},
expected_shape: None,
optional: false,
}];
let txt = render_text(&r);
assert!(txt.contains("blk.{0,2,3}.x.weight"), "{txt}");
}
#[test]
fn text_includes_hypotheses_and_metadata_deltas() {
let mut r = matching_report();
r.verdict = Verdict::ProfileMismatch;
r.missing_tensors = vec![MissingTensor {
pattern: TensorPattern {
name: "blk.{layer}.ffn_norm.weight".into(),
per_layer_count: Some(2),
layers: vec![0, 1],
},
expected_shape: None,
optional: false,
}];
r.unexpected_tensors = vec![UnexpectedTensor {
pattern: TensorPattern {
name: "blk.{layer}.post_attention_norm.weight".into(),
per_layer_count: Some(2),
layers: vec![0, 1],
},
shape: vec![4096],
dtype: TensorDtype::F16,
}];
r.metadata_deltas = MetadataDeltas {
unexpected: vec![MetadataDelta {
key: "foo.key".into(),
value: Some(MetadataValue::UInt(1)),
}],
missing_required: vec![MetadataDelta {
key: "bar.key".into(),
value: None,
}],
};
r.hypotheses = vec![Hypothesis {
id: 1,
name: Some("rename".into()),
triggered_by: HypothesisTriggers {
missing: vec!["blk.*.ffn_norm.weight".into()],
unexpected: vec!["blk.*.post_attention_norm.weight".into()],
},
message: "FFN norm renamed.".into(),
}];
r.summary.required_missing = 1;
r.summary.unexpected_patterns = 1;
r.summary.hypotheses = 1;
let txt = render_text(&r);
assert!(txt.contains("[1] rename"));
assert!(txt.contains("FFN norm renamed."));
assert!(txt.contains("+ foo.key"));
assert!(txt.contains("- bar.key"));
}
#[test]
fn text_renders_unknown_architecture() {
let mut r = matching_report();
r.verdict = Verdict::UnknownArchitecture;
r.architecture = ArchitectureInfo {
declared: None,
source: "none".into(),
};
r.profile = ProfileRef::None;
let txt = render_text(&r);
assert!(txt.contains("Verdict: UNKNOWN ARCHITECTURE"));
assert!(txt.contains("Architecture: <none>"));
assert!(txt.contains("Profile: <none>"));
assert!(txt.contains("Exit: 3"));
}
#[test]
fn text_truncates_very_long_lists() {
let mut r = matching_report();
r.verdict = Verdict::ProfileMismatch;
r.missing_tensors = (0..(MAX_TENSOR_LIST + 5) as u32)
.map(|i| MissingTensor {
pattern: TensorPattern {
name: format!("tensor{i}.weight"),
per_layer_count: None,
layers: vec![],
},
expected_shape: None,
optional: false,
})
.collect();
r.summary.required_missing = r.missing_tensors.len() as u32;
let txt = render_text(&r);
assert!(
txt.contains("(5 more; pass --json for full list)"),
"should note truncation: {}",
&txt[txt.len().saturating_sub(200)..]
);
}
#[test]
fn json_renders_with_schema_version() {
let json = render_json(&matching_report());
assert_eq!(json["schema_version"], serde_json::json!(1));
assert_eq!(json["verdict"], serde_json::json!("matches"));
}
}