use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ContractFile {
pub contract: ContractMeta,
pub invariants: Vec<Invariant>,
pub verification: VerificationTargets,
}
#[derive(Debug, Deserialize)]
pub struct ContractMeta {
pub name: String,
pub version: String,
pub module: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct Invariant {
pub id: String,
pub name: String,
pub description: String,
pub preconditions: Vec<String>,
pub postconditions: Vec<String>,
pub equation: String,
pub module_path: String,
pub test_binding: String,
}
#[derive(Debug, Deserialize)]
pub struct VerificationTargets {
pub unit_tests: Vec<String>,
pub coverage_target: u32,
pub mutation_target: u32,
pub complexity_max_cyclomatic: u32,
pub complexity_max_cognitive: u32,
}
#[derive(Debug)]
pub struct VerificationResult {
pub contract_name: String,
pub total_invariants: usize,
pub verified_bindings: usize,
pub missing_bindings: Vec<String>,
pub invariant_status: HashMap<String, InvariantStatus>,
}
#[derive(Debug, Clone)]
pub struct InvariantStatus {
pub id: String,
pub name: String,
pub test_found: bool,
pub test_binding: String,
}
impl VerificationResult {
#[must_use]
pub fn all_verified(&self) -> bool {
self.missing_bindings.is_empty()
}
#[must_use]
pub fn report(&self) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(
out,
"Contract: {} ({}/{})",
self.contract_name, self.verified_bindings, self.total_invariants,
);
for status in self.invariant_status.values() {
let mark = if status.test_found { "✓" } else { "✗" };
let _ = writeln!(out, " [{mark}] {} — {}", status.id, status.name,);
}
if !self.missing_bindings.is_empty() {
let _ = writeln!(out, "\nMissing bindings:");
for b in &self.missing_bindings {
let _ = writeln!(out, " - {b}");
}
}
out
}
}
pub fn parse_contract(yaml_content: &str) -> Result<ContractFile, String> {
serde_yaml_ng::from_str(yaml_content).map_err(|e| format!("YAML parse error: {e}"))
}
pub fn verify_bindings(contract: &ContractFile, known_tests: &[String]) -> VerificationResult {
let mut status = HashMap::new();
let mut missing = Vec::new();
let mut verified = 0;
for inv in &contract.invariants {
let found = known_tests.iter().any(|t| t.contains(&inv.test_binding));
if found {
verified += 1;
} else {
missing.push(format!("{}: {} (expected: {})", inv.id, inv.name, inv.test_binding,));
}
status.insert(
inv.id.clone(),
InvariantStatus {
id: inv.id.clone(),
name: inv.name.clone(),
test_found: found,
test_binding: inv.test_binding.clone(),
},
);
}
VerificationResult {
contract_name: contract.contract.name.clone(),
total_invariants: contract.invariants.len(),
verified_bindings: verified,
missing_bindings: missing,
invariant_status: status,
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_YAML: &str = include_str!("../../contracts/agent-loop-v1.yaml");
#[test]
fn test_parse_contract() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
assert_eq!(contract.contract.name, "agent-loop-v1");
assert_eq!(contract.contract.version, "1.0.0");
assert_eq!(contract.contract.module, "batuta::agent");
assert!(!contract.invariants.is_empty());
}
#[test]
fn test_invariant_count() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
assert_eq!(
contract.invariants.len(),
16,
"expected 16 invariants (8 loop + 4 pool/sanitization + 4 tool)"
);
}
#[test]
fn test_invariant_ids() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let ids: Vec<&str> = contract.invariants.iter().map(|i| i.id.as_str()).collect();
assert!(ids.contains(&"INV-001"));
assert!(ids.contains(&"INV-007"));
}
#[test]
fn test_invariant_fields_populated() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
for inv in &contract.invariants {
assert!(!inv.name.is_empty(), "{} has empty name", inv.id);
assert!(!inv.description.is_empty(), "{} has empty description", inv.id);
assert!(!inv.preconditions.is_empty(), "{} has no preconditions", inv.id);
assert!(!inv.postconditions.is_empty(), "{} has no postconditions", inv.id);
assert!(!inv.equation.is_empty(), "{} has empty equation", inv.id);
assert!(!inv.module_path.is_empty(), "{} has empty module_path", inv.id);
assert!(!inv.test_binding.is_empty(), "{} has empty test_binding", inv.id);
}
}
#[test]
fn test_verification_targets() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
assert_eq!(contract.verification.coverage_target, 95);
assert_eq!(contract.verification.mutation_target, 80);
assert_eq!(contract.verification.complexity_max_cyclomatic, 30);
assert_eq!(contract.verification.complexity_max_cognitive, 25);
assert!(!contract.verification.unit_tests.is_empty());
}
#[test]
fn test_verify_all_bindings_found() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let known_tests: Vec<String> =
contract.invariants.iter().map(|i| i.test_binding.clone()).collect();
let result = verify_bindings(&contract, &known_tests);
assert!(result.all_verified());
assert_eq!(result.verified_bindings, contract.invariants.len());
}
#[test]
fn test_verify_missing_binding() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let result = verify_bindings(&contract, &[]);
assert!(!result.all_verified());
assert_eq!(result.verified_bindings, 0);
assert_eq!(result.missing_bindings.len(), contract.invariants.len());
}
#[test]
fn test_verify_partial_bindings() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let known_tests = vec![contract.invariants[0].test_binding.clone()];
let result = verify_bindings(&contract, &known_tests);
assert!(!result.all_verified());
assert_eq!(result.verified_bindings, 1);
}
#[test]
fn test_report_format() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let result = verify_bindings(&contract, &[]);
let report = result.report();
assert!(report.contains("agent-loop-v1"));
assert!(report.contains("Missing bindings"));
}
#[test]
fn test_report_all_pass() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let known_tests: Vec<String> =
contract.invariants.iter().map(|i| i.test_binding.clone()).collect();
let result = verify_bindings(&contract, &known_tests);
let report = result.report();
assert!(!report.contains("Missing bindings"));
}
#[test]
fn test_contract_equations_have_code_bindings() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let bound_equations = [
"loop_termination", "capability_match", "guard_budget", ];
for inv in &contract.invariants {
let eq_lines: Vec<&str> =
inv.equation.lines().map(str::trim).filter(|l| !l.is_empty()).collect();
assert!(!eq_lines.is_empty(), "{}: empty equation", inv.id);
}
let bound_count = contract
.invariants
.iter()
.filter(|inv| {
bound_equations.iter().any(|eq| {
inv.equation.contains(eq)
|| inv.module_path.contains("record_cost")
|| inv.module_path.contains("run_agent_loop")
|| inv.module_path.contains("capability_matches")
})
})
.count();
assert!(bound_count >= 3, "expected >= 3 #[contract] bindings, got {bound_count}");
}
#[test]
fn test_all_contract_bindings_exist() {
let contract = parse_contract(TEST_YAML).expect("parse failed");
let existing_tests = [
"agent::guard::tests::test_iteration_limit",
"agent::guard::tests::test_counters",
"agent::runtime::tests::test_capability_denied_handled",
"agent::guard::tests::test_pingpong_detection",
"agent::guard::tests::test_cost_budget",
"agent::guard::tests::test_consecutive_max_tokens",
"agent::runtime::tests::test_conversation_stored_in_memory",
"agent::pool::tests::test_pool_capacity_limit",
"agent::pool::tests::test_pool_fan_out_fan_in",
"agent::pool::tests::test_pool_join_all",
"agent::tool::tests::test_sanitize_output_system_injection",
"agent::tool::spawn::tests::test_spawn_tool_depth_limit",
"agent::tool::network::tests::test_blocked_host",
"agent::tool::inference::tests::test_inference_tool_timeout",
"agent::runtime::tests_advanced::test_sovereign_privacy_blocks_network",
"agent::guard::tests::test_token_budget_exhausted",
];
let known: Vec<String> = existing_tests.iter().map(|s| (*s).to_string()).collect();
let result = verify_bindings(&contract, &known);
assert!(result.all_verified(), "Missing bindings:\n{}", result.report());
}
}