repopilot 0.10.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::findings::types::FindingCategory;
use crate::output::color;
use crate::output::report_stats::{NamedCount, ReportStats, severity_order};

pub(crate) fn console_severity_counts_text(stats: &ReportStats) -> String {
    let parts = severity_order()
        .iter()
        .filter_map(|severity| {
            let count = stats.severity_count(*severity);
            (count > 0).then(|| color::severity_count(*severity, count))
        })
        .collect::<Vec<_>>();

    join_or_none(parts, " | ")
}

pub(crate) fn markdown_severity_counts_text(stats: &ReportStats) -> String {
    let parts = severity_order()
        .iter()
        .filter_map(|severity| {
            let count = stats.severity_count(*severity);
            (count > 0).then(|| format!("{} {}", count, severity.lowercase_label()))
        })
        .collect::<Vec<_>>();

    join_or_none(parts, ", ")
}

pub(crate) fn named_counts_text(counts: &[NamedCount]) -> String {
    if counts.is_empty() {
        return "none".to_string();
    }

    counts
        .iter()
        .map(|count| format!("{} ({})", count.label, count.count))
        .collect::<Vec<_>>()
        .join(", ")
}

pub(crate) fn category_title(category: &FindingCategory) -> &'static str {
    match category {
        FindingCategory::Security => "Security",
        FindingCategory::Architecture => "Architecture",
        FindingCategory::Framework => "Framework",
        FindingCategory::CodeQuality => "Code Quality",
        FindingCategory::Testing => "Testing",
    }
}

pub(crate) fn category_label_rank(label: &str) -> usize {
    match label {
        "security" => 0,
        "architecture" => 1,
        "framework" => 2,
        "code-quality" => 3,
        "testing" => 4,
        _ => usize::MAX,
    }
}

pub(crate) fn first_sentence(text: &str, max_len: usize) -> String {
    let sentence = text.split(". ").next().unwrap_or(text);
    if sentence.len() <= max_len {
        sentence.to_string()
    } else {
        format!("{}...", &sentence[..max_len])
    }
}

pub(crate) fn tristate_label(value: Option<bool>) -> &'static str {
    match value {
        Some(true) => "enabled",
        Some(false) => "disabled",
        None => "unknown",
    }
}

fn join_or_none(parts: Vec<String>, separator: &str) -> String {
    if parts.is_empty() {
        "none".to_string()
    } else {
        parts.join(separator)
    }
}