mod deterministic;
pub(crate) mod input;
mod render;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use clap::{ArgGroup, Args};
use crate::analysis::findings::{self, Finding, Severity};
use crate::analysis::result::{AnalysisResult, FailureReason, union_failures};
use crate::auth;
use crate::cli::{OutputFormat, severity_parser};
use crate::config::{self, LlmConfig};
use crate::llm::cache::Cache;
use crate::llm::chain::ProviderChain;
#[derive(Debug, Args)]
#[command(group(ArgGroup::new("input").args(["paths", "staged", "diff"]).multiple(false)))]
pub struct CheckArgs {
#[arg(value_name = "PATH")]
pub paths: Vec<PathBuf>,
#[arg(long)]
pub staged: bool,
#[arg(long, value_name = "REF")]
pub diff: Option<String>,
#[arg(long, value_name = "REF", requires = "diff")]
pub tip: Option<String>,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
#[arg(long, value_name = "SEVERITY", value_parser = severity_parser())]
pub fail_on: Option<Severity>,
}
pub struct CheckOutcome {
pub tool_findings: Vec<Finding>,
pub llm_findings: Vec<Finding>,
pub failures: BTreeMap<PathBuf, FailureReason>,
pub provider_uses: Vec<ProviderUse>,
pub exit: Exit,
}
pub struct ProviderUse {
pub index: usize,
pub model: String,
pub location: String,
pub files: usize,
}
pub async fn run(args: &CheckArgs, root: &Path) -> Result<Exit> {
run_with(
args,
root,
Cache::new(Cache::default_root(), CACHE_TTL_DAYS, CACHE_MAX_BYTES),
)
.await
}
pub const CACHE_TTL_DAYS: u64 = 30;
pub const CACHE_MAX_BYTES: u64 = 256 * 1024 * 1024;
pub(crate) async fn run_with(args: &CheckArgs, root: &Path, cache: Cache) -> Result<Exit> {
run_against(args, root, cache, &auth::default_path()?).await
}
pub(crate) async fn run_against(
args: &CheckArgs,
root: &Path,
cache: Cache,
auth_path: &Path,
) -> Result<Exit> {
let config_path = root.join(config::default_config_path());
let mut config = config::load(&config_path)
.with_context(|| format!("could not load {}", config_path.display()))?;
let store = auth::AuthStore::load(auth_path)
.with_context(|| format!("could not read the auth store at {}", auth_path.display()))?;
auth::resolve(&mut config, &store);
let providers = config.providers();
if providers.is_empty() {
return Err(config::ConfigError::NoProviders(config_path.clone()).into());
}
let work = input::resolve(args, root)
.await
.with_context(|| format!("could not resolve input under {}", root.display()))?;
let (deterministic_result, llm_result) = tokio::join!(
deterministic::run(&work, root),
run_llm(&work, &providers, cache.clone()),
);
let (tool_findings, tool_failures) = deterministic_result;
let (llm_result, provider_uses) = llm_result?;
let _ = cache.evict_if_needed();
let mut failures: BTreeMap<PathBuf, FailureReason> = BTreeMap::new();
union_failures(&mut failures, work.read_failures);
union_failures(&mut failures, tool_failures);
union_failures(&mut failures, llm_result.failed_files);
let mut outcome = CheckOutcome {
tool_findings,
llm_findings: llm_result.findings,
failures,
provider_uses,
exit: Exit::Clean,
};
outcome.exit = gate(&outcome, args.fail_on);
render::render(&outcome, args.format)?;
Ok(outcome.exit)
}
fn gate(outcome: &CheckOutcome, fail_on: Option<Severity>) -> Exit {
if !outcome.failures.is_empty() {
return Exit::Unanalyzed;
}
if any_blocking_tool_finding(&outcome.tool_findings) {
return Exit::FoundIssues;
}
if let Some(threshold) = fail_on
&& findings::any_at_or_above(&outcome.llm_findings, threshold)
{
return Exit::FoundIssues;
}
Exit::Clean
}
fn any_blocking_tool_finding(findings: &[Finding]) -> bool {
!findings.is_empty()
}
async fn run_llm(
work: &input::Work,
cfgs: &[&LlmConfig],
cache: Cache,
) -> Result<(AnalysisResult, Vec<ProviderUse>)> {
let chain =
ProviderChain::new(cfgs).map_err(|e| anyhow!("could not build LLM analyzer: {e}"))?;
let analyzer = crate::analysis::code_quality::CodeQualityAnalyzer::new(chain, cache);
let result = analyzer.analyze_files(&work.by_file).await;
let uses = provider_uses(analyzer.chain());
Ok((result, uses))
}
fn provider_uses(chain: &ProviderChain) -> Vec<ProviderUse> {
chain
.providers()
.iter()
.enumerate()
.filter(|(_, provider)| provider.served() > 0)
.map(|(index, provider)| ProviderUse {
index,
model: provider.model().to_owned(),
location: provider.location().to_owned(),
files: provider.served(),
})
.collect()
}
pub use crate::Exit;
#[cfg(test)]
mod tests;