sniff-cli 0.1.5

An exhaustive LLM-backed slop finder for codebases
Documentation
use super::{env, io, llm, stats};
use crate::report_types::RunReport;
use indicatif::ProgressStyle;
use std::hash::{Hash, Hasher};
use std::io::{Error as IoError, ErrorKind};

fn build_progress_style() -> Result<ProgressStyle, String> {
    ProgressStyle::default_bar()
        .template("[{bar:18.cyan/dim}] {pos}/{len} {percent}%")
        .map_err(|err| err.to_string())
        .map(|style| style.progress_chars("=>-"))
}

fn exit_code_for_run(has_issues: bool, ai_failed_reviews: usize) -> i32 {
    if has_issues || ai_failed_reviews > 0 {
        1
    } else {
        0
    }
}

fn report_path_for_target(target_path: &std::path::Path) -> std::path::PathBuf {
    let current_dir = std::env::current_dir().ok();
    let resolved_target = strip_windows_verbatim_prefix(
        std::fs::canonicalize(target_path).unwrap_or_else(|_| target_path.to_path_buf()),
    );

    if let Some(current_dir) = current_dir
        && resolved_target.starts_with(&current_dir)
    {
        return current_dir.join("sniff-report.md");
    }

    let mut candidate = if resolved_target.is_dir() {
        resolved_target
    } else {
        resolved_target
            .parent()
            .map(std::path::Path::to_path_buf)
            .unwrap_or_else(|| std::path::PathBuf::from("."))
    };

    loop {
        if [".git", "Cargo.toml", "pyproject.toml", "package.json"]
            .iter()
            .any(|marker| candidate.join(marker).exists())
        {
            return candidate.join("sniff-report.md");
        }

        let Some(parent) = candidate.parent() else {
            return candidate.join("sniff-report.md");
        };
        if parent == candidate {
            return candidate.join("sniff-report.md");
        }
        candidate = parent.to_path_buf();
    }
}

fn checkpoint_path_for_target(
    target_path: &std::path::Path,
    report_path: &std::path::Path,
) -> std::path::PathBuf {
    if target_path.is_dir() {
        return report_path.with_file_name(".sniff-checkpoint.json");
    }

    let resolved_target = std::fs::canonicalize(target_path)
        .unwrap_or_else(|_| target_path.to_path_buf())
        .to_string_lossy()
        .to_string();
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    resolved_target.hash(&mut hasher);
    report_path.with_file_name(format!(".sniff-checkpoint-{:016x}.json", hasher.finish()))
}

fn strip_windows_verbatim_prefix(path: std::path::PathBuf) -> std::path::PathBuf {
    let text = path.to_string_lossy();
    if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
        return std::path::PathBuf::from(format!(r"\\{}", rest));
    }
    if let Some(rest) = text.strip_prefix(r"\\?\") {
        return std::path::PathBuf::from(rest);
    }
    path
}

