repopilot 0.9.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::audits::context::classify_file;
use crate::explain::model::{ExplainContext, ExplainDecision, ExplainReport, ExplainSource};
use crate::findings::types::Severity;
use crate::knowledge::decision::decide_for_file;
use crate::knowledge::language::{detect_language_for_path, profile_by_id};
use crate::knowledge::model::{RuleDecisionAction, SupportLevel};
use crate::scan::facts::FileFacts;
use std::fs;
use std::io;
use std::path::Path;

pub fn build_explain_report(
    path: &Path,
    rule_id: Option<&str>,
    signal: Option<&str>,
    base_severity: Severity,
) -> Result<ExplainReport, io::Error> {
    let content = fs::read_to_string(path)?;
    let language_name = detect_language_for_path(path).map(str::to_string);
    let lines_of_code = count_non_empty_lines(&content);
    let has_inline_tests = content.contains("#[cfg(test)]");

    let file = FileFacts {
        path: path.to_path_buf(),
        language: language_name.clone(),
        lines_of_code,
        branch_count: 0,
        imports: Vec::new(),
        content: Some(content),
        has_inline_tests,
    };

    let audit_context = classify_file(&file);
    let language_id = audit_context.language_id();
    let language_support =
        profile_by_id(language_id).map(|profile| support_level_label(profile.support).to_string());

    let context = ExplainContext {
        language: language_id.to_string(),
        language_support,
        frameworks: audit_context
            .framework_ids()
            .into_iter()
            .map(str::to_string)
            .collect(),
        roles: audit_context
            .role_ids()
            .into_iter()
            .map(str::to_string)
            .collect(),
        paradigms: audit_context
            .paradigm_ids()
            .into_iter()
            .map(str::to_string)
            .collect(),
        runtimes: audit_context
            .runtime_ids()
            .into_iter()
            .map(str::to_string)
            .collect(),
        is_test: audit_context.is_test,
        is_production_code: audit_context.is_production_code(),
    };

    let decision = rule_id.map(|rule_id| {
        let decision = decide_for_file(rule_id, &file, base_severity, signal);

        ExplainDecision {
            rule_id: rule_id.to_string(),
            signal: signal.map(str::to_string),
            base_severity,
            action: decision_action_label(decision.action).to_string(),
            final_severity: decision.severity,
            reason: decision.reason,
        }
    });

    Ok(ExplainReport {
        path: path.display().to_string(),
        source: ExplainSource {
            language_name,
            lines_of_code,
            has_inline_tests,
        },
        context,
        decision,
    })
}

fn count_non_empty_lines(content: &str) -> usize {
    content
        .lines()
        .filter(|line| !line.trim().is_empty())
        .count()
}

fn support_level_label(level: SupportLevel) -> &'static str {
    match level {
        SupportLevel::DetectOnly => "detect-only",
        SupportLevel::ImportAware => "import-aware",
        SupportLevel::ContextAware => "context-aware",
        SupportLevel::RuleAware => "rule-aware",
    }
}

fn decision_action_label(action: RuleDecisionAction) -> &'static str {
    match action {
        RuleDecisionAction::Apply => "apply",
        RuleDecisionAction::Suppress => "suppress",
        RuleDecisionAction::Downgrade => "downgrade",
        RuleDecisionAction::Upgrade => "upgrade",
    }
}