use camino::Utf8PathBuf;
use cordance_core::lock::{DriftReport, OutputDriftEntry, SourceDriftEntry, SourceLock};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::check_cmd;
use crate::mcp::error::{McpToolError, McpToolResult};
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct CheckDriftParams {
#[serde(default)]
pub target: Option<String>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct CheckDriftOutput {
pub schema: String,
pub clean: bool,
pub exit_code: i32,
pub source_drifts: Vec<SourceDriftSummary>,
pub fenced_output_drifts: Vec<OutputDriftSummary>,
pub unfenced_output_drifts: Vec<OutputDriftSummary>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct SourceDriftSummary {
pub id: String,
pub path: String,
pub old_sha256: String,
pub new_sha256: String,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct OutputDriftSummary {
pub path: String,
pub old_sha256: String,
pub new_sha256: String,
}
pub fn drift(target: &Utf8PathBuf) -> McpToolResult<CheckDriftOutput> {
let lock_path = target.join(".cordance/sources.lock");
if !lock_path.exists() {
tracing::warn!(path = %lock_path, "check_drift: sources.lock missing");
return Err(McpToolError::artifact_missing("sources.lock"));
}
let raw = std::fs::read_to_string(&lock_path).map_err(McpToolError::from_io)?;
let prev: SourceLock = serde_json::from_str(&raw)
.map_err(|e| McpToolError::internal_redacted("parse sources.lock", e))?;
let fresh = check_cmd::compute_fresh_lock(target).map_err(McpToolError::from_anyhow)?;
let fenced = check_cmd::collect_fenced_outputs(&fresh, target);
let report = fresh.diff(&prev, &fenced);
Ok(to_output(&report))
}
fn to_output(report: &DriftReport) -> CheckDriftOutput {
CheckDriftOutput {
schema: "cordance-check-drift.v1".to_string(),
clean: report.is_clean(),
exit_code: report.exit_code(),
source_drifts: report
.source_drifts
.iter()
.map(summarise_source_drift)
.collect(),
fenced_output_drifts: report
.fenced_output_drifts
.iter()
.map(summarise_output_drift)
.collect(),
unfenced_output_drifts: report
.unfenced_output_drifts
.iter()
.map(summarise_output_drift)
.collect(),
}
}
fn summarise_source_drift(d: &SourceDriftEntry) -> SourceDriftSummary {
SourceDriftSummary {
id: d.id.clone(),
path: d.path.clone(),
old_sha256: d.old_sha256.clone(),
new_sha256: d.new_sha256.clone(),
}
}
fn summarise_output_drift(d: &OutputDriftEntry) -> OutputDriftSummary {
OutputDriftSummary {
path: d.path.clone(),
old_sha256: d.old_sha256.clone(),
new_sha256: d.new_sha256.clone(),
}
}