fn clear_previous_report(report_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
    match std::fs::metadata(report_path) {
        Ok(metadata) if metadata.is_file() => {
            std::fs::remove_file(report_path).map_err(|err| {
                IoError::other(format!(
                    "failed to remove stale report {}: {err}",
                    report_path.display()
                ))
            })?;
        }
        Ok(_) => {
            return Err(IoError::other(format!(
                "cannot write report: {} exists and is not a file",
                report_path.display()
            ))
            .into());
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
        Err(err) => {
            return Err(IoError::other(format!(
                "failed to inspect report {}: {err}",
                report_path.display()
            ))
            .into());
        }
    }

    Ok(())
}

async fn scan_target(
    path: &str,
    config: &crate::config::ResolvedConfig,
) -> Result<Vec<crate::types::FileRecord>, String> {
    io::scan_files(path, config).await
}

async fn build_run_report(
    path: &str,
    only_files: bool,
    config: &crate::config::ResolvedConfig,
    file_records: &mut [crate::types::FileRecord],
    bar_style: &ProgressStyle,
    checkpoint_path: &std::path::Path,
) -> Result<(RunReport, bool), Box<dyn std::error::Error>> {
    let review = llm::prepare_review_artifacts(
        path,
        only_files,
        config,
        file_records,
        bar_style,
        Some(checkpoint_path),
    )
    .await?;

    let stats = stats::generate_stats(stats::StatsInput {
        file_records,
        static_flags: &review.static_flags,
        verdicts: &review.verdicts,
        in_tok: review.in_tok,
        out_tok: review.out_tok,
        ai_expected_reviews: review.ai_expected_reviews,
        method_reviews_expected: review.method_reviews_expected,
    });

    if stats.ai_failed_reviews > 0 {
        return Err(IoError::other(format!(
            "AI review incomplete: {} of {} expected reviews did not complete for {path}. Sniff will not write the report.",
            stats.ai_failed_reviews, stats.ai_expected_reviews
        ))
        .into());
    }

    Ok(stats::build_run_report_from_parts(
        file_records,
        review.static_flags,
        review.verdicts,
        only_files,
        stats,
    ))
}

pub async fn run(
    path: &str,
    only_files: bool,
    skip_dotenv: bool,
) -> Result<i32, Box<dyn std::error::Error>> {
    let target_path = std::path::Path::new(path);
    let report_path = report_path_for_target(target_path);
    let checkpoint_path = checkpoint_path_for_target(target_path, &report_path);
    env::load_working_dir_env(skip_dotenv).map_err(IoError::other)?;
    env::load_target_env(target_path, skip_dotenv).map_err(IoError::other)?;
    let config = crate::config_loader::resolve_config(target_path)
        .map_err(|err| IoError::new(ErrorKind::InvalidInput, err))?;
    crate::roles::clear_file_role_cache();
    clear_previous_report(&report_path)?;

    eprintln!("Scanning source files...");
    let mut file_records = scan_target(path, &config)
        .await
        .map_err(|err| IoError::other(format!("file scan failed: {err}")))?;

    if file_records.is_empty() {
        return Err(IoError::new(
            ErrorKind::InvalidInput,
            "No supported source files found. Sniff currently handles Rust, Python, JavaScript/TypeScript, Go, and Kotlin.",
        )
        .into());
    }
    eprintln!("Found {} supported files.", file_records.len());
    if checkpoint_path.exists() {
        eprintln!(
            "Resuming completed reviews from {}",
            checkpoint_path.display()
        );
    }
    if !config.llm.endpoint.trim().is_empty() {
        eprintln!("Using LLM endpoint: {}", config.llm.endpoint.trim());
    }
    eprintln!("Preparing report...");
    let bar_style = build_progress_style()
        .map_err(|err| IoError::other(format!("failed to build progress style: {}", err)))?;
    let (run_report, has_issues) = build_run_report(
        path,
        only_files,
        &config,
        &mut file_records,
        &bar_style,
        &checkpoint_path,
    )
    .await?;

    let report_path_text = report_path.to_string_lossy().to_string();
    crate::reporter::render_report(&run_report, &config, Some(&report_path_text))
        .map_err(IoError::other)?;

    Ok(exit_code_for_run(
        has_issues,
        run_report.stats.ai_failed_reviews,
    ))
}

#[cfg(test)]
mod tests {
    use super::{checkpoint_path_for_target, report_path_for_target};
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn external_nested_target_writes_report_at_repository_root() {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let root = std::env::temp_dir().join(format!("sniff-report-root-{nonce}"));
        let nested = root.join("src");
        fs::create_dir_all(&nested).unwrap();
        fs::create_dir_all(root.join(".git")).unwrap();

        let report_path = report_path_for_target(&nested);

        assert_eq!(report_path, root.join("sniff-report.md"));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn file_checkpoint_is_isolated_from_directory_checkpoint() {
        let root = std::env::temp_dir().join("sniff-checkpoint-scope-test");
        fs::create_dir_all(&root).unwrap();
        let target = root.join("module.py");
        fs::write(&target, "def run():\n    return 1\n").unwrap();
        let report = root.join("sniff-report.md");

        let path = checkpoint_path_for_target(&target, &report);
        assert_ne!(path, root.join(".sniff-checkpoint.json"));
        assert!(
            path.file_name()
                .and_then(|name| name.to_str())
                .is_some_and(
                    |name| name.starts_with(".sniff-checkpoint-") && name.ends_with(".json")
                )
        );

        fs::remove_dir_all(root).ok();
    }
}