use std::collections::HashSet;
use std::path::Path;
use std::time::Instant;
use crate::schema::Contract;
use crate::schema::EnforcementLevel;
use super::finding::LintFinding;
use super::rules::RuleSeverity;
use super::{GateDetail, GateResult};
#[allow(clippy::too_many_lines)]
pub(crate) fn run_verify_gate(
contracts: &[(String, Contract)],
project_root: &Path,
) -> (GateResult, Vec<LintFinding>) {
let start = Instant::now();
let mut findings = Vec::new();
let mut total_refs = 0usize;
let mut missing = 0usize;
let mut source_tests = HashSet::new();
for sub in &["src", "crates", "generated", "tests"] {
let d = project_root.join(sub);
if d.exists() {
collect_test_fns(&d, &mut source_tests);
}
}
let effective_root = if project_root.as_os_str().is_empty() {
Path::new(".")
} else {
project_root
};
let root_canon = effective_root
.canonicalize()
.unwrap_or_else(|_| effective_root.to_path_buf());
if let Ok(entries) = std::fs::read_dir(&root_canon) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() && path.join("Cargo.toml").exists() {
let name = path.file_name().unwrap_or_default();
if name == "src" || name == "crates" || name == "tests" {
continue;
}
for sub in &["src", "tests"] {
let d = path.join(sub);
if d.exists() {
collect_test_fns(&d, &mut source_tests);
}
}
}
}
}
if let Some(parent) = project_root.parent() {
for (stem, _) in contracts {
if let Some(repo) = stem.split('/').next() {
for sub in &["src", "crates"] {
let d = parent.join(repo).join(sub);
if d.exists() {
collect_test_fns(&d, &mut source_tests);
}
}
}
}
}
for (stem, contract) in contracts {
for ft in &contract.falsification_tests {
if let Some(ref test_name) = ft.test {
let raw = test_name.trim().trim_matches('"');
let name = raw.rsplit("::").next().unwrap_or(raw);
if name.starts_with("test_") || name.starts_with("prop_") {
total_refs += 1;
if !source_tests.contains(name) {
missing += 1;
let is_gpu = name.contains("gpu")
|| name.contains("wgpu")
|| name.contains("cuda")
|| stem.contains("wgpu")
|| stem.contains("gpu");
let sev = if is_gpu {
RuleSeverity::Warning
} else {
RuleSeverity::Error
};
let note = if is_gpu {
" (may require --features gpu)"
} else {
""
};
let mut f = LintFinding::new(
"PV-VER-001",
sev,
format!("Unfalsifiable: test `{name}` not found in src/{note}"),
format!("contracts/{stem}.yaml"),
);
f.contract_stem = Some(stem.clone());
findings.push(f);
}
}
}
}
}
let error_count = findings
.iter()
.filter(|f| f.rule_id == "PV-VER-001" && f.severity == RuleSeverity::Error)
.count();
let passed = error_count == 0;
let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
(
GateResult {
name: "verify".into(),
passed,
skipped: false,
duration_ms: duration,
detail: GateDetail::Verify {
total_refs,
existing: total_refs - missing,
missing,
},
},
findings,
)
}
fn collect_test_fns(dir: &Path, tests: &mut HashSet<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_test_fns(&path, tests);
} else if path.extension().is_some_and(|e| e == "rs") {
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
if let Some(pos) = line.find("fn test_").or_else(|| line.find("fn prop_")) {
let rest = &line[pos + 3..];
let name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
tests.insert(name);
}
}
}
}
}
}
}
pub(crate) fn run_enforce_gate(contracts: &[(String, Contract)]) -> (GateResult, Vec<LintFinding>) {
let start = Instant::now();
let mut findings = Vec::new();
let mut total_eqs = 0usize;
let mut with_pre = 0usize;
let mut with_post = 0usize;
let mut with_lean = 0usize;
for (stem, contract) in contracts {
if !contract.requires_proofs() {
continue; }
for (eq_name, eq) in &contract.equations {
total_eqs += 1;
if eq.preconditions.is_empty() {
findings.push(LintFinding {
rule_id: "PV-ENF-001".into(),
severity: RuleSeverity::Warning,
message: format!("Equation `{eq_name}` has no preconditions"),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: Some(format!(
"Add to equations.{eq_name}:\n preconditions:\n - \"!input.is_empty()\""
)),
evidence: None,
});
} else {
with_pre += 1;
}
if eq.postconditions.is_empty() {
findings.push(LintFinding {
rule_id: "PV-ENF-001".into(),
severity: RuleSeverity::Warning,
message: format!("Equation `{eq_name}` has no postconditions"),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: Some(format!(
"Add to equations.{eq_name}:\n postconditions:\n - \"result.len() > 0\""
)),
evidence: None,
});
} else {
with_post += 1;
}
if eq.lean_theorem.is_some() {
with_lean += 1;
} else {
let lean_name = capitalize_first(eq_name);
findings.push(LintFinding {
rule_id: "PV-ENF-002".into(),
severity: RuleSeverity::Warning,
message: format!(
"Equation `{eq_name}` has no lean_theorem — proof recommended"
),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: Some(format!(
"Add to equations.{eq_name}:\n lean_theorem: \"Theorems.{lean_name}\""
)),
evidence: None,
});
}
}
}
let has_errors = findings.iter().any(|f| f.severity == RuleSeverity::Error);
let passed = !has_errors || total_eqs == 0;
let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
(
GateResult {
name: "enforce".into(),
passed,
skipped: false,
duration_ms: duration,
detail: GateDetail::Enforce {
equations_total: total_eqs,
equations_with_pre: with_pre,
equations_with_post: with_post,
equations_with_lean: with_lean,
},
},
findings,
)
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub(crate) fn run_enforcement_level_gate(
contracts: &[(String, Contract)],
min_level: EnforcementLevel,
) -> (GateResult, Vec<LintFinding>) {
let start = Instant::now();
let mut findings = Vec::new();
let mut below = 0usize;
for (stem, contract) in contracts {
if !contract.requires_proofs() {
continue; }
let declared = contract
.metadata
.enforcement_level
.unwrap_or(EnforcementLevel::Standard);
let actual = compute_actual_level(contract);
if actual < declared {
findings.push(LintFinding {
rule_id: "PV-ENF-001".into(),
severity: RuleSeverity::Warning,
message: format!(
"Contract `{stem}` declares enforcement_level={declared:?} but only achieves {actual:?}"
),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
below += 1;
}
if actual < min_level {
findings.push(LintFinding {
rule_id: "PV-ENF-001".into(),
severity: RuleSeverity::Warning,
message: format!(
"Contract `{stem}` at level {actual:?}, below required {min_level:?}"
),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
below += 1;
}
if let Some(ref locked) = contract.metadata.locked_level {
let locked_level = match locked.to_lowercase().as_str() {
"basic" | "l1" => EnforcementLevel::Basic,
"strict" | "l4" => EnforcementLevel::Strict,
"proven" | "l5" => EnforcementLevel::Proven,
_ => EnforcementLevel::Standard,
};
if actual < locked_level {
findings.push(LintFinding {
rule_id: "PV-LCK-001".into(),
severity: RuleSeverity::Error,
message: format!(
"Contract `{stem}` locked at {locked} but regressed to {actual:?}. Use `pv unlock` to release."
),
file: format!("contracts/{stem}.yaml"),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
}
}
}
let has_errors = findings.iter().any(|f| f.severity == RuleSeverity::Error);
let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
(
GateResult {
name: "enforcement-level".into(),
passed: !has_errors,
skipped: false,
duration_ms: duration,
detail: GateDetail::Skipped {
reason: format!("{} contracts, {} below level", contracts.len(), below),
},
},
findings,
)
}
fn compute_actual_level(contract: &Contract) -> EnforcementLevel {
let has_falsification = !contract.falsification_tests.is_empty();
let has_kani = !contract.kani_harnesses.is_empty();
let has_lean = contract
.verification_summary
.as_ref()
.is_some_and(|v| v.l4_lean_proved > 0 && v.l4_sorry_count == 0);
if has_lean {
EnforcementLevel::Proven
} else if has_falsification && has_kani {
EnforcementLevel::Standard
} else {
EnforcementLevel::Basic
}
}
pub(crate) fn run_reverse_coverage_gate(
binding_path: &Path,
crate_dir: &Path,
) -> (GateResult, Vec<LintFinding>) {
const DEFAULT_THRESHOLD: f64 = 50.0;
let start = Instant::now();
let mut findings = Vec::new();
let report = crate::reverse_coverage::reverse_coverage(crate_dir, binding_path);
for uf in &report.unbound {
findings.push(LintFinding {
rule_id: "PV-RCV-001".into(),
severity: RuleSeverity::Warning,
message: format!(
"Public function `{}` has no contract binding ({}:{})",
uf.path, uf.file, uf.line,
),
file: uf.file.clone(),
line: Some(u32::try_from(uf.line).unwrap_or(0)),
contract_stem: None,
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
}
let passed = report.coverage_pct >= DEFAULT_THRESHOLD;
if !passed {
findings.push(LintFinding {
rule_id: "PV-RCV-002".into(),
severity: RuleSeverity::Error,
message: format!(
"Reverse coverage {:.1}% is below threshold {DEFAULT_THRESHOLD:.1}%",
report.coverage_pct,
),
file: String::new(),
line: None,
contract_stem: None,
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
}
let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
(
GateResult {
name: "reverse-coverage".into(),
passed,
skipped: false,
duration_ms: duration,
detail: GateDetail::ReverseCoverage {
total_pub_fns: report.total_pub_fns,
bound_fns: report.bound_fns,
unbound_fns: report.unbound.len(),
coverage_pct: report.coverage_pct,
threshold_pct: DEFAULT_THRESHOLD,
},
},
findings,
)
}
#[cfg(test)]
#[allow(clippy::all)]
#[path = "gates_extended_tests.rs"]
mod tests;
pub(crate) fn check_stale_suppressions(
findings: &[LintFinding],
suppressed_rules: &[String],
suppressed_findings: &[String],
) -> Vec<LintFinding> {
let mut stale = Vec::new();
let active_rules: HashSet<&str> = findings.iter().map(|f| f.rule_id.as_str()).collect();
for rule in suppressed_rules {
if !active_rules.contains(rule.as_str()) {
stale.push(LintFinding {
rule_id: "PV-SUP-001".into(),
severity: RuleSeverity::Warning,
message: format!(
"Suppression for rule `{rule}` is stale — the rule no longer fires. Remove the suppression."
),
file: String::new(),
line: None,
contract_stem: None,
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
}
}
let active_stems: HashSet<&str> = findings
.iter()
.filter_map(|f| f.contract_stem.as_deref())
.collect();
for stem in suppressed_findings {
if !active_stems.contains(stem.as_str()) {
stale.push(LintFinding {
rule_id: "PV-SUP-001".into(),
severity: RuleSeverity::Warning,
message: format!(
"Suppression for `{stem}` is stale — no findings exist for this contract. Remove the suppression."
),
file: String::new(),
line: None,
contract_stem: Some(stem.clone()),
suppressed: false,
suppression_reason: None,
is_new: false,
snippet: None,
suggestion: None,
evidence: None,
});
}
}
stale
}