use anyhow::{anyhow, Context, Result};
use camino::Utf8PathBuf;
use cordance_core::advise::Severity;
use cordance_core::lock::{DriftReport, SourceLock};
use cordance_core::pack::{CordancePack, PackTargets};
use crate::config::Config;
use crate::pack_cmd::{self, OutputMode, PackConfig};
pub fn run(cfg: &Config, target: &Utf8PathBuf) -> Result<()> {
let pack = build_status_pack(cfg, target)?;
let source_lock = inspect_source_lock(target, &pack)?;
let advise = AdviseCounts::from_pack(&pack);
let readiness = readiness(&source_lock, &advise);
println!("cordance status");
println!("target: {target}");
println!("config: {}", config_summary(target));
println!("source lock: {}", source_lock.summary());
println!(
"advise: {} findings ({} error, {} warning, {} info)",
advise.total, advise.errors, advise.warnings, advise.info
);
println!("doctrine pin: {}", doctrine_pin_summary(&pack));
println!("axiom pin: {}", axiom_pin_summary(&pack));
println!("pack id: {}", pack_id_summary(&pack));
println!("readiness: {readiness}");
Ok(())
}
fn build_status_pack(cfg: &Config, target: &Utf8PathBuf) -> Result<CordancePack> {
let pack_config = PackConfig {
target: target.clone(),
output_mode: OutputMode::DryRun,
selected_targets: PackTargets::all(),
doctrine_root: read_only_doctrine_root_override(cfg, target)?,
llm_provider: Some("none".into()),
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
pack_cmd::run(&pack_config).context("building read-only status pack")
}
fn read_only_doctrine_root_override(
cfg: &Config,
target: &Utf8PathBuf,
) -> Result<Option<Utf8PathBuf>> {
if cfg.doctrine_root(target).exists() {
return Ok(None);
}
let temp_dir = Utf8PathBuf::from_path_buf(std::env::temp_dir()).map_err(|path| {
anyhow!(
"system temp directory is not valid UTF-8: {}",
path.display()
)
})?;
Ok(Some(temp_dir))
}
fn config_summary(target: &Utf8PathBuf) -> String {
let config_path = target.join("cordance.toml");
if config_path.exists() {
"cordance.toml found".into()
} else {
"using default config (cordance.toml missing)".into()
}
}
fn inspect_source_lock(target: &Utf8PathBuf, pack: &CordancePack) -> Result<SourceLockStatus> {
let lock_path = target.join(".cordance/sources.lock");
if !lock_path.exists() {
return Ok(SourceLockStatus::Missing);
}
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 fenced = crate::check_cmd::collect_fenced_outputs(&pack.source_lock, target);
let report = pack.source_lock.diff(&prev, &fenced);
if report.is_clean() {
Ok(SourceLockStatus::Clean)
} else {
Ok(SourceLockStatus::Drift(report))
}
}
enum SourceLockStatus {
Missing,
Clean,
Drift(DriftReport),
}
impl SourceLockStatus {
fn summary(&self) -> String {
match self {
Self::Missing => "missing (run `cordance pack` first)".into(),
Self::Clean => "clean".into(),
Self::Drift(report) => format!(
"drifted ({} source, {} managed output, {} user-owned output)",
report.source_drifts.len(),
report.fenced_output_drifts.len(),
report.unfenced_output_drifts.len()
),
}
}
}
struct AdviseCounts {
total: usize,
errors: usize,
warnings: usize,
info: usize,
}
impl AdviseCounts {
fn from_pack(pack: &CordancePack) -> Self {
let mut counts = Self {
total: pack.advise.findings.len(),
errors: 0,
warnings: 0,
info: 0,
};
for finding in &pack.advise.findings {
match finding.severity {
Severity::Error => counts.errors += 1,
Severity::Warning => counts.warnings += 1,
Severity::Info => counts.info += 1,
}
}
counts
}
}
fn doctrine_pin_summary(pack: &CordancePack) -> String {
let Some(pin) = pack.doctrine_pins.first() else {
return "none".into();
};
let short_commit = pin.commit.chars().take(12).collect::<String>();
format!("{}@{}", pin.repo, short_commit)
}
fn axiom_pin_summary(pack: &CordancePack) -> &str {
pack.project.axiom_pin.as_deref().unwrap_or("unknown")
}
fn pack_id_summary(pack: &CordancePack) -> String {
if pack.source_lock.pack_id.is_empty() {
return "none".into();
}
pack.source_lock.pack_id.chars().take(12).collect()
}
const fn readiness(source_lock: &SourceLockStatus, advise: &AdviseCounts) -> &'static str {
match source_lock {
SourceLockStatus::Drift(report)
if !report.source_drifts.is_empty() || !report.fenced_output_drifts.is_empty() =>
{
"blocked"
}
_ if advise.errors > 0 => "blocked",
SourceLockStatus::Missing | SourceLockStatus::Drift(_) => "attention",
_ if advise.warnings > 0 => "attention",
SourceLockStatus::Clean => "ready",
}
}