use crate::{
ArtifactLayout, DryRunner, LeanConfig, LeanManifestReportFiles, LocalProcessRunner,
ObligationBundle, SmtConfig, VerificationReport, VerifierRunner, dry_run_bundle_artifacts,
dry_run_report, execute_report, write_bundle_artifacts,
};
#[cfg(feature = "amari")]
use crate::{StatisticalVerification, ThreeTierVerificationReport, three_tier_report};
use std::{fs, io, path::Path, string::String};
pub const VERIFICATION_SIDECAR_SCHEMA_VERSION: &str = "1";
pub const DEFAULT_REPORT_STEM: &str = "verification-report";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReportFiles {
pub json_path: String,
pub markdown_path: String,
pub lean_diagnostics_json_path: Option<String>,
pub lean_manifest_path: Option<String>,
pub three_tier_json_path: Option<String>,
pub three_tier_markdown_path: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VerificationSession {
bundle: ObligationBundle,
layout: ArtifactLayout,
lean_module_name: String,
smt: SmtConfig,
lean: LeanConfig,
report_stem: String,
#[cfg(feature = "amari")]
statistical_verifications: Vec<StatisticalVerification>,
}
impl VerificationSession {
pub fn new(
bundle: ObligationBundle,
layout: ArtifactLayout,
lean_module_name: impl Into<String>,
) -> Self {
Self {
bundle,
layout,
lean_module_name: lean_module_name.into(),
smt: SmtConfig::default(),
lean: LeanConfig::default(),
report_stem: DEFAULT_REPORT_STEM.into(),
#[cfg(feature = "amari")]
statistical_verifications: Vec::new(),
}
}
pub fn with_smt_config(mut self, smt: SmtConfig) -> Self {
self.smt = smt;
self
}
pub fn with_lean_config(mut self, lean: LeanConfig) -> Self {
self.lean = lean;
self
}
pub fn with_report_stem(mut self, report_stem: impl Into<String>) -> Self {
self.report_stem = report_stem.into();
self
}
#[cfg(feature = "amari")]
pub fn with_statistical_verification(mut self, verification: StatisticalVerification) -> Self {
self.statistical_verifications.push(verification);
self
}
#[cfg(feature = "amari")]
pub fn with_statistical_verifications(
mut self,
verifications: impl IntoIterator<Item = StatisticalVerification>,
) -> Self {
self.statistical_verifications.extend(verifications);
self
}
pub fn bundle(&self) -> &ObligationBundle {
&self.bundle
}
pub fn layout(&self) -> &ArtifactLayout {
&self.layout
}
pub fn lean_module_name(&self) -> &str {
&self.lean_module_name
}
pub fn report_stem(&self) -> &str {
&self.report_stem
}
pub fn dry_run_report(&self) -> VerificationReport {
let artifacts = dry_run_bundle_artifacts(
&self.bundle,
&self.layout,
&self.lean_module_name,
&self.smt,
&self.lean,
);
dry_run_report(&self.bundle, &artifacts)
}
pub fn verify_report(&self, runner: &impl VerifierRunner) -> io::Result<VerificationReport> {
let artifacts = write_bundle_artifacts(
&self.bundle,
&self.layout,
&self.lean_module_name,
&self.smt,
&self.lean,
)?;
Ok(execute_report(&self.bundle, &artifacts, runner))
}
pub fn verify_local_report(&self) -> io::Result<VerificationReport> {
self.verify_report(&LocalProcessRunner)
}
pub fn verify_with_ci_outputs(
&self,
runner: &impl VerifierRunner,
) -> io::Result<VerificationOutput> {
let artifacts = write_bundle_artifacts(
&self.bundle,
&self.layout,
&self.lean_module_name,
&self.smt,
&self.lean,
)?;
let report = execute_report(&self.bundle, &artifacts, runner);
let report_files = write_report_files(
&self.layout.root,
&self.report_stem,
&self.bundle,
&report,
artifacts.lean_manifest.as_ref(),
#[cfg(feature = "amari")]
&self.statistical_verifications,
)?;
Ok(VerificationOutput {
report,
report_files,
})
}
pub fn verify_local_with_ci_outputs(&self) -> io::Result<VerificationOutput> {
self.verify_with_ci_outputs(&LocalProcessRunner)
}
pub fn verify_with_dry_runner(&self) -> io::Result<VerificationReport> {
self.verify_report(&DryRunner)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerificationOutput {
pub report: VerificationReport,
pub report_files: ReportFiles,
}
pub fn verify_bundle(
bundle: &ObligationBundle,
layout: &ArtifactLayout,
lean_module_name: &str,
smt: &SmtConfig,
lean: &LeanConfig,
runner: &impl VerifierRunner,
) -> io::Result<VerificationReport> {
VerificationSession::new(bundle.clone(), layout.clone(), lean_module_name)
.with_smt_config(smt.clone())
.with_lean_config(lean.clone())
.verify_report(runner)
}
pub fn verify_bundle_with_ci_outputs(
bundle: &ObligationBundle,
layout: &ArtifactLayout,
lean_module_name: &str,
smt: &SmtConfig,
lean: &LeanConfig,
runner: &impl VerifierRunner,
) -> io::Result<VerificationOutput> {
VerificationSession::new(bundle.clone(), layout.clone(), lean_module_name)
.with_smt_config(smt.clone())
.with_lean_config(lean.clone())
.verify_with_ci_outputs(runner)
}
fn write_report_files(
root: impl AsRef<Path>,
report_stem: &str,
bundle: &ObligationBundle,
report: &VerificationReport,
lean_manifest: Option<&crate::artifact::LeanManifest>,
#[cfg(feature = "amari")] statistical_verifications: &[StatisticalVerification],
) -> io::Result<ReportFiles> {
#[cfg(not(feature = "amari"))]
let _ = bundle;
let root = root.as_ref();
fs::create_dir_all(root)?;
let lean_manifest_path = report.lean_module.as_ref().map(|module| {
path_to_string(
&root
.join("lean")
.join(format!("{}.manifest.json", module.module_name)),
)
});
let lean_diagnostics_json_path = report
.lean_module
.as_ref()
.map(|_| -> io::Result<String> {
let path = root.join(format!("{report_stem}.lean-diagnostics.json"));
fs::write(&path, render_lean_diagnostics_json(report))?;
Ok(path_to_string(&path))
})
.transpose()?;
#[cfg(feature = "amari")]
let three_tier = (!statistical_verifications.is_empty())
.then(|| three_tier_report(bundle, statistical_verifications, Some(report)));
#[cfg(feature = "amari")]
let three_tier_json_path = if let Some(three_tier) = &three_tier {
let path = root.join(format!("{report_stem}.three-tier.json"));
fs::write(
&path,
render_three_tier_json_with_links(three_tier, report_stem, root),
)?;
Some(path_to_string(&path))
} else {
None
};
#[cfg(feature = "amari")]
let three_tier_markdown_path = if let Some(three_tier) = &three_tier {
let path = root.join(format!("{report_stem}.three-tier.md"));
fs::write(
&path,
render_three_tier_markdown_with_links(three_tier, report_stem, root),
)?;
Some(path_to_string(&path))
} else {
None
};
#[cfg(not(feature = "amari"))]
let three_tier_json_path = None;
#[cfg(not(feature = "amari"))]
let three_tier_markdown_path = None;
let json_path = root.join(format!("{report_stem}.json"));
let markdown_path = root.join(format!("{report_stem}.md"));
let report_files = ReportFiles {
json_path: path_to_string(&json_path),
markdown_path: path_to_string(&markdown_path),
lean_diagnostics_json_path,
lean_manifest_path,
three_tier_json_path,
three_tier_markdown_path,
};
fs::write(
&json_path,
render_report_json_with_links(report, &report_files),
)?;
fs::write(
&markdown_path,
render_report_markdown_with_links(report, &report_files),
)?;
if let (Some(path), Some(manifest)) = (&report_files.lean_manifest_path, lean_manifest) {
write_lean_manifest_with_report_links(path, manifest, &report_files)?;
}
Ok(report_files)
}
fn render_report_json_with_links(report: &VerificationReport, files: &ReportFiles) -> String {
let mut json = report.to_json();
if json.ends_with('}') {
json.pop();
json.push_str(",\"report_files\":{");
json.push_str(&format!(
"\"schema_version\":\"{}\",\"json_path\":\"{}\",\"markdown_path\":\"{}\"",
VERIFICATION_SIDECAR_SCHEMA_VERSION,
escape_json(&files.json_path),
escape_json(&files.markdown_path)
));
if let Some(path) = &files.lean_diagnostics_json_path {
json.push_str(&format!(
",\"lean_diagnostics_json_path\":\"{}\"",
escape_json(path)
));
}
if let Some(path) = &files.lean_manifest_path {
json.push_str(&format!(
",\"lean_manifest_path\":\"{}\"",
escape_json(path)
));
}
if let Some(path) = &files.three_tier_json_path {
json.push_str(&format!(
",\"three_tier_json_path\":\"{}\"",
escape_json(path)
));
}
if let Some(path) = &files.three_tier_markdown_path {
json.push_str(&format!(
",\"three_tier_markdown_path\":\"{}\"",
escape_json(path)
));
}
json.push_str("}}");
}
json
}
fn render_report_markdown_with_links(report: &VerificationReport, files: &ReportFiles) -> String {
let mut markdown = report.to_markdown();
markdown.push_str("\nReport files:\n");
markdown.push_str(&format!(
"- Schema version: `{}`\n",
VERIFICATION_SIDECAR_SCHEMA_VERSION
));
markdown.push_str(&format!("- JSON: `{}`\n", files.json_path));
markdown.push_str(&format!("- Markdown: `{}`\n", files.markdown_path));
if let Some(path) = &files.lean_diagnostics_json_path {
markdown.push_str(&format!("- Lean diagnostics JSON: `{}`\n", path));
}
if let Some(path) = &files.lean_manifest_path {
markdown.push_str(&format!("- Lean manifest: `{}`\n", path));
}
if let Some(path) = &files.three_tier_json_path {
markdown.push_str(&format!("- Three-tier JSON: `{}`\n", path));
}
if let Some(path) = &files.three_tier_markdown_path {
markdown.push_str(&format!("- Three-tier Markdown: `{}`\n", path));
}
markdown
}
#[cfg(feature = "amari")]
fn render_three_tier_json_with_links(
report: &ThreeTierVerificationReport,
report_stem: &str,
root: &Path,
) -> String {
let mut json = report.to_json();
if json.ends_with('}') {
json.pop();
json.push_str(",\"report_files\":{");
json.push_str(&format!(
"\"schema_version\":\"{}\",\"json_path\":\"{}\",\"markdown_path\":\"{}\"",
VERIFICATION_SIDECAR_SCHEMA_VERSION,
escape_json(&path_to_string(
&root.join(format!("{report_stem}.three-tier.json"))
)),
escape_json(&path_to_string(
&root.join(format!("{report_stem}.three-tier.md"))
))
));
json.push_str("}}");
}
json
}
#[cfg(feature = "amari")]
fn render_three_tier_markdown_with_links(
report: &ThreeTierVerificationReport,
report_stem: &str,
root: &Path,
) -> String {
let mut markdown = report.to_markdown();
markdown.push_str("\nReport files:\n");
markdown.push_str(&format!(
"- Schema version: `{}`\n",
VERIFICATION_SIDECAR_SCHEMA_VERSION
));
markdown.push_str(&format!(
"- JSON: `{}`\n",
path_to_string(&root.join(format!("{report_stem}.three-tier.json")))
));
markdown.push_str(&format!(
"- Markdown: `{}`\n",
path_to_string(&root.join(format!("{report_stem}.three-tier.md")))
));
markdown
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn write_lean_manifest_with_report_links(
manifest_path: &str,
manifest: &crate::artifact::LeanManifest,
files: &ReportFiles,
) -> io::Result<()> {
let manifest = manifest.clone().with_report_files({
let report_files =
LeanManifestReportFiles::new(files.json_path.clone(), files.markdown_path.clone());
match &files.lean_diagnostics_json_path {
Some(path) => report_files.with_lean_diagnostics_json_path(path.clone()),
None => report_files,
}
});
fs::write(manifest_path, manifest.to_json())
}
fn render_lean_diagnostics_json(report: &VerificationReport) -> String {
fn esc(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
let module = match &report.lean_module {
Some(module) => module,
None => return "null".into(),
};
let obligation_entries = report
.obligations
.iter()
.filter(|obligation| {
obligation.lean_theorem_ref.is_some() || !obligation.lean_diagnostics.is_empty()
})
.map(|obligation| {
let diagnostics = obligation
.lean_diagnostics
.iter()
.map(|diagnostic| format!("\"{}\"", esc(diagnostic)))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"obligation_name\":\"{}\",\"theorem_ref\":{},\"diagnostics\":[{}]}}",
esc(&obligation.obligation_name),
obligation
.lean_theorem_ref
.as_ref()
.map(|theorem_ref| format!("\"{}\"", esc(theorem_ref)))
.unwrap_or_else(|| "null".into()),
diagnostics
)
})
.collect::<Vec<_>>()
.join(",");
let module_diagnostics = module
.diagnostics
.iter()
.map(|diagnostic| format!("\"{}\"", esc(diagnostic)))
.collect::<Vec<_>>()
.join(",");
let theorem_failures = module
.theorem_failures
.iter()
.map(|failure| format!("\"{}\"", esc(failure)))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"schema_version\":\"{}\",\"module_name\":\"{}\",\"module_diagnostics\":[{}],\"theorem_failures\":[{}],\"obligations\":[{}]}}",
VERIFICATION_SIDECAR_SCHEMA_VERSION,
esc(&module.module_name),
module_diagnostics,
theorem_failures,
obligation_entries
)
}
fn path_to_string(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AlgebraicSignature, ExecutionResult, ExecutionStatus, LeanDiagnostic, LeanOutput, Origin,
SmtOutput, Sort,
};
#[cfg(feature = "amari")]
use crate::{StatisticalBound, verify_rare_event};
fn sample_session(root: &Path) -> VerificationSession {
let sig = AlgebraicSignature::monoid(Sort::Int, "combine", "e");
let bundle = ObligationBundle::monoid(
"sum_monoid",
Origin::new("karpal-core", "Monoid for Sum<i32>"),
&sig,
);
VerificationSession::new(bundle, ArtifactLayout::new(root), "KarpalVerify")
}
struct SuccessRunner;
impl VerifierRunner for SuccessRunner {
fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
ExecutionResult {
plan: plan.clone(),
status: match plan.kind {
crate::CommandKind::Smt => ExecutionStatus::Unsat,
crate::CommandKind::Lean | crate::CommandKind::Kani => ExecutionStatus::Success,
},
stdout: String::new(),
stderr: String::new(),
exit_code: Some(0),
backend_version: Some("tool 1.0".into()),
smt_output: Some(SmtOutput {
status: Some(ExecutionStatus::Unsat),
model: None,
reason_unknown: None,
}),
lean_output: (plan.kind == crate::CommandKind::Lean).then(|| LeanOutput {
diagnostics: vec![LeanDiagnostic {
file: Some("lean/KarpalVerify.lean".into()),
line: Some(4),
column: Some(2),
severity: "warning".into(),
message: "declaration uses sorry: associativity".into(),
theorem_hits: vec!["associativity".into()],
}],
theorem_hits: vec!["associativity".into()],
}),
}
}
}
#[test]
fn dry_run_report_orchestrates_artifacts_and_report() {
let session = sample_session(Path::new("target/karpal-verify-session-dry-run"));
let report = session.dry_run_report();
assert_eq!(report.obligation_count(), 3);
assert_eq!(report.root, "target/karpal-verify-session-dry-run");
assert!(
report
.obligations
.iter()
.all(|entry| entry.status() == Some(ExecutionStatus::DryRun))
);
}
#[test]
fn verify_report_builds_artifacts_runs_plans_and_returns_report() {
let temp = std::env::temp_dir().join("karpal_verify_session_report_test");
if temp.exists() {
let _ = fs::remove_dir_all(&temp);
}
let session = sample_session(&temp);
let report = session
.verify_report(&SuccessRunner)
.expect("session verification should succeed");
assert!(report.is_success());
assert!(temp.join("smt").exists());
assert!(temp.join("lean").exists());
assert_eq!(
report
.lean_module
.as_ref()
.map(|module| module.theorem_failures.clone()),
Some(vec!["KarpalVerify.associativity".into()])
);
let _ = fs::remove_dir_all(&temp);
}
#[test]
fn verify_with_ci_outputs_writes_reports_beside_artifacts() {
let temp = std::env::temp_dir().join("karpal_verify_session_ci_output_test");
if temp.exists() {
let _ = fs::remove_dir_all(&temp);
}
let output = sample_session(&temp)
.with_report_stem("summary")
.verify_with_ci_outputs(&SuccessRunner)
.expect("ci outputs should be written");
assert!(output.report.is_success());
assert!(Path::new(&output.report_files.json_path).exists());
assert!(Path::new(&output.report_files.markdown_path).exists());
assert!(
Path::new(
output
.report_files
.lean_diagnostics_json_path
.as_deref()
.expect("lean diagnostics sidecar should be written")
)
.exists()
);
assert!(
Path::new(
output
.report_files
.lean_manifest_path
.as_deref()
.expect("lean manifest path should be recorded")
)
.exists()
);
assert!(output.report_files.json_path.ends_with("summary.json"));
assert!(output.report_files.markdown_path.ends_with("summary.md"));
#[cfg(not(feature = "amari"))]
{
assert!(output.report_files.three_tier_json_path.is_none());
assert!(output.report_files.three_tier_markdown_path.is_none());
}
assert!(
output
.report_files
.lean_diagnostics_json_path
.as_deref()
.unwrap()
.ends_with("summary.lean-diagnostics.json")
);
assert!(
output
.report_files
.lean_manifest_path
.as_deref()
.unwrap()
.ends_with("lean/KarpalVerify.manifest.json")
);
let json = fs::read_to_string(&output.report_files.json_path)
.expect("summary json should be readable");
let markdown = fs::read_to_string(&output.report_files.markdown_path)
.expect("summary markdown should be readable");
let sidecar = fs::read_to_string(
output
.report_files
.lean_diagnostics_json_path
.as_deref()
.expect("lean diagnostics sidecar path should be present"),
)
.expect("lean diagnostics sidecar should be readable");
let manifest = fs::read_to_string(
output
.report_files
.lean_manifest_path
.as_deref()
.expect("lean manifest path should be present"),
)
.expect("lean manifest should be readable");
assert!(json.contains("\"schema_version\":\"1\""));
assert!(json.contains("\"report_files\""));
assert!(json.contains("\"lean_manifest_path\""));
assert!(json.contains("\"lean_diagnostics_json_path\""));
assert!(json.contains("\"schema_version\":\"1\",\"json_path\""));
assert!(markdown.contains("Report files:"));
assert!(markdown.contains("Schema version: `1`"));
assert!(markdown.contains("Lean diagnostics JSON"));
assert!(markdown.contains("Lean manifest"));
assert!(sidecar.contains("\"schema_version\":\"1\""));
assert!(manifest.contains("\"schema_version\":\"1\""));
assert!(manifest.contains("\"report_files\""));
assert!(manifest.contains("\"json_path\""));
assert!(manifest.contains("\"markdown_path\""));
assert!(manifest.contains("\"lean_diagnostics_json_path\""));
let _ = fs::remove_dir_all(&temp);
}
#[cfg(feature = "amari")]
#[test]
fn verify_with_ci_outputs_writes_three_tier_sidecars_when_statistical_evidence_exists() {
let temp = std::env::temp_dir().join("karpal_verify_session_three_tier_test");
if temp.exists() {
let _ = fs::remove_dir_all(&temp);
}
let bundle = sample_session(&temp).bundle().clone();
let statistical = verify_rare_event(
&bundle.obligations()[0],
&StatisticalBound::new(0.05).with_samples(128),
|| false,
);
let output = sample_session(&temp)
.with_report_stem("summary")
.with_statistical_verification(statistical)
.verify_with_ci_outputs(&DryRunner)
.expect("ci outputs should be written");
let three_tier_json_path = output
.report_files
.three_tier_json_path
.as_deref()
.expect("three-tier json should be written");
let three_tier_markdown_path = output
.report_files
.three_tier_markdown_path
.as_deref()
.expect("three-tier markdown should be written");
assert!(Path::new(three_tier_json_path).exists());
assert!(Path::new(three_tier_markdown_path).exists());
assert!(three_tier_json_path.ends_with("summary.three-tier.json"));
assert!(three_tier_markdown_path.ends_with("summary.three-tier.md"));
let json = fs::read_to_string(three_tier_json_path).expect("three-tier json readable");
let markdown =
fs::read_to_string(three_tier_markdown_path).expect("three-tier markdown readable");
let main_json =
fs::read_to_string(&output.report_files.json_path).expect("main report json readable");
let main_markdown = fs::read_to_string(&output.report_files.markdown_path)
.expect("main report markdown readable");
assert!(json.contains("\"impossible_count\":1"));
assert!(json.contains("\"external_count\":2"));
assert!(json.contains("\"report_files\""));
assert!(markdown.contains("Three-Tier Verification Report"));
assert!(markdown.contains("Schema version: `1`"));
assert!(main_json.contains("\"three_tier_json_path\""));
assert!(main_json.contains("\"three_tier_markdown_path\""));
assert!(main_markdown.contains("Three-tier JSON"));
assert!(main_markdown.contains("Three-tier Markdown"));
let _ = fs::remove_dir_all(&temp);
}
}