use std::path::PathBuf;
use clap::Args;
use ossctl_core::contract::{self, LoadError, Normalized};
use ossctl_core::protocol::audit::AuditReport;
use crate::error::CliError;
use crate::output::OutputFormat;
use crate::sys::{RealCommandRunner, RealFs, RealGitRepo};
#[derive(Args, Debug)]
pub struct AuditArgs {
#[arg(long, value_name = "PATH")]
pub repo_root: Option<PathBuf>,
}
pub fn run(args: &AuditArgs, format: OutputFormat) -> Result<(), CliError> {
let repo_root = resolve_repo_root(args.repo_root.as_ref())?;
if !repo_root.is_dir() {
return Err(CliError::user(
"invalid_repo_root",
format!("repo_root '{}' is not a directory", repo_root.display()),
)
.with_invalid_value(repo_root.display().to_string()));
}
let root = std::fs::canonicalize(&repo_root).map_err(|e| {
CliError::system(
"io_error",
format!(
"cannot canonicalize repo_root '{}': {e}",
repo_root.display()
),
)
})?;
let normalized = contract::normalize(&root, &RealFs).map_err(load_error_to_cli)?;
if !normalized.is_valid() {
return Err(invalid_contract_error(&normalized));
}
let git = RealGitRepo::new(&root);
let facts = ossctl_core::facts::gather(&root, &RealFs, &git);
let report = ossctl_core::audit::audit(
&root,
&normalized.contract,
&facts,
&RealFs,
&RealCommandRunner,
);
match format {
OutputFormat::Json => crate::output::emit_json(&report, &[])?,
OutputFormat::Text => render_audit_text(&report),
}
Ok(())
}
fn resolve_repo_root(flag: Option<&PathBuf>) -> Result<PathBuf, CliError> {
match flag {
Some(p) => Ok(p.clone()),
None => std::env::current_dir()
.map_err(|e| CliError::system("io_error", format!("cannot resolve cwd: {e}"))),
}
}
fn load_error_to_cli(e: LoadError) -> CliError {
let code = match e {
LoadError::NotFound(_) => "contract_not_found",
LoadError::Io(..) => "io_error",
LoadError::Utf8(_) => "invalid_encoding",
};
CliError::system(code, e.to_string())
}
fn invalid_contract_error(normalized: &Normalized) -> CliError {
let problems = &normalized.problems.errors;
let message = format!(
"{} would not normalize: {} problem(s) — fix the contract before auditing",
contract::CONTRACT_FILENAME,
problems.len()
);
CliError::user("invalid_contract", message).with_problems(problems.clone())
}
fn render_audit_text(report: &AuditReport) {
let core = report.core_complete.as_str();
println!("repo_root: {}", report.repo_root);
println!("maturity: {}", report.maturity.as_str());
println!("gated core: {core}");
println!("gaps: {}", report.gaps.len());
for g in &report.gaps {
println!(
" [{:>11}] {:<24} ({}, {}, {}) — {}",
g.severity.as_str(),
g.id,
g.category.as_str(),
g.member,
g.status.as_str(),
g.detail
);
}
let cp = &report.community_profile;
if cp.checked {
println!("community: GitHub community-profile checked");
} else {
println!(
"community: not checked ({})",
cp.unavailable_reason.as_deref().unwrap_or("unknown")
);
}
}