sniff-cli 0.1.1

An exhaustive LLM-backed slop finder for codebases
Documentation
use crate::report_types::LLMVerdict;
use crate::types::FindingTier;

pub(crate) fn parse_result_fields(
    result: &serde_json::Value,
) -> (FindingTier, String, String, Option<bool>, Option<bool>) {
    let smelly = result
        .get("smelly")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let tier = match result.get("tier").and_then(|v| v.as_str()) {
        Some("slop") => FindingTier::Slop,
        Some("kinda_slop") => FindingTier::KindaSlop,
        Some("clean") => FindingTier::Clean,
        _ if smelly => FindingTier::Slop,
        _ => FindingTier::Clean,
    };
    let evidence = result
        .get("evidence")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .trim()
        .to_string();
    let reason = result
        .get("reason")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .trim()
        .to_string();
    let cohesive = result.get("cohesive").and_then(|v| v.as_bool());
    let name_accurate = result.get("name_accurate").and_then(|v| v.as_bool());
    (tier, evidence, reason, cohesive, name_accurate)
}

pub(crate) fn build_method_verdict(
    result: &serde_json::Value,
    file_path: &str,
    method_name: &str,
    loc: usize,
    start_line: usize,
    end_line: usize,
) -> LLMVerdict {
    let (tier, evidence, reason, _, _) = parse_result_fields(result);
    LLMVerdict {
        verdict_type: "method".to_string(),
        file_path: file_path.to_string(),
        method_name: Some(method_name.to_string()),
        check_type: "method".to_string(),
        smelly: !matches!(tier, FindingTier::Clean),
        tier,
        cohesive: None,
        name_accurate: None,
        evidence,
        reason,
        loc,
        start_line,
        end_line,
    }
}

pub(crate) fn build_file_verdict(result: &serde_json::Value, file_path: &str) -> LLMVerdict {
    let (tier, evidence, reason, cohesive, name_accurate) = parse_result_fields(result);
    LLMVerdict {
        verdict_type: "file".to_string(),
        file_path: file_path.to_string(),
        method_name: None,
        check_type: "file".to_string(),
        smelly: !matches!(tier, FindingTier::Clean),
        tier,
        cohesive,
        name_accurate,
        evidence,
        reason,
        loc: 0,
        start_line: 0,
        end_line: 0,
    }
}

pub(crate) fn evidence_is_exact_substring(source: &str, evidence: &str) -> bool {
    let trimmed = evidence.trim();
    if trimmed.is_empty() {
        return false;
    }

    if source.contains(trimmed) {
        return true;
    }

    fn strip_whitespace(text: &str) -> String {
        text.chars().filter(|ch| !ch.is_whitespace()).collect()
    }

    let normalized_source = strip_whitespace(source);
    let normalized_evidence = strip_whitespace(trimmed);
    !normalized_evidence.is_empty() && normalized_source.contains(&normalized_evidence)
}

#[cfg(test)]
mod tests {
    use super::evidence_is_exact_substring;

    #[test]
    fn evidence_match_accepts_exact_substrings() {
        assert!(evidence_is_exact_substring(
            "def demo(value):\n    return value\n",
            "def demo(value):"
        ));
    }

    #[test]
    fn evidence_match_accepts_whitespace_variants() {
        assert!(evidence_is_exact_substring(
            "def extract_python_signatures(items):\n    return 1\n",
            "def  extract_python_signatures( items ) :"
        ));
    }

    #[test]
    fn evidence_match_rejects_missing_text() {
        assert!(!evidence_is_exact_substring(
            "def demo(value):\n    return value\n",
            "def other(value):"
        ));
    }
}