repopilot 0.9.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::audits::traits::FileAudit;
use crate::findings::types::{Evidence, Finding, FindingCategory, Severity};
use crate::knowledge::decision::apply_file_decision;
use crate::scan::config::ScanConfig;
use crate::scan::facts::FileFacts;
use crate::scan::markers::detect_markers;
use crate::scan::types::{Marker, MarkerKind};

pub struct CodeMarkerAudit;

impl FileAudit for CodeMarkerAudit {
    fn audit(&self, file: &FileFacts, _config: &ScanConfig) -> Vec<Finding> {
        if should_skip_marker_audit(&file.path) {
            return vec![];
        }

        detect_markers(&file.path, file.content.as_deref().unwrap_or(""))
            .iter()
            .filter_map(|marker| {
                let finding = build_marker_finding(marker);
                let rule_id = finding.rule_id.clone();
                apply_file_decision(&rule_id, file, finding, None)
            })
            .collect()
    }
}

pub fn detect_code_marker_findings(file: &FileFacts) -> Vec<Finding> {
    detect_markers(&file.path, file.content.as_deref().unwrap_or(""))
        .iter()
        .filter_map(|marker| {
            let finding = build_marker_finding(marker);
            let rule_id = finding.rule_id.clone();
            apply_file_decision(&rule_id, file, finding, None)
        })
        .collect()
}

fn build_marker_finding(marker: &Marker) -> Finding {
    let marker_str = match marker.kind {
        MarkerKind::Todo => "todo",
        MarkerKind::Fixme => "fixme",
        MarkerKind::Hack => "hack",
    };
    let uppercase = marker_str.to_uppercase();

    Finding {
        id: String::new(),
        rule_id: format!("code-marker.{marker_str}"),
        recommendation: Finding::recommendation_for_rule_id(&format!("code-marker.{marker_str}")),
        title: format!("{uppercase} marker found"),
        description: format!(
            "A {uppercase} marker was found in the codebase and should be reviewed."
        ),
        category: FindingCategory::CodeQuality,
        severity: marker_severity(marker_str),
        confidence: Default::default(),
        evidence: vec![Evidence {
            path: marker.path.clone(),
            line_start: marker.line_number,
            line_end: None,
            snippet: marker.text.trim().to_string(),
        }],
        workspace_package: None,
        docs_url: None,
    }
}

fn marker_severity(marker: &str) -> Severity {
    match marker {
        "fixme" | "hack" => Severity::Medium,
        "todo" => Severity::Low,
        _ => Severity::Info,
    }
}

fn should_skip_marker_audit(path: &std::path::Path) -> bool {
    let is_markdown = path
        .extension()
        .and_then(|ext| ext.to_str())
        .is_some_and(|ext| ext.eq_ignore_ascii_case("md"));

    is_markdown || has_component(path, "tests") || has_component(path, "test")
}

fn has_component(path: &std::path::Path, component: &str) -> bool {
    path.components()
        .any(|c| c.as_os_str().to_string_lossy() == component)
}