use std::collections::HashMap;
use serde_json::Value;
use crate::ast::{Declaration, Program, TypeDefinition};
#[derive(Debug, Clone, PartialEq)]
pub struct TypeSchema {
pub name: String,
pub fields: Vec<FieldSchema>,
pub range: Option<(f64, f64)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldSchema {
pub name: String,
pub type_name: String,
pub generic_param: String,
pub optional: bool,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
pub struct BodyValidationError {
pub expected_type: String,
pub field_path: String,
pub expected: String,
pub got: String,
pub hint: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub expected_cardinality: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub got_cardinality: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub got_length: Option<u64>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub remediation_url: String,
}
impl std::fmt::Display for BodyValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.hint)
}
}
impl std::error::Error for BodyValidationError {}
pub const BUILTIN_PRIMITIVES: &[&str] = &[
"String",
"Integer",
"Float",
"Boolean",
"Duration",
"Any",
];
pub fn builtin_range(name: &str) -> Option<(f64, f64)> {
match name {
"RiskScore" | "ConfidenceScore" => Some((0.0, 1.0)),
"SentimentScore" => Some((-1.0, 1.0)),
_ => None,
}
}
pub fn collect_type_table(program: &Program) -> HashMap<String, TypeSchema> {
let mut table = HashMap::new();
for decl in &program.declarations {
if let Declaration::Type(td) = decl {
table.insert(td.name.clone(), type_schema_from(td));
}
}
table
}
fn type_schema_from(td: &TypeDefinition) -> TypeSchema {
let fields = td
.fields
.iter()
.map(|f| FieldSchema {
name: f.name.clone(),
type_name: f.type_expr.name.clone(),
generic_param: f.type_expr.generic_param.clone(),
optional: f.type_expr.optional,
})
.collect();
let range = td
.range_constraint
.as_ref()
.map(|rc| (rc.min_value, rc.max_value));
TypeSchema {
name: td.name.clone(),
fields,
range,
}
}
fn json_tag(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) => {
if n.is_i64() || n.is_u64() {
"integer"
} else {
"number"
}
}
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub fn validate_body(
body: &Value,
type_name: &str,
table: &HashMap<String, TypeSchema>,
) -> Result<(), BodyValidationError> {
let t = type_name.trim();
if t.is_empty() {
return Ok(());
}
if let Some(inner) = strip_flow_envelope(t) {
let obj = match body.as_object() {
Some(o) => o,
None => {
return Err(BodyValidationError {
expected_type: type_name.to_string(),
field_path: String::new(),
expected: t.to_string(),
got: json_tag(body).to_string(),
hint: format!(
"axonendpoint declared `output: {t}` but the response \
body is not a JSON object — the FlowEnvelope wire \
shape requires `{{ontological_type, result, …}}`. \
This typically indicates a bug in the response wrapper."
),
..Default::default()
});
}
};
let result_slot = obj
.get("result")
.cloned()
.unwrap_or(Value::Null);
if inner == "Any" {
return Ok(());
}
return validate_body(&result_slot, &inner, table);
}
let (head, generic) = parse_generic_head(t);
validate_value(body, &head, &generic, "", table, t)
}
fn strip_flow_envelope(t: &str) -> Option<String> {
let rest = t.strip_prefix("FlowEnvelope<")?;
let inner = rest.strip_suffix('>')?;
Some(inner.trim().to_string())
}
fn parse_generic_head(t: &str) -> (String, String) {
if let Some(rest) = t.strip_prefix("List<") {
if let Some(inner) = rest.strip_suffix('>') {
return ("List".to_string(), inner.trim().to_string());
}
}
if let Some(rest) = t.strip_prefix("Stream<") {
if let Some(inner) = rest.strip_suffix('>') {
return ("Stream".to_string(), inner.trim().to_string());
}
}
(t.to_string(), String::new())
}
fn validate_value(
v: &Value,
type_name: &str,
generic_param: &str,
field_path: &str,
table: &HashMap<String, TypeSchema>,
body_type: &str,
) -> Result<(), BodyValidationError> {
if type_name == "Stream" {
return Ok(());
}
if BUILTIN_PRIMITIVES.contains(&type_name) {
return validate_primitive(v, type_name, field_path, body_type);
}
if let Some((lo, hi)) = builtin_range(type_name) {
return validate_ranged_number(v, type_name, lo, hi, field_path, body_type);
}
if type_name == "List" {
return validate_list(v, generic_param, field_path, table, body_type);
}
if let Some(schema) = table.get(type_name) {
if let Some((lo, hi)) = schema.range {
return validate_ranged_number(v, type_name, lo, hi, field_path, body_type);
}
return validate_struct(v, schema, field_path, table, body_type);
}
Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: type_name.to_string(),
got: json_tag(v).to_string(),
hint: format!(
"axonendpoint declared an unknown body type `{type_name}` for field \
`{field_path}` — neither a built-in primitive nor a declared \
`type` in the deployed source. Add `type {type_name} {{ … }}` to \
the source or correct the spelling."
),
..Default::default()
})
}
fn validate_primitive(
v: &Value,
type_name: &str,
field_path: &str,
body_type: &str,
) -> Result<(), BodyValidationError> {
let ok = match (type_name, v) {
("String", Value::String(_)) => true,
("Integer", Value::Number(n)) => n.is_i64() || n.is_u64(),
("Float", Value::Number(_)) => true,
("Boolean", Value::Bool(_)) => true,
("Duration", Value::String(_)) => true,
("Any", _) => true,
_ => false,
};
if ok {
return Ok(());
}
Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: type_name.to_string(),
got: json_tag(v).to_string(),
hint: format!(
"Body field `{field_path}` must be a `{type_name}` but received a \
{got}. Adjust the request body or the axonendpoint's `body:` \
declaration.",
field_path = if field_path.is_empty() { "<body>" } else { field_path },
type_name = type_name,
got = json_tag(v),
),
..Default::default()
})
}
pub fn fmt_f64(n: f64) -> String {
if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e16 {
return format!("{}", n as i64);
}
format!("{n}")
}
fn validate_ranged_number(
v: &Value,
type_name: &str,
lo: f64,
hi: f64,
field_path: &str,
body_type: &str,
) -> Result<(), BodyValidationError> {
let n = match (v, v.as_f64()) {
(Value::Number(_), Some(n)) => n,
_ => {
return Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: type_name.to_string(),
got: json_tag(v).to_string(),
hint: format!(
"Body field `{path}` must be a `{type_name}` (numeric in \
[{lo}, {hi}]) but received a {got}.",
path = if field_path.is_empty() { "<body>" } else { field_path },
type_name = type_name,
got = json_tag(v),
lo = fmt_f64(lo),
hi = fmt_f64(hi),
),
..Default::default()
});
}
};
if n < lo || n > hi {
let lo_s = fmt_f64(lo);
let hi_s = fmt_f64(hi);
let n_s = fmt_f64(n);
return Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: format!("{type_name} ∈ [{lo_s}, {hi_s}]"),
got: n_s.clone(),
hint: format!(
"Body field `{path}` must satisfy `{type_name} ∈ [{lo_s}, \
{hi_s}]` but received `{n_s}`.",
path = if field_path.is_empty() { "<body>" } else { field_path },
),
..Default::default()
});
}
Ok(())
}
fn validate_list(
v: &Value,
element_type: &str,
field_path: &str,
table: &HashMap<String, TypeSchema>,
body_type: &str,
) -> Result<(), BodyValidationError> {
let arr = match v.as_array() {
Some(a) => a,
None => {
return Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: format!("List<{element_type}>"),
got: json_tag(v).to_string(),
hint: format!(
"Body field `{path}` must be a `List<{element_type}>` \
(JSON array) but received a {got}.",
path = if field_path.is_empty() { "<body>" } else { field_path },
got = json_tag(v),
),
expected_cardinality: if field_path.is_empty() {
"plural".to_string()
} else {
String::new()
},
got_cardinality: if field_path.is_empty() {
match v {
Value::Object(_) => "singular".to_string(),
Value::Null => "unit".to_string(),
_ => "singular".to_string(),
}
} else {
String::new()
},
got_length: None,
remediation_url: if field_path.is_empty() {
"https://axon-lang.io/docs/cardinality-mismatch".to_string()
} else {
String::new()
},
});
}
};
if element_type.is_empty() {
return Ok(());
}
let (elem_head, elem_generic) = parse_generic_head(element_type);
for (idx, elem) in arr.iter().enumerate() {
let elem_path = if field_path.is_empty() {
format!("[{idx}]")
} else {
format!("{field_path}[{idx}]")
};
validate_value(
elem,
&elem_head,
&elem_generic,
&elem_path,
table,
body_type,
)?;
}
Ok(())
}
fn validate_struct(
v: &Value,
schema: &TypeSchema,
field_path: &str,
table: &HashMap<String, TypeSchema>,
body_type: &str,
) -> Result<(), BodyValidationError> {
let obj = match v.as_object() {
Some(o) => o,
None => {
return Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: field_path.to_string(),
expected: schema.name.clone(),
got: json_tag(v).to_string(),
hint: format!(
"Body field `{path}` must be a `{type_name}` (JSON object) \
but received a {got}. {cardinality_hint}",
path = if field_path.is_empty() { "<body>" } else { field_path },
type_name = schema.name,
got = json_tag(v),
cardinality_hint = if field_path.is_empty() && v.is_array() {
format!(
"The flow returned a `List<{tn}>` (array of {n} \
items) but the endpoint declared `output: {tn}` \
(singular). Either change the endpoint to \
`output: List<{tn}>` or collapse the flow's tail \
to a single item (e.g. `return result[0]`). \
(Fase 38.x.f D2)",
tn = schema.name,
n = v.as_array().map(|a| a.len()).unwrap_or(0),
)
} else {
String::new()
},
),
expected_cardinality: if field_path.is_empty() {
"singular".to_string()
} else {
String::new()
},
got_cardinality: if field_path.is_empty() {
match v {
Value::Array(_) => "plural".to_string(),
Value::Null => "unit".to_string(),
_ => "singular".to_string(),
}
} else {
String::new()
},
got_length: if field_path.is_empty() {
v.as_array().map(|a| a.len() as u64)
} else {
None
},
remediation_url: if field_path.is_empty() && v.is_array() {
"https://axon-lang.io/docs/cardinality-mismatch".to_string()
} else {
String::new()
},
});
}
};
for field in &schema.fields {
let child_path = if field_path.is_empty() {
field.name.clone()
} else {
format!("{field_path}.{}", field.name)
};
match obj.get(&field.name) {
None => {
if field.optional {
continue;
}
return Err(BodyValidationError {
expected_type: body_type.to_string(),
field_path: child_path.clone(),
expected: field.type_name.clone(),
got: "missing".to_string(),
hint: format!(
"Body field `{child_path}` is required (declared as \
`{type_name}` on `{struct_name}`) but is absent from \
the request body.",
type_name = field.type_name,
struct_name = schema.name,
),
..Default::default()
});
}
Some(child) => {
if field.optional && child.is_null() {
continue;
}
validate_value(
child,
&field.type_name,
&field.generic_param,
&child_path,
table,
body_type,
)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn t_string() -> TypeSchema {
TypeSchema {
name: "String".to_string(),
fields: vec![],
range: None,
}
}
fn person_schema() -> TypeSchema {
TypeSchema {
name: "Person".to_string(),
fields: vec![
FieldSchema {
name: "name".to_string(),
type_name: "String".to_string(),
generic_param: String::new(),
optional: false,
},
FieldSchema {
name: "age".to_string(),
type_name: "Integer".to_string(),
generic_param: String::new(),
optional: true,
},
],
range: None,
}
}
#[test]
fn empty_body_type_passes_any_body() {
let table = HashMap::new();
let body = serde_json::json!({"anything": "goes"});
assert!(validate_body(&body, "", &table).is_ok());
}
#[test]
fn primitive_string_ok() {
let table = HashMap::new();
let body = serde_json::json!("hello");
assert!(validate_body(&body, "String", &table).is_ok());
}
#[test]
fn primitive_string_rejects_number() {
let table = HashMap::new();
let body = serde_json::json!(42);
let err = validate_body(&body, "String", &table).unwrap_err();
assert_eq!(err.expected, "String");
assert_eq!(err.got, "integer");
}
#[test]
fn integer_rejects_float() {
let table = HashMap::new();
let body = serde_json::json!(3.14);
let err = validate_body(&body, "Integer", &table).unwrap_err();
assert_eq!(err.expected, "Integer");
assert_eq!(err.got, "number");
}
#[test]
fn float_accepts_integer_json() {
let table = HashMap::new();
let body = serde_json::json!(42);
assert!(validate_body(&body, "Float", &table).is_ok());
let body = serde_json::json!(3.14);
assert!(validate_body(&body, "Float", &table).is_ok());
}
#[test]
fn structured_missing_required_field() {
let mut table = HashMap::new();
table.insert("Person".to_string(), person_schema());
let body = serde_json::json!({"age": 30});
let err = validate_body(&body, "Person", &table).unwrap_err();
assert_eq!(err.field_path, "name");
assert_eq!(err.got, "missing");
}
#[test]
fn structured_optional_field_can_be_absent() {
let mut table = HashMap::new();
table.insert("Person".to_string(), person_schema());
let body = serde_json::json!({"name": "alice"});
assert!(validate_body(&body, "Person", &table).is_ok());
}
#[test]
fn structured_optional_field_can_be_null() {
let mut table = HashMap::new();
table.insert("Person".to_string(), person_schema());
let body = serde_json::json!({"name": "alice", "age": null});
assert!(validate_body(&body, "Person", &table).is_ok());
}
#[test]
fn structured_unknown_extra_fields_accepted() {
let mut table = HashMap::new();
table.insert("Person".to_string(), person_schema());
let body = serde_json::json!({"name": "alice", "extra": "data"});
assert!(validate_body(&body, "Person", &table).is_ok());
}
#[test]
fn list_validates_each_element() {
let mut table = HashMap::new();
table.insert("String".to_string(), t_string());
let body = serde_json::json!(["a", "b", "c"]);
let err = validate_body(&body, "List", &table);
assert!(err.is_ok());
}
#[test]
fn list_rejects_non_array() {
let table = HashMap::new();
let body = serde_json::json!({"not": "array"});
let r = validate_value(&body, "List", "String", "", &table, "List");
let err = r.unwrap_err();
assert!(err.expected.contains("List"));
assert_eq!(err.got, "object");
}
#[test]
fn list_element_violation_reports_indexed_path() {
let table = HashMap::new();
let body = serde_json::json!(["a", 42, "c"]);
let r = validate_value(&body, "List", "String", "", &table, "List");
let err = r.unwrap_err();
assert_eq!(err.field_path, "[1]");
assert_eq!(err.got, "integer");
}
#[test]
fn range_type_rejects_out_of_bounds() {
let table = HashMap::new();
let body = serde_json::json!(1.5);
let err = validate_body(&body, "RiskScore", &table).unwrap_err();
assert!(err.expected.contains("RiskScore"));
}
#[test]
fn range_type_accepts_in_bounds() {
let table = HashMap::new();
let body = serde_json::json!(0.7);
assert!(validate_body(&body, "RiskScore", &table).is_ok());
}
#[test]
fn unknown_type_returns_diagnostic() {
let table = HashMap::new();
let body = serde_json::json!({});
let err = validate_body(&body, "NotDeclared", &table).unwrap_err();
assert!(err.hint.contains("NotDeclared"));
}
#[test]
fn nested_struct_field_path_is_dotted() {
let mut table = HashMap::new();
table.insert("Person".to_string(), person_schema());
table.insert(
"Loan".to_string(),
TypeSchema {
name: "Loan".to_string(),
fields: vec![FieldSchema {
name: "applicant".to_string(),
type_name: "Person".to_string(),
generic_param: String::new(),
optional: false,
}],
range: None,
},
);
let body = serde_json::json!({"applicant": {"age": 30}});
let err = validate_body(&body, "Loan", &table).unwrap_err();
assert_eq!(err.field_path, "applicant.name");
assert_eq!(err.expected_type, "Loan");
}
#[test]
fn json_tag_distinguishes_integer_and_number() {
assert_eq!(json_tag(&serde_json::json!(42)), "integer");
assert_eq!(json_tag(&serde_json::json!(3.14)), "number");
}
#[test]
fn fase38xf9_validate_body_accepts_list_of_primitive() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!(["alice", "bob"]);
let r = validate_body(&body, "List<String>", &table);
assert!(
r.is_ok(),
"List<String> over a String array must validate. Got: {r:?}"
);
}
#[test]
fn fase38xf9_validate_body_accepts_list_of_struct() {
let mut table: HashMap<String, TypeSchema> = HashMap::new();
table.insert("Person".to_string(), person_schema());
let body = serde_json::json!([{"name": "alice", "age": 30}, {"name": "bob", "age": 25}]);
let r = validate_body(&body, "List<Person>", &table);
assert!(
r.is_ok(),
"List<Person> over a Person array must validate. Got: {r:?}"
);
}
#[test]
fn fase38xf9_validate_body_rejects_list_of_unknown_inner() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!([{}]);
let r = validate_body(&body, "List<UnknownType>", &table);
assert!(r.is_err(), "List<UnknownType> must surface the inner-type miss.");
let err = r.unwrap_err();
assert!(
err.hint.contains("UnknownType"),
"diagnostic must name the inner type (`UnknownType`), not the outer `List<...>` shape. \
Got hint: {}",
err.hint
);
}
#[test]
fn fase38xf9_validate_body_rejects_list_against_non_array() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!({"not": "an array"});
let r = validate_body(&body, "List<String>", &table);
assert!(r.is_err(), "object against List<String> must error.");
let err = r.unwrap_err();
assert_eq!(err.got, "object");
assert!(err.expected.contains("List"));
}
#[test]
fn fase38xf9_validate_body_accepts_nested_list_of_list() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!([["a", "b"], ["c"]]);
let r = validate_body(&body, "List<List<String>>", &table);
assert!(
r.is_ok(),
"Nested List<List<String>> over an array-of-arrays must validate. Got: {r:?}"
);
}
#[test]
fn fase38xf9_validate_body_stream_returns_ok_early() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!({"anything": "goes"});
let r = validate_body(&body, "Stream<Token>", &table);
assert!(
r.is_ok(),
"Stream<T> at the body validator layer must be a defensive Ok. \
Got: {r:?}"
);
}
#[test]
fn fase39d_parse_generic_head_list() {
let (h, g) = parse_generic_head("List<TenantRecord>");
assert_eq!(h, "List");
assert_eq!(g, "TenantRecord");
}
#[test]
fn fase39d_parse_generic_head_stream() {
let (h, g) = parse_generic_head("Stream<Token>");
assert_eq!(h, "Stream");
assert_eq!(g, "Token");
}
#[test]
fn fase39d_parse_generic_head_nested_list() {
let (h, g) = parse_generic_head("List<List<X>>");
assert_eq!(h, "List");
assert_eq!(g, "List<X>");
}
#[test]
fn fase39d_parse_generic_head_bare_type() {
let (h, g) = parse_generic_head("TenantRecord");
assert_eq!(h, "TenantRecord");
assert_eq!(g, "");
}
#[test]
fn fase39d_parse_generic_head_inner_whitespace_trimmed() {
let (h, g) = parse_generic_head("List< TenantRecord >");
assert_eq!(h, "List");
assert_eq!(g, "TenantRecord");
}
#[test]
fn fase39d_strip_flow_envelope_singular() {
assert_eq!(
strip_flow_envelope("FlowEnvelope<TenantRecord>"),
Some("TenantRecord".to_string())
);
}
#[test]
fn fase39d_strip_flow_envelope_list() {
assert_eq!(
strip_flow_envelope("FlowEnvelope<List<TenantRecord>>"),
Some("List<TenantRecord>".to_string())
);
}
#[test]
fn fase39d_strip_flow_envelope_returns_none_on_bare() {
assert_eq!(strip_flow_envelope("TenantRecord"), None);
assert_eq!(strip_flow_envelope("List<X>"), None);
assert_eq!(strip_flow_envelope(""), None);
}
#[test]
fn fase39d_validate_body_unwraps_flow_envelope_with_struct() {
let mut table: HashMap<String, TypeSchema> = HashMap::new();
table.insert("Person".to_string(), person_schema());
let envelope = serde_json::json!({
"ontological_type": "Person",
"result": {"name": "alice", "age": 30},
"certainty": 1.0,
"provenance_chain": [],
"step_audit": {},
"audit_chain_hash": "",
"blame_attribution": null,
"execution_metrics": {},
"trace_id": "t"
});
let r = validate_body(&envelope, "FlowEnvelope<Person>", &table);
assert!(r.is_ok(), "FlowEnvelope<Person> over a Person body must validate. Got: {r:?}");
}
#[test]
fn fase39d_validate_body_unwraps_flow_envelope_with_list() {
let mut table: HashMap<String, TypeSchema> = HashMap::new();
table.insert("Person".to_string(), person_schema());
let envelope = serde_json::json!({
"ontological_type": "List<Person>",
"result": [
{"name": "alice", "age": 30},
{"name": "bob", "age": 25}
],
"certainty": 1.0,
"provenance_chain": [],
"step_audit": {},
"audit_chain_hash": "",
"blame_attribution": null,
"execution_metrics": {},
"trace_id": "t"
});
let r = validate_body(&envelope, "FlowEnvelope<List<Person>>", &table);
assert!(
r.is_ok(),
"FlowEnvelope<List<Person>> over a Person array result must \
validate. Got: {r:?}"
);
}
#[test]
fn fase39d_validate_body_rejects_flow_envelope_with_wrong_inner_type() {
let mut table: HashMap<String, TypeSchema> = HashMap::new();
table.insert("Person".to_string(), person_schema());
let envelope = serde_json::json!({
"ontological_type": "Person",
"result": {"name": "alice", "age": "thirty"},
"certainty": 1.0,
"provenance_chain": [],
"step_audit": {},
"audit_chain_hash": "",
"blame_attribution": null,
"execution_metrics": {},
"trace_id": "t"
});
let r = validate_body(&envelope, "FlowEnvelope<Person>", &table);
assert!(
r.is_err(),
"Wrong inner-type MUST surface as validation error"
);
let err = r.unwrap_err();
assert_eq!(err.field_path, "age");
}
#[test]
fn fase39d_validate_body_rejects_flow_envelope_with_non_object_body() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let body = serde_json::json!("not an object");
let r = validate_body(&body, "FlowEnvelope<Any>", &table);
assert!(
r.is_err(),
"Non-object body MUST fail FlowEnvelope<T> shape check"
);
let err = r.unwrap_err();
assert!(err.hint.contains("FlowEnvelope"));
}
#[test]
fn fase39d_validate_body_flow_envelope_any_skips_inner_validation() {
let table: HashMap<String, TypeSchema> = HashMap::new();
let envelope = serde_json::json!({
"ontological_type": "Any",
"result": {"anything": "goes"},
"certainty": 1.0,
"provenance_chain": [],
"step_audit": {},
"audit_chain_hash": "",
"blame_attribution": null,
"execution_metrics": {},
"trace_id": "t"
});
let r = validate_body(&envelope, "FlowEnvelope<Any>", &table);
assert!(r.is_ok());
}
#[test]
fn fase39d_validate_body_flow_envelope_with_missing_result_slot() {
let mut table: HashMap<String, TypeSchema> = HashMap::new();
table.insert("Person".to_string(), person_schema());
let envelope = serde_json::json!({
"ontological_type": "Person",
"certainty": 1.0
});
let r = validate_body(&envelope, "FlowEnvelope<Person>", &table);
assert!(
r.is_err(),
"Missing result slot MUST fail when inner type is non-Any"
);
}
#[test]
fn fase39d_validate_value_no_longer_carries_section_0_preamble() {
let src = std::fs::read_to_string("src/route_schema.rs")
.expect("read route_schema.rs");
assert!(
!src.contains("§0 — §Fase 38.x.f.9 (POST-CLOSE HOTFIX 2026-05-21) — generic-\n // aware parsing"),
"§Fase 39.d §S — the v1.40.2/v1.40.3 §0 preamble inside \
validate_value MUST stay retired. Generic parsing belongs \
at the canonical validate_body entry now."
);
}
#[test]
fn fase39d_d5_gate_simplified_calls_validate_body_directly() {
let src = std::fs::read_to_string("src/axon_server.rs")
.expect("read axon_server.rs");
let active_extract_calls = src.matches(
"crate::wire_envelope::extract_inner_ontological_type(&route.output_type)"
).count();
assert!(
active_extract_calls <= 1,
"§Fase 39.d §S — the D5 gate MUST NOT manually call \
`extract_inner_ontological_type` for unwrapping (that work \
moved into validate_body). Found {active_extract_calls} \
active references."
);
}
}