use std::collections::HashSet;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use cordance_core::lock::SourceLock;
use cordance_core::pack::PackTargets;
use crate::pack_cmd::{self, OutputMode, PackConfig};
pub fn run(target: &Utf8PathBuf) -> Result<i32> {
let lock_path = target.join(".cordance/sources.lock");
if !lock_path.exists() {
eprintln!("cordance check: no sources.lock found at {lock_path}");
eprintln!(" Run 'cordance pack' first to generate a lock.");
return Ok(1);
}
let prev_json =
std::fs::read_to_string(&lock_path).with_context(|| format!("reading {lock_path}"))?;
let prev: SourceLock =
serde_json::from_str(&prev_json).with_context(|| format!("parsing {lock_path}"))?;
let fresh = compute_fresh_lock(target)?;
let fenced = collect_fenced_outputs(&fresh, target);
let report = fresh.diff(&prev, &fenced);
if report.is_clean() {
println!("cordance check: clean");
} else {
if !report.source_drifts.is_empty() {
eprintln!(
"Source drift detected ({} file(s)):",
report.source_drifts.len()
);
for d in &report.source_drifts {
if d.old_sha256 == "ADDED" {
let new_short: String = d.new_sha256.chars().take(8).collect();
eprintln!(" + {} ({new_short})", d.path);
} else if d.new_sha256 == "DELETED" {
let old_short: String = d.old_sha256.chars().take(8).collect();
eprintln!(" - {} ({old_short})", d.path);
} else {
let old_short: String = d.old_sha256.chars().take(8).collect();
let new_short: String = d.new_sha256.chars().take(8).collect();
eprintln!(" ~ {} ({old_short} -> {new_short})", d.path);
}
}
}
if !report.fenced_output_drifts.is_empty() {
eprintln!(
"Managed region edited out-of-band ({} file(s)):",
report.fenced_output_drifts.len()
);
for d in &report.fenced_output_drifts {
eprintln!(" {}", d.path);
}
}
if !report.unfenced_output_drifts.is_empty() {
println!(
"User-owned changes ({} file(s)) — not an error:",
report.unfenced_output_drifts.len()
);
}
}
Ok(report.exit_code())
}
pub fn compute_fresh_lock(target: &Utf8PathBuf) -> Result<SourceLock> {
let scan_cfg = PackConfig {
target: target.clone(),
output_mode: OutputMode::DryRun,
selected_targets: PackTargets {
claude_code: true,
cursor: true,
codex: true,
axiom_harness_target: true,
cortex_receipt: true,
},
doctrine_root: None,
llm_provider: Some("none".into()),
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
let fresh_pack = pack_cmd::run(&scan_cfg).context("cordance check: rescan failed")?;
Ok(SourceLock::compute_from_pack(&fresh_pack))
}
pub fn collect_fenced_outputs(lock: &SourceLock, repo_root: &Utf8PathBuf) -> HashSet<String> {
let mut set = HashSet::new();
for entry in &lock.outputs {
let abs = repo_root.join(&entry.path);
let Ok(content) = std::fs::read_to_string(&abs) else {
continue;
};
if cordance_core::fence::find_regions(&content)
.map(|regions| !regions.is_empty())
.unwrap_or(false)
{
set.insert(entry.path.as_str().to_string());
}
}
set
}