repopilot 0.20.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::helpers::{normalize, path_contains_component};
use crate::audits::context::model::LanguageKind;
use std::path::Path;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextSignals {
    pub path: Vec<ContextSignal>,
    pub language: Vec<ContextSignal>,
    pub content: Vec<ContextSignal>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContextSignal {
    pub id: &'static str,
    pub confidence: SignalConfidence,
    pub reason: &'static str,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalConfidence {
    Medium,
    High,
}

impl ContextSignals {
    pub fn detect(path: &Path, language: LanguageKind, content: &str) -> Self {
        let mut signals = Self {
            path: Vec::new(),
            language: Vec::new(),
            content: Vec::new(),
        };

        if is_infrastructure_path(path) {
            if is_known_infrastructure_filename(path) {
                signals.path.push(ContextSignal {
                    id: "path.infrastructure-filename",
                    confidence: SignalConfidence::High,
                    reason: "file name is a known infrastructure orchestration file",
                });
            }

            if has_infrastructure_component(path) {
                signals.path.push(ContextSignal {
                    id: "path.infrastructure-component",
                    confidence: SignalConfidence::High,
                    reason: "path contains a known infrastructure directory component",
                });
            }
        }

        if is_declarative_language(language) {
            signals.language.push(ContextSignal {
                id: "language.declarative",
                confidence: SignalConfidence::High,
                reason: "language is primarily declarative or data-description oriented",
            });
        }

        if is_infrastructure_language(language) {
            signals.language.push(ContextSignal {
                id: "language.infrastructure",
                confidence: SignalConfidence::High,
                reason: "language is specific to infrastructure orchestration",
            });
        }

        if is_functional_first_language(language) {
            signals.language.push(ContextSignal {
                id: "language.functional-first",
                confidence: SignalConfidence::High,
                reason: "language is commonly used with functional-first idioms",
            });
        }

        if content.contains("apiVersion:")
            && (content.contains("kind: Deployment")
                || content.contains("kind: Service")
                || content.contains("kind: ConfigMap"))
        {
            signals.content.push(ContextSignal {
                id: "content.kubernetes-manifest",
                confidence: SignalConfidence::Medium,
                reason: "content resembles a Kubernetes manifest",
            });
        }

        signals
    }

    pub fn has(&self, id: &str) -> bool {
        self.path
            .iter()
            .chain(self.language.iter())
            .chain(self.content.iter())
            .any(|signal| signal.id == id)
    }

    pub fn is_declarative_context(&self) -> bool {
        self.has("language.declarative")
    }

    pub fn is_functional_first_language(&self) -> bool {
        self.has("language.functional-first")
    }

    pub fn is_infrastructure_context(&self) -> bool {
        self.has("language.infrastructure")
            || self.has("path.infrastructure-filename")
            || self.has("path.infrastructure-component")
            || self.has("content.kubernetes-manifest")
    }
}

const INFRASTRUCTURE_COMPONENTS: &[&str] = &[
    "infra",
    "infrastructure",
    "terraform",
    "k8s",
    "kubernetes",
    "helm",
    ".github",
    "workflows",
];

const INFRASTRUCTURE_FILENAMES: &[&str] = &[
    "docker-compose.yml",
    "docker-compose.yaml",
    "compose.yml",
    "compose.yaml",
    "kustomization.yml",
    "kustomization.yaml",
    "helmfile.yml",
    "helmfile.yaml",
];

pub fn is_functional_first_language(language: LanguageKind) -> bool {
    matches!(
        language,
        LanguageKind::Elixir
            | LanguageKind::Erlang
            | LanguageKind::Haskell
            | LanguageKind::OCaml
            | LanguageKind::FSharp
            | LanguageKind::Scala
    )
}

pub fn is_declarative_language(language: LanguageKind) -> bool {
    matches!(
        language,
        LanguageKind::Terraform
            | LanguageKind::Dockerfile
            | LanguageKind::Nix
            | LanguageKind::Yaml
            | LanguageKind::Toml
            | LanguageKind::Json
            | LanguageKind::Sql
            | LanguageKind::Html
            | LanguageKind::Css
            | LanguageKind::Scss
            | LanguageKind::Markdown
    )
}

pub fn is_infrastructure_language(language: LanguageKind) -> bool {
    matches!(
        language,
        LanguageKind::Terraform | LanguageKind::Dockerfile | LanguageKind::Nix
    )
}

fn is_infrastructure_path(path: &Path) -> bool {
    is_known_infrastructure_filename(path) || has_infrastructure_component(path)
}

fn is_known_infrastructure_filename(path: &Path) -> bool {
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .map(normalize)
        .unwrap_or_default();

    INFRASTRUCTURE_FILENAMES
        .iter()
        .any(|candidate| file_name == *candidate)
}

fn has_infrastructure_component(path: &Path) -> bool {
    path_contains_component(path, INFRASTRUCTURE_COMPONENTS)
}

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

    #[test]
    fn detects_infrastructure_filenames() {
        assert!(is_infrastructure_path(Path::new("docker-compose.yml")));
        assert!(is_infrastructure_path(Path::new("deploy/helmfile.yaml")));
        assert!(is_infrastructure_path(Path::new(
            ".github/workflows/ci.yml"
        )));
    }

    #[test]
    fn separates_declarative_from_infrastructure_languages() {
        assert!(is_declarative_language(LanguageKind::Json));
        assert!(is_declarative_language(LanguageKind::Yaml));
        assert!(is_declarative_language(LanguageKind::Toml));
        assert!(is_declarative_language(LanguageKind::Markdown));
        assert!(!is_infrastructure_language(LanguageKind::Json));
        assert!(!is_infrastructure_language(LanguageKind::Yaml));
        assert!(!is_infrastructure_language(LanguageKind::Toml));
        assert!(!is_infrastructure_language(LanguageKind::Markdown));
        assert!(is_infrastructure_language(LanguageKind::Terraform));
    }

    #[test]
    fn detects_functional_first_languages() {
        assert!(is_functional_first_language(LanguageKind::Elixir));
        assert!(is_functional_first_language(LanguageKind::Haskell));
        assert!(!is_functional_first_language(LanguageKind::Rust));
    }

    #[test]
    fn context_signals_explain_infrastructure_context() {
        let signals = ContextSignals::detect(
            Path::new("deploy/k8s/app.yml"),
            LanguageKind::Yaml,
            "apiVersion: apps/v1\nkind: Deployment\n",
        );

        assert!(signals.is_declarative_context());
        assert!(signals.is_infrastructure_context());
        assert!(signals.has("path.infrastructure-component"));
        assert!(signals.has("content.kubernetes-manifest"));
    }
}