sniff-cli 0.1.2

An exhaustive LLM-backed slop finder for codebases
Documentation
use crate::report_types::RunReport;
use std::io::Write;

use super::summary::append_footer;

pub(super) fn write_markdown_report(
    run_report: &RunReport,
    out_path: &str,
    cost_str: &str,
) -> Result<(), String> {
    let mut md_lines = vec!["# Sniff Report".to_string(), "".to_string()];
    let mut kinda_count = 0usize;
    for verdict in &run_report.file_verdicts {
        match verdict.verdict {
            crate::types::FindingTier::Slop => render_file_verdict_markdown(verdict, &mut md_lines),
            crate::types::FindingTier::KindaSlop => {
                kinda_count += 1;
            }
            crate::types::FindingTier::Clean => {}
        }
    }
    if kinda_count > 0 {
        md_lines.push("## Kinda Slop Findings".to_string());
        md_lines.push(String::new());
        md_lines.push(
            "_These are mild slop signals that did not clear the main slop threshold._".to_string(),
        );
        md_lines.push(String::new());
        for verdict in &run_report.file_verdicts {
            if verdict.verdict == crate::types::FindingTier::KindaSlop {
                render_file_verdict_markdown(verdict, &mut md_lines);
            }
        }
        md_lines.push(String::new());
    }
    append_footer(
        &mut md_lines,
        &run_report.stats,
        &run_report.file_verdicts,
        cost_str,
    );
    let mut f = std::fs::File::create(out_path)
        .map_err(|err| format!("failed to create report file {out_path}: {}", err))?;
    f.write_all(md_lines.join("\n").as_bytes())
        .map_err(|err| format!("failed to write report file {out_path}: {}", err))?;
    Ok(())
}

pub(super) fn render_file_verdict_markdown(
    verdict: &crate::report_types::FileVerdict,
    md_lines: &mut Vec<String>,
) {
    if verdict.verdict == crate::types::FindingTier::Clean {
        return;
    }

    md_lines.push(format!("## `{}`", verdict.file_path));
    md_lines.push(String::new());
    md_lines.push(format!("- **Role:** `{}`", verdict.role));
    md_lines.push(format!("- **Verdict:** `{}`", verdict.verdict.label()));

    if !verdict.top_reasons.is_empty() {
        md_lines.push("- **Top reasons:**".to_string());
        for reason in &verdict.top_reasons {
            md_lines.push(format!("  - {}", reason));
        }
    }

    if !verdict.flagged_methods.is_empty() {
        md_lines.push(format!(
            "- **Flagged methods:** {}",
            verdict
                .flagged_methods
                .iter()
                .map(|method| format!("`{}`", method))
                .collect::<Vec<_>>()
                .join(", ")
        ));
    }

    md_lines.push(format!(
        "- **Recommended action:** {}",
        verdict.recommended_action
    ));
    md_lines.push(String::new());
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::report_types::{FileVerdict, RunReport, RunStats};
    use crate::types::FindingTier;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_report_path() -> String {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        std::env::temp_dir()
            .join(format!("sniff-markdown-report-{nanos}.md"))
            .to_string_lossy()
            .to_string()
    }

    fn verdict(file_path: &str, tier: FindingTier) -> FileVerdict {
        FileVerdict {
            file_path: file_path.to_string(),
            role: "core_library".to_string(),
            verdict: tier,
            top_reasons: vec!["reason".to_string()],
            flagged_methods: vec!["sample".to_string()],
            recommended_action: "trim the largest offender and keep the file focused".to_string(),
        }
    }

    #[test]
    fn markdown_report_shows_kinda_slop_appendix() {
        let report = RunReport {
            file_verdicts: vec![
                verdict("slop.rs", FindingTier::Slop),
                verdict("borderline.rs", FindingTier::KindaSlop),
            ],
            static_flags: vec![],
            llm_verdicts: vec![],
            stats: RunStats {
                files_scanned: 2,
                methods_analyzed: 2,
                ..RunStats::default()
            },
        };
        let out_path = temp_report_path();

        write_markdown_report(&report, &out_path, "$0.00").unwrap();
        let md = fs::read_to_string(&out_path).unwrap();
        let _ = fs::remove_file(&out_path);

        assert!(md.contains("slop.rs"));
        assert!(md.contains("borderline.rs"));
        assert!(md.contains("Kinda Slop Findings"));
        assert!(md.contains("mild slop signals"));
    }
}