repopilot 0.13.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use super::{FindingIntent, FindingVisibilityDecision};
use crate::findings::types::{Confidence, Finding, FindingCategory, Severity};
use crate::risk::RiskPriority;
use std::path::Path;

pub fn classify_visibility(finding: &Finding) -> FindingVisibilityDecision {
    let intent = classify_intent(finding);

    if is_validated_secret_leak(finding) {
        return FindingVisibilityDecision::visible(
            FindingIntent::SecurityRisk,
            "validated secret or private key candidate",
        );
    }

    if finding.severity <= Severity::Low {
        return FindingVisibilityDecision::hidden(
            intent,
            "low-severity findings are strict-mode suggestions by default",
        );
    }

    match intent {
        FindingIntent::SecurityRisk => security_visibility(finding),
        FindingIntent::RuntimeRisk => runtime_visibility(finding),
        FindingIntent::ActionableRisk => actionable_visibility(finding),
        FindingIntent::Maintainability => maintainability_visibility(finding),
        FindingIntent::TestingGap => FindingVisibilityDecision::hidden(
            intent,
            "testing gaps are hidden in the default profile",
        ),
        FindingIntent::Informational => FindingVisibilityDecision::hidden(
            intent,
            "informational findings are hidden in the default profile",
        ),
    }
}

fn security_visibility(finding: &Finding) -> FindingVisibilityDecision {
    if finding.severity >= Severity::High && finding.confidence != Confidence::Low {
        return FindingVisibilityDecision::visible(
            FindingIntent::SecurityRisk,
            "high-confidence security risk",
        );
    }

    FindingVisibilityDecision::hidden(
        FindingIntent::SecurityRisk,
        "security signal is below the default confidence/severity threshold",
    )
}

fn runtime_visibility(finding: &Finding) -> FindingVisibilityDecision {
    if is_script_boundary_runtime_exit(finding) {
        return FindingVisibilityDecision::hidden(
            FindingIntent::RuntimeRisk,
            "process exit in script/tooling boundary is a strict-mode suggestion",
        );
    }

    if is_high_priority(finding.risk.priority) {
        return FindingVisibilityDecision::visible(
            FindingIntent::RuntimeRisk,
            "high-priority runtime risk",
        );
    }

    if finding.severity >= Severity::High && finding.confidence != Confidence::Low {
        return FindingVisibilityDecision::visible(
            FindingIntent::RuntimeRisk,
            "high-severity runtime risk",
        );
    }

    FindingVisibilityDecision::hidden(
        FindingIntent::RuntimeRisk,
        "runtime signal is not actionable enough for the default profile",
    )
}

fn actionable_visibility(finding: &Finding) -> FindingVisibilityDecision {
    if is_import_graph_rule(&finding.rule_id)
        && finding.risk.priority.is_at_least(RiskPriority::P2)
        && finding.confidence != Confidence::Low
    {
        return FindingVisibilityDecision::visible(
            FindingIntent::ActionableRisk,
            "stable import-graph architecture risk",
        );
    }

    if is_high_priority(finding.risk.priority) {
        return FindingVisibilityDecision::visible(
            FindingIntent::ActionableRisk,
            "high-priority actionable risk",
        );
    }

    if finding.severity >= Severity::High && finding.confidence == Confidence::High {
        return FindingVisibilityDecision::visible(
            FindingIntent::ActionableRisk,
            "high-severity high-confidence actionable risk",
        );
    }

    FindingVisibilityDecision::hidden(
        FindingIntent::ActionableRisk,
        "actionable signal is below the default visibility threshold",
    )
}

fn maintainability_visibility(finding: &Finding) -> FindingVisibilityDecision {
    if is_strict_only_heuristic_rule(&finding.rule_id) {
        return FindingVisibilityDecision::hidden(
            FindingIntent::Maintainability,
            "broad maintainability heuristics are strict-mode suggestions",
        );
    }

    if is_high_priority(finding.risk.priority) && finding.confidence != Confidence::Low {
        return FindingVisibilityDecision::visible(
            FindingIntent::Maintainability,
            "high-priority maintainability risk",
        );
    }

    FindingVisibilityDecision::hidden(
        FindingIntent::Maintainability,
        "maintainability signal is below the default priority threshold",
    )
}

fn classify_intent(finding: &Finding) -> FindingIntent {
    if finding.category == FindingCategory::Testing {
        return FindingIntent::TestingGap;
    }

    if finding.category == FindingCategory::Security {
        return FindingIntent::SecurityRisk;
    }

    if is_runtime_rule(&finding.rule_id) {
        return FindingIntent::RuntimeRisk;
    }

    if is_import_graph_rule(&finding.rule_id) {
        return FindingIntent::ActionableRisk;
    }

    if is_maintainability_rule(&finding.rule_id) {
        return FindingIntent::Maintainability;
    }

    if finding.severity >= Severity::High {
        return FindingIntent::ActionableRisk;
    }

    FindingIntent::Informational
}

fn is_runtime_rule(rule_id: &str) -> bool {
    matches!(
        rule_id,
        "language.rust.panic-risk"
            | "language.javascript.runtime-exit-risk"
            | "language.go.panic-exit-risk"
            | "language.python.exception-risk"
            | "language.jvm.exception-risk"
            | "language.csharp.exception-risk"
    )
}

fn is_maintainability_rule(rule_id: &str) -> bool {
    matches!(
        rule_id,
        "testing.source-without-test"
            | "code-marker.todo"
            | "architecture.deep-nesting"
            | "architecture.too-many-modules"
            | "architecture.large-file"
            | "architecture.barrel-file-risk"
            | "architecture.deep-relative-imports"
            | "code-quality.long-function"
            | "code-quality.complex-file"
            | "code-quality.cyclomatic-complexity"
    )
}

fn is_import_graph_rule(rule_id: &str) -> bool {
    matches!(
        rule_id,
        "architecture.circular-dependency"
            | "architecture.excessive-fan-out"
            | "architecture.high-instability-hub"
    )
}

fn is_strict_only_heuristic_rule(rule_id: &str) -> bool {
    matches!(
        rule_id,
        "testing.source-without-test"
            | "testing.missing-test-folder"
            | "code-marker.todo"
            | "code-marker.fixme"
            | "code-marker.hack"
            | "architecture.deep-nesting"
            | "architecture.too-many-modules"
            | "architecture.large-file"
            | "architecture.barrel-file-risk"
            | "architecture.deep-relative-imports"
            | "code-quality.long-function"
            | "code-quality.complex-file"
            | "code-quality.cyclomatic-complexity"
    )
}

fn is_validated_secret_leak(finding: &Finding) -> bool {
    matches!(
        finding.rule_id.as_str(),
        "security.secret-candidate" | "security.private-key-candidate"
    ) && finding.severity >= Severity::High
        && finding.confidence != Confidence::Low
}

fn is_high_priority(priority: RiskPriority) -> bool {
    matches!(priority, RiskPriority::P0 | RiskPriority::P1)
}

fn is_script_boundary_runtime_exit(finding: &Finding) -> bool {
    if finding.rule_id != "language.javascript.runtime-exit-risk" {
        return false;
    }

    let Some(evidence) = finding.evidence.first() else {
        return false;
    };

    is_script_or_tooling_path(&evidence.path)
}

fn is_script_or_tooling_path(path: &Path) -> bool {
    let path_text = path.to_string_lossy().replace('\\', "/").to_lowercase();

    if path_text.contains("/src/") || path_text.starts_with("src/") {
        return false;
    }

    path_text.contains("/scripts/")
        || path_text.starts_with("scripts/")
        || path_text.contains("/tools/")
        || path_text.starts_with("tools/")
        || path_text.contains("/bin/")
        || path_text.starts_with("bin/")
        || path_text.contains("/ci/")
        || path_text.starts_with("ci/")
        || path_text.contains("/.github/")
        || path_text.starts_with(".github/")
        || path_text.contains("guard")
        || path_text.contains("check")
        || path_text.contains("lint")
        || path_text.contains("verify")
}