use anyhow::{Context, Result};
use colored::Colorize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Serialize)]
struct VerifyEntry {
file: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
expected: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
actual: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<String>,
}
impl VerifyEntry {
fn matched(file: &str, expected: &str, actual: &str) -> Self {
Self {
file: file.to_string(),
status: "matched",
expected: Some(expected.to_string()),
actual: Some(actual.to_string()),
note: None,
}
}
fn mismatched(file: &str, expected: &str, actual: &str) -> Self {
Self {
file: file.to_string(),
status: "mismatched",
expected: Some(expected.to_string()),
actual: Some(actual.to_string()),
note: None,
}
}
fn error(file: &str, expected: &str, note: &str) -> Self {
Self {
file: file.to_string(),
status: "mismatched",
expected: Some(expected.to_string()),
actual: None,
note: Some(note.to_string()),
}
}
fn missing(file: &str, expected: &str) -> Self {
Self {
file: file.to_string(),
status: "missing",
expected: Some(expected.to_string()),
actual: None,
note: None,
}
}
}
pub fn provenance_verify_command(checkpoint_path: PathBuf, json: bool) -> Result<()> {
let checkpoint_path =
std::path::absolute(&checkpoint_path).context("failed to resolve checkpoint path")?;
eprintln!(
"{} {}",
"Provenance Verify".bold().cyan(),
checkpoint_path.display()
);
eprintln!();
let checkpoint_content = std::fs::read_to_string(&checkpoint_path)
.with_context(|| format!("failed to read {}", checkpoint_path.display()))?;
let checkpoint: serde_json::Value =
serde_json::from_str(&checkpoint_content).with_context(|| {
format!(
"failed to parse checkpoint JSON: {}",
checkpoint_path.display()
)
})?;
if let Some(sha) = checkpoint
.get("workflow_git_sha")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
eprintln!(" {} workflow git HEAD: {}", "•".dimmed(), sha);
}
if let Some(path) = checkpoint.get("workflow_path").and_then(|v| v.as_str()) {
eprintln!(" {} workflow path: {}", "•".dimmed(), path);
}
eprintln!();
let stored_checksums: HashMap<String, String> = if let Some(checksums) =
checkpoint.get("checksums").and_then(|v| v.as_object())
{
checksums
.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
.collect()
} else {
let companion = checkpoint_path.with_extension("checksums.json");
if companion.exists() {
let content = std::fs::read_to_string(&companion)
.context("failed to read companion checksums file")?;
serde_json::from_str(&content).context("failed to parse companion checksums file")?
} else {
HashMap::new()
}
};
let workdir = checkpoint
.get("workdir")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| {
checkpoint_path
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf()
});
if json {
eprintln!(" {} workdir: {}", "•".dimmed(), workdir.display());
}
if stored_checksums.is_empty() {
if let Some(completed) = checkpoint.get("completed_rules").and_then(|v| v.as_array()) {
eprintln!(
" {} No stored checksums found. Run workflow with --provenance to enable tracking.",
"Note:".bold().yellow()
);
eprintln!(
" {} completed rules: {}\n",
"Found".bold(),
completed.len()
);
for rule_val in completed {
if let Some(rule) = rule_val.as_str() {
eprintln!(" {} {}", "✓".green(), rule);
}
}
eprintln!(
"\n{} To verify integrity, provide a checksums file.",
"Hint:".bold().cyan()
);
if json {
let output = serde_json::json!({
"command": "provenance",
"verify": {
"checkpoint": checkpoint_path.display().to_string(),
"matched": 0,
"mismatched": 0,
"missing": 0,
"entries": [],
"note": "No stored checksums found. Run workflow with --provenance to enable tracking.",
},
});
println!("{}", serde_json::to_string_pretty(&output)?);
}
return Ok(());
}
eprintln!(
" {} No completed rules or checksums found.",
"✗".red().bold()
);
if json {
let output = serde_json::json!({
"command": "provenance",
"verify": {
"checkpoint": checkpoint_path.display().to_string(),
"matched": 0,
"mismatched": 0,
"missing": 0,
"entries": [],
"note": "No completed rules or checksums found in the checkpoint.",
},
});
println!("{}", serde_json::to_string_pretty(&output)?);
}
return Ok(());
}
let mut matched = 0usize;
let mut mismatched = 0usize;
let mut missing = 0usize;
let mut entries: Vec<VerifyEntry> = Vec::new();
let mut files_to_check: Vec<&String> = stored_checksums.keys().collect();
files_to_check.sort_unstable();
for file in files_to_check {
let expected = &stored_checksums[file];
let full_path = workdir.join(file);
if !full_path.exists() {
eprintln!(" {} {} (file missing)", "✗".red().bold(), file);
entries.push(VerifyEntry::missing(file, expected));
missing += 1;
continue;
}
match oxo_flow_core::executor::checkpoint::compute_file_checksum(&full_path) {
Ok(actual) if actual == *expected => {
eprintln!(" {} {} {}", "✓".green().bold(), file, actual.dimmed());
entries.push(VerifyEntry::matched(file, expected, &actual));
matched += 1;
}
Ok(actual) => {
eprintln!(
" {} {} (expected: {}, actual: {})",
"✗".red().bold(),
file,
expected,
actual
);
entries.push(VerifyEntry::mismatched(file, expected, &actual));
mismatched += 1;
}
Err(e) => {
eprintln!(" {} {} (checksum error: {})", "✗".red().bold(), file, e);
entries.push(VerifyEntry::error(file, expected, &e.to_string()));
mismatched += 1;
}
}
}
eprintln!();
eprintln!(
"{} {} matched, {} mismatched, {} missing",
"Summary:".bold(),
matched,
mismatched,
missing
);
if json {
let output = serde_json::json!({
"command": "provenance",
"verify": {
"checkpoint": checkpoint_path.display().to_string(),
"matched": matched,
"mismatched": mismatched,
"missing": missing,
"entries": entries,
},
});
println!("{}", serde_json::to_string_pretty(&output)?);
}
if mismatched > 0 || missing > 0 {
std::process::exit(1);
}
Ok(())
}