use serde_json::Value;
use super::message_types::SystemSubtype;
use super::ClaudeOutput;
#[derive(Debug, Clone)]
pub struct FrameAudit {
pub message_type: String,
pub fully_wrapped: bool,
pub issues: Vec<String>,
}
pub fn audit_frame(raw: &Value) -> FrameAudit {
let mut issues = Vec::new();
let parsed: ClaudeOutput = match serde_json::from_value(raw.clone()) {
Ok(parsed) => parsed,
Err(e) => {
let message_type = raw
.get("type")
.and_then(Value::as_str)
.unwrap_or("<unknown>")
.to_string();
return FrameAudit {
message_type,
fully_wrapped: false,
issues: vec![format!(
"does not deserialize into a typed ClaudeOutput: {e}"
)],
};
}
};
let message_type = parsed.message_type();
match serde_json::to_value(&parsed) {
Ok(reserialized) => diff_lost(raw, &reserialized, "", &mut issues),
Err(e) => issues.push(format!("typed value failed to re-serialize: {e}")),
}
if let ClaudeOutput::System(sys) = &parsed {
match (&sys.subtype, sys.typed_value()) {
(SystemSubtype::Unknown(s), _) => issues.push(format!(
"system subtype '{s}' is not modeled — its fields stay in an untyped Value"
)),
(_, Some(typed)) => diff_lost(&sys.data, &typed, "", &mut issues),
(subtype, None) => issues.push(format!(
"system subtype '{subtype}' has no dedicated typed view"
)),
}
}
FrameAudit {
message_type,
fully_wrapped: issues.is_empty(),
issues,
}
}
pub fn assert_fully_wrapped(raw: &Value) {
let audit = audit_frame(raw);
assert!(
audit.fully_wrapped,
"frame (type={}) is not fully wrapped:\n - {}\nraw frame: {}",
audit.message_type,
audit.issues.join("\n - "),
raw,
);
}
fn carries_no_data(v: &Value) -> bool {
match v {
Value::Null => true,
Value::Array(a) => a.is_empty(),
Value::Object(o) => o.is_empty(),
_ => false,
}
}
fn diff_lost(wire: &Value, typed: &Value, path: &str, out: &mut Vec<String>) {
match (wire, typed) {
(Value::Object(w), Value::Object(t)) => {
for (key, wire_val) in w {
let child = format!("{path}/{key}");
match t.get(key) {
Some(typed_val) => diff_lost(wire_val, typed_val, &child, out),
None if carries_no_data(wire_val) => {}
None => out.push(format!(
"field `{child}` present on the wire but dropped by the typed model"
)),
}
}
}
(Value::Array(w), Value::Array(t)) => {
if w.len() != t.len() {
out.push(format!(
"array `{path}` has {} element(s) on the wire but {} after typed round-trip",
w.len(),
t.len()
));
} else {
for (i, (wv, tv)) in w.iter().zip(t.iter()).enumerate() {
diff_lost(wv, tv, &format!("{path}/{i}"), out);
}
}
}
_ => {
if wire != typed {
out.push(format!(
"value at `{path}` changed on typed round-trip (wire={wire}, typed={typed})"
));
}
}
}
}