use std::collections::HashSet;
use crate::error::{Severity, Violation};
use crate::schema::types::{Contract, ContractKind};
pub fn validate_contract(contract: &Contract) -> Vec<Violation> {
let mut violations = Vec::new();
validate_metadata(contract, &mut violations);
if contract.kind() == ContractKind::Kernel && !contract.is_registry() {
validate_equations(contract, &mut violations);
validate_provability_invariant(contract, &mut violations);
validate_proof_obligations(contract, &mut violations);
validate_falsification_tests(contract, &mut violations);
validate_kani_harnesses(contract, &mut violations);
validate_qa_gate(contract, &mut violations);
} else {
validate_proof_obligations(contract, &mut violations);
validate_falsification_tests(contract, &mut violations);
validate_kani_harnesses(contract, &mut violations);
}
if contract.kind() == ContractKind::BeatBenchmark {
validate_beat_benchmark(contract, &mut violations);
}
violations
}
const BEAT_INCUMBENTS: [&str; 5] = ["scikit-learn", "pytorch", "unsloth", "ollama", "llama.cpp"];
fn validate_beat_benchmark(contract: &Contract, violations: &mut Vec<Violation>) {
let push = |violations: &mut Vec<Violation>, rule: &str, message: String, field: &str| {
violations.push(Violation {
severity: Severity::Error,
rule: rule.to_string(),
message,
location: Some(format!("beat.{field}")),
});
};
let Some(beat) = contract.beat.as_ref() else {
violations.push(Violation {
severity: Severity::Error,
rule: "BEAT-001".to_string(),
message: "beat-benchmark contract must define a `beat:` block \
(incumbent, metric, direction, beat_threshold, ci_gate_name)"
.to_string(),
location: Some("beat".to_string()),
});
return;
};
let incumbent = beat.incumbent.trim().to_lowercase();
if incumbent.is_empty() {
push(
violations,
"BEAT-002",
"beat.incumbent must not be empty".to_string(),
"incumbent",
);
} else if !BEAT_INCUMBENTS.iter().any(|p| incumbent.contains(p)) {
push(
violations,
"BEAT-002",
format!(
"beat.incumbent {:?} must name one of the four pillars ({})",
beat.incumbent,
BEAT_INCUMBENTS.join(", ")
),
"incumbent",
);
}
if beat.metric.trim().is_empty() {
push(
violations,
"BEAT-003",
"beat.metric must name the measured quantity (e.g. accuracy, wall_clock_ms, \
tokens_per_sec)"
.to_string(),
"metric",
);
}
match beat.direction.trim() {
"higher_is_better" | "lower_is_better" => {}
other => push(
violations,
"BEAT-004",
format!(
"beat.direction must be `higher_is_better` or `lower_is_better`, got {other:?}"
),
"direction",
),
}
match beat.beat_threshold {
None => push(
violations,
"BEAT-005",
"beat.beat_threshold is required — the pinned value CI fails below".to_string(),
"beat_threshold",
),
Some(t) if !t.is_finite() => push(
violations,
"BEAT-005",
format!("beat.beat_threshold must be finite, got {t}"),
"beat_threshold",
),
Some(_) => {}
}
if beat.ci_gate_name.trim().is_empty() {
push(
violations,
"BEAT-006",
"beat.ci_gate_name must name the CI test that enforces this gate".to_string(),
"ci_gate_name",
);
}
match beat
.approved_compute
.as_deref()
.map(|c| c.trim().to_uppercase())
{
None => push(
violations,
"BEAT-007",
"beat.approved_compute is required — must be `CPU` or `GPU`".to_string(),
"approved_compute",
),
Some(ref c) if c != "CPU" && c != "GPU" => push(
violations,
"BEAT-007",
format!(
"beat.approved_compute must be `CPU` or `GPU`, got {:?}",
beat.approved_compute
),
"approved_compute",
),
Some(_) => {}
}
}
fn validate_provability_invariant(contract: &Contract, violations: &mut Vec<Violation>) {
for v in contract.provability_violations() {
violations.push(Violation {
severity: Severity::Error,
rule: "PROVABILITY-001".to_string(),
message: v,
location: None,
});
}
}
fn validate_metadata(contract: &Contract, violations: &mut Vec<Violation>) {
if contract.metadata.references.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-001".to_string(),
message: "metadata.references must not be empty — \
every contract must cite its source paper(s)"
.to_string(),
location: Some("metadata.references".to_string()),
});
}
if contract.metadata.version.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-002".to_string(),
message: "metadata.version must not be empty".to_string(),
location: Some("metadata.version".to_string()),
});
}
}
fn validate_equations(contract: &Contract, violations: &mut Vec<Violation>) {
if contract.equations.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-003".to_string(),
message: "equations must contain at least one equation".to_string(),
location: Some("equations".to_string()),
});
}
for (name, eq) in &contract.equations {
if eq.formula.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-004".to_string(),
message: format!("equations.{name}.formula must not be empty"),
location: Some(format!("equations.{name}.formula")),
});
}
}
}
fn validate_proof_obligations(contract: &Contract, violations: &mut Vec<Violation>) {
use crate::schema::types::ObligationType;
let mut seen_ids = HashSet::new();
for (i, ob) in contract.proof_obligations.iter().enumerate() {
if ob.property.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-005".to_string(),
message: format!("proof_obligations[{i}].property must not be empty"),
location: Some(format!("proof_obligations[{i}].property")),
});
}
if let Some(ref formal) = ob.formal {
if !seen_ids.insert(formal.clone()) {
violations.push(Violation {
severity: Severity::Warning,
rule: "SCHEMA-006".to_string(),
message: format!("Duplicate formal predicate: {formal}"),
location: Some(format!("proof_obligations[{i}].formal")),
});
}
}
if ob.requires.is_some() && ob.obligation_type != ObligationType::Postcondition {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-014".to_string(),
message: format!(
"proof_obligations[{i}].requires is only valid on \
postcondition obligations (found on {})",
ob.obligation_type
),
location: Some(format!("proof_obligations[{i}].requires")),
});
}
if ob.applies_to_phase.is_some()
&& ob.obligation_type != ObligationType::LoopInvariant
&& ob.obligation_type != ObligationType::LoopVariant
{
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-015".to_string(),
message: format!(
"proof_obligations[{i}].applies_to_phase is only valid on \
loop_invariant or loop_variant obligations (found on {})",
ob.obligation_type
),
location: Some(format!("proof_obligations[{i}].applies_to_phase")),
});
}
if ob.parent_contract.is_some() && ob.obligation_type != ObligationType::Subcontract {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-016".to_string(),
message: format!(
"proof_obligations[{i}].parent_contract is only valid on \
subcontract obligations (found on {})",
ob.obligation_type
),
location: Some(format!("proof_obligations[{i}].parent_contract")),
});
}
if let Some(ref parent) = ob.parent_contract {
if ob.obligation_type == ObligationType::Subcontract
&& !contract.metadata.depends_on.contains(parent)
{
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-017".to_string(),
message: format!(
"proof_obligations[{i}].parent_contract \"{parent}\" \
must be listed in metadata.depends_on"
),
location: Some(format!("proof_obligations[{i}].parent_contract")),
});
}
}
}
}
fn validate_falsification_tests(contract: &Contract, violations: &mut Vec<Violation>) {
let mut ids = HashSet::new();
for test in &contract.falsification_tests {
if !ids.insert(&test.id) {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-007".to_string(),
message: format!("Duplicate falsification test ID: {}", test.id),
location: Some(format!("falsification_tests.{}", test.id)),
});
}
if test.prediction.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-008".to_string(),
message: format!(
"falsification_tests.{}.prediction must not be empty — \
every test must make a falsifiable prediction",
test.id
),
location: Some(format!("falsification_tests.{}.prediction", test.id)),
});
}
if test.if_fails.is_empty() {
violations.push(Violation {
severity: Severity::Warning,
rule: "SCHEMA-009".to_string(),
message: format!(
"falsification_tests.{}.if_fails is empty — \
should describe root cause diagnosis",
test.id
),
location: Some(format!("falsification_tests.{}.if_fails", test.id)),
});
}
}
}
fn validate_kani_harnesses(contract: &Contract, violations: &mut Vec<Violation>) {
let mut ids = HashSet::new();
for harness in &contract.kani_harnesses {
if !ids.insert(&harness.id) {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-010".to_string(),
message: format!("Duplicate Kani harness ID: {}", harness.id),
location: Some(format!("kani_harnesses.{}", harness.id)),
});
}
if harness.obligation.is_empty() {
violations.push(Violation {
severity: Severity::Error,
rule: "SCHEMA-011".to_string(),
message: format!(
"kani_harnesses.{}.obligation must not be empty — \
every harness must reference a proof obligation",
harness.id
),
location: Some(format!("kani_harnesses.{}.obligation", harness.id)),
});
}
if harness.bound.is_none() {
violations.push(Violation {
severity: Severity::Warning,
rule: "SCHEMA-012".to_string(),
message: format!(
"kani_harnesses.{}.bound not specified — \
Kani requires an unwind bound",
harness.id
),
location: Some(format!("kani_harnesses.{}.bound", harness.id)),
});
}
}
}
fn validate_qa_gate(contract: &Contract, violations: &mut Vec<Violation>) {
if contract.qa_gate.is_none() {
violations.push(Violation {
severity: Severity::Warning,
rule: "SCHEMA-013".to_string(),
message: "No qa_gate defined — contract should define a \
certeza quality gate"
.to_string(),
location: Some("qa_gate".to_string()),
});
}
}
#[cfg(test)]
mod tests {
include!("validator_tests.rs");
}