use std::path::Path;
use std::time::Instant;
use serde::Serialize;
use crate::ontology::sigma::SigmaError;
use crate::ontology::verdict::Reason;
use crate::ontology::witness::{
census_id_set_sha256, check, pc_checker, relations_sha256, unencoded_edges, witness_path,
Checked, ClauseSet, Witness, WitnessResult, FIRED,
};
use super::finding::LintFinding;
use super::relations_gate::{typed_graph, TypedGraph};
use super::rules::RuleSeverity;
use super::{GateDetail, GateExtra, GateResult, Verdict};
pub const GATE: &str = "ont-consistency";
#[derive(Debug)]
pub enum ConsistencyOutcome {
NoSigma,
Malformed(SigmaError),
NoCheckable { contracts_checked: usize },
PositiveControlFailed { which: &'static str, why: String },
WitnessStale {
path: String,
why: String,
census_id_set_sha256: String,
relations_sha256: String,
},
Ran {
result: Box<GateResult>,
findings: Vec<LintFinding>,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct WitnessReport {
pub path: String,
pub kind: String,
pub pc_reasoner: String,
pub stale: bool,
pub census_id_set_sha256: String,
pub relations_sha256: String,
pub reasoner_git_sha: Option<String>,
pub cpu_ms: u64,
}
#[must_use]
pub fn run_consistency_gate(contract_dir: &Path) -> ConsistencyOutcome {
let start = Instant::now();
let pc = match pc_checker() {
Ok(fired) => fired,
Err(why) => {
return ConsistencyOutcome::PositiveControlFailed {
which: "pc_checker",
why,
}
}
};
let (ids, edges) = match typed_graph(contract_dir) {
TypedGraph::NoSigma => return ConsistencyOutcome::NoSigma,
TypedGraph::Malformed(e) => return ConsistencyOutcome::Malformed(e),
TypedGraph::Read { ids, edges } => (ids, edges),
};
let cs = ClauseSet::from_graph(&ids, &edges);
let checkable_n = cs.checkable_n();
if checkable_n == 0 {
return ConsistencyOutcome::NoCheckable {
contracts_checked: ids.len(),
};
}
let census_sha = census_id_set_sha256(&ids);
let relations_sha = relations_sha256(&edges);
let path = witness_path(contract_dir, &relations_sha);
let shown = format!("witness/{relations_sha}.json");
let stale = |why: String| ConsistencyOutcome::WitnessStale {
path: shown.clone(),
why,
census_id_set_sha256: census_sha.clone(),
relations_sha256: relations_sha.clone(),
};
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => return stale(format!("{shown}: {e}")),
};
let witness: Witness = match serde_json::from_str(&text) {
Ok(w) => w,
Err(e) => return stale(format!("{shown} does not parse as a witness: {e}")),
};
if witness.relations_sha256 != relations_sha || witness.census_id_set_sha256 != census_sha {
return stale(format!(
"{shown} records census {} / relations {}, and the corpus is census {census_sha} / relations {relations_sha}",
witness.census_id_set_sha256, witness.relations_sha256
));
}
if witness.pc_reasoner != FIRED {
return ConsistencyOutcome::PositiveControlFailed {
which: "pc_reasoner",
why: format!("{shown} records pc_reasoner `{}`", witness.pc_reasoner),
};
}
let (findings, core) = judge(&cs, &witness, &shown);
let passed = findings.is_empty();
let report = WitnessReport {
path: shown,
kind: match witness.result {
WitnessResult::UnsatCore(_) => "unsat_core".into(),
WitnessResult::Model(_) => "model".into(),
},
pc_reasoner: witness.pc_reasoner.clone(),
stale: false,
census_id_set_sha256: census_sha,
relations_sha256: relations_sha,
reasoner_git_sha: witness.reasoner_git_sha.clone(),
cpu_ms: witness.cpu_ms,
};
let result = GateResult {
name: GATE.into(),
passed,
skipped: false,
verdict: Verdict::from_gate(passed, false),
duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
detail: GateDetail::Validate {
contracts: ids.len(),
errors: findings.len(),
warnings: 0,
error_messages: findings.iter().map(|f| f.message.clone()).collect(),
},
extra: Some(GateExtra::Consistency {
checkable_n,
units: cs.units.len(),
unencoded_edges: unencoded_edges(&edges),
pc_checker: pc.to_string(),
core,
violations: findings.len(),
witness: report,
}),
};
ConsistencyOutcome::Ran {
result: Box::new(result),
findings,
}
}
fn judge(cs: &ClauseSet, witness: &Witness, shown: &str) -> (Vec<LintFinding>, Vec<String>) {
let lie = |why: String| {
LintFinding::new(
"PV-ONT-023",
RuleSeverity::Error,
format!("{shown} does not check against the current graph: {why}. Re-run `make contracts`; never edit a witness by hand"),
format!("contracts/{shown}"),
)
};
if witness.checkable_n != cs.checkable_n() {
return (
vec![lie(format!(
"it records checkable_n {} and the graph has {}",
witness.checkable_n,
cs.checkable_n()
))],
Vec::new(),
);
}
match check(cs, &witness.result) {
Ok(Checked::Sat) => (Vec::new(), Vec::new()),
Ok(Checked::Unsat { core }) => {
let core: Vec<String> = core.into_iter().collect();
let (a, b) = match &witness.result {
WitnessResult::UnsatCore(c) => c.conflict.clone(),
WitnessResult::Model(_) => {
return (vec![lie("a model checked as a core".into())], Vec::new())
}
};
let finding = LintFinding::new(
"PV-ONT-022",
RuleSeverity::Error,
format!(
"the typed relations are inconsistent: `{a}` contradicts `{b}`, and the core [{}] puts both in force",
core.join(", ")
),
format!("contracts/{a}.yaml"),
);
(vec![finding], core)
}
Err(e) => (vec![lie(e.to_string())], Vec::new()),
}
}
#[must_use]
pub fn decline_reason(outcome: &ConsistencyOutcome) -> Option<Reason> {
match outcome {
ConsistencyOutcome::NoSigma | ConsistencyOutcome::NoCheckable { .. } => {
Some(Reason::NoCheckable)
}
ConsistencyOutcome::PositiveControlFailed { .. } => Some(Reason::PositiveControlFailed),
ConsistencyOutcome::WitnessStale { .. } => Some(Reason::WitnessStale),
ConsistencyOutcome::Malformed(_) | ConsistencyOutcome::Ran { .. } => None,
}
}
#[must_use]
pub fn why(outcome: &ConsistencyOutcome) -> String {
match outcome {
ConsistencyOutcome::NoSigma => "no contracts/ontology.yaml".into(),
ConsistencyOutcome::Malformed(e) => format!("Σ is malformed: {e}"),
ConsistencyOutcome::NoCheckable { contracts_checked } => format!(
"no typed relation clause in {contracts_checked} contracts — R-2: zero is a decline"
),
ConsistencyOutcome::PositiveControlFailed { which, why } => {
format!("positive control {which} did not fire: {why}")
}
ConsistencyOutcome::WitnessStale { why, .. } => {
format!(
"the witness is stale: {why}. Run `make contracts` to regenerate it with pv-sat"
)
}
ConsistencyOutcome::Ran { .. } => String::new(),
}
}
#[cfg(test)]
#[path = "consistency_gate_tests.rs"]
mod tests;