frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use std::path::Path;

use crate::domain::PathClass;
use crate::path_class::classify_repository_path;

use super::HybridSourceClass;
use super::support::{
    hybrid_source_class, is_bench_support_path, is_example_support_path, is_test_support_path,
};
use super::tokens::hybrid_identifier_tokens;

pub(in crate::searcher) fn is_python_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    matches!(
        normalized.as_str(),
        "__main__.py" | "main.py" | "app.py" | "manage.py" | "cli.py" | "run.py"
    ) || [
        "/__main__.py",
        "/main.py",
        "/app.py",
        "/manage.py",
        "/cli.py",
        "/run.py",
    ]
    .iter()
    .any(|suffix| normalized.ends_with(suffix))
}

pub(in crate::searcher) fn is_lua_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if !normalized.ends_with(".lua") || is_test_support_path(&normalized) {
        return false;
    }

    let candidate = Path::new(&normalized);
    let Some(stem) = candidate
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(|stem| stem.trim().to_ascii_lowercase())
    else {
        return false;
    };
    let parts = candidate
        .components()
        .filter_map(|component| component.as_os_str().to_str())
        .collect::<Vec<_>>();
    let is_repo_root_file = parts.len() == 1;
    if is_repo_root_file {
        return matches!(
            stem.as_str(),
            "main" | "init" | "app" | "bootstrap" | "cli" | "run" | "server"
        );
    }

    let has_loader_root = parts
        .iter()
        .take(parts.len().saturating_sub(1))
        .any(|part| matches!(*part, "bin" | "cli" | "cmd" | "lua" | "script" | "scripts"));
    if !has_loader_root {
        return false;
    }

    let has_cli_context = parts
        .iter()
        .any(|part| matches!(*part, "cli" | "command" | "commands"));
    if has_cli_context {
        return true;
    }

    let has_runtime_context = parts
        .iter()
        .any(|part| matches!(*part, "daemon" | "server" | "service" | "worker"));
    has_runtime_context
        && matches!(
            stem.as_str(),
            "bootstrap" | "cli" | "daemon" | "init" | "main" | "run" | "server" | "service"
        )
}

pub(in crate::searcher) fn is_kotlin_android_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if is_test_support_path(&normalized) {
        return false;
    }

    let candidate = Path::new(path.trim_start_matches("./"));
    let Some(extension) = candidate.extension().and_then(|ext| ext.to_str()) else {
        return false;
    };
    if !matches!(extension.to_ascii_lowercase().as_str(), "java" | "kt") {
        return false;
    }

    if !(normalized.starts_with("src/main/") || normalized.contains("/src/main/")) {
        return false;
    }

    let Some(stem) = candidate.file_stem().and_then(|stem| stem.to_str()) else {
        return false;
    };
    let stem_tokens = hybrid_identifier_tokens(stem);
    if stem_tokens.is_empty() {
        return false;
    }

    stem_tokens
        .iter()
        .any(|token| matches!(token.as_str(), "activity" | "application" | "navigation"))
        || stem_tokens
            .windows(2)
            .any(|window| matches!(window, [first, second] if first == "nav" && second == "graph"))
}

pub(in crate::searcher) fn is_kotlin_android_ui_runtime_surface_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if is_test_support_path(&normalized) {
        return false;
    }

    let candidate = Path::new(path.trim_start_matches("./"));
    let Some(extension) = candidate.extension().and_then(|ext| ext.to_str()) else {
        return false;
    };
    if !matches!(extension.to_ascii_lowercase().as_str(), "java" | "kt") {
        return false;
    }

    if !(normalized.starts_with("src/main/") || normalized.contains("/src/main/")) {
        return false;
    }

    let Some(stem) = candidate.file_stem().and_then(|stem| stem.to_str()) else {
        return false;
    };
    let stem_tokens = hybrid_identifier_tokens(stem);
    if stem_tokens.is_empty() {
        return false;
    }

    stem_tokens.last().is_some_and(|token| token == "screen")
        || stem_tokens.len() >= 2
            && matches!(
                stem_tokens[stem_tokens.len() - 2..],
                [ref first, ref second] if first == "view" && second == "model"
            )
}

pub(in crate::searcher) fn is_typescript_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if is_test_support_path(&normalized) {
        return false;
    }

    let candidate = Path::new(&normalized);
    let Some(extension) = candidate.extension().and_then(|ext| ext.to_str()) else {
        return false;
    };
    if !matches!(
        extension,
        "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" | "mts" | "cts"
    ) {
        return false;
    }

    let Some(stem) = candidate.file_stem().and_then(|stem| stem.to_str()) else {
        return false;
    };
    let stem = stem.trim().to_ascii_lowercase();
    if matches!(
        stem.as_str(),
        "main" | "server" | "cli" | "app" | "bootstrap"
    ) {
        return true;
    }

    let looks_like_runtime_tree =
        matches!(classify_repository_path(&normalized), PathClass::Runtime)
            || normalized.starts_with("app/")
            || normalized.contains("/app/")
            || normalized.contains("/lib/");
    if !looks_like_runtime_tree {
        return false;
    }

    let Some(parent_name) = candidate
        .parent()
        .and_then(|parent| parent.file_name())
        .and_then(|name| name.to_str())
    else {
        return false;
    };
    if parent_name != "src" {
        return false;
    }
    if stem != "index" {
        return false;
    }

    hybrid_identifier_tokens(&normalized)
        .into_iter()
        .any(|token| {
            matches!(
                token.as_str(),
                "app" | "bootstrap" | "cli" | "daemon" | "server" | "service" | "worker"
            )
        })
}

pub(in crate::searcher) fn is_typescript_runtime_module_index_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if is_test_support_path(&normalized)
        || is_example_support_path(&normalized)
        || is_bench_support_path(&normalized)
    {
        return false;
    }

    let candidate = Path::new(&normalized);
    let Some(extension) = candidate.extension().and_then(|ext| ext.to_str()) else {
        return false;
    };
    if !matches!(
        extension,
        "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" | "mts" | "cts"
    ) {
        return false;
    }

    let looks_like_runtime_tree =
        matches!(classify_repository_path(&normalized), PathClass::Runtime)
            || normalized.starts_with("app/")
            || normalized.contains("/app/")
            || normalized.contains("/lib/");
    if !looks_like_runtime_tree {
        return false;
    }

    let Some(stem) = candidate.file_stem().and_then(|stem| stem.to_str()) else {
        return false;
    };
    if !stem.eq_ignore_ascii_case("index") {
        return false;
    }

    let path_tokens = hybrid_identifier_tokens(&normalized);
    if path_tokens.iter().any(|token| {
        matches!(
            token.as_str(),
            "bench"
                | "benches"
                | "config"
                | "configs"
                | "fixture"
                | "fixtures"
                | "mock"
                | "mocks"
                | "spec"
                | "specs"
                | "stories"
                | "story"
                | "test"
                | "tests"
        )
    }) {
        return false;
    }

    true
}

pub(in crate::searcher) fn is_go_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if !normalized.ends_with(".go") || normalized.ends_with("_test.go") {
        return false;
    }
    if is_test_support_path(&normalized) {
        return false;
    }

    if normalized == "main.go" || normalized.ends_with("/main.go") {
        return true;
    }

    (normalized.starts_with("cmd/") || normalized.contains("/cmd/"))
        && Path::new(&normalized)
            .file_name()
            .and_then(|name| name.to_str())
            .is_some_and(|name| name.eq_ignore_ascii_case("root.go"))
}

pub(in crate::searcher) fn is_roc_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    if !normalized.ends_with(".roc") || is_test_support_path(&normalized) {
        return false;
    }

    normalized == "main.roc"
        || normalized == "platform/main.roc"
        || normalized.ends_with("/platform/main.roc")
}

pub(in crate::searcher) fn is_navigation_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    let path_class = classify_repository_path(normalized);
    if !matches!(path_class, PathClass::Runtime | PathClass::Support) {
        return false;
    }

    let Some(stem) = Path::new(normalized)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(|stem| stem.to_ascii_lowercase())
    else {
        return false;
    };

    hybrid_identifier_tokens(&stem).into_iter().any(|token| {
        matches!(
            token.as_str(),
            "api"
                | "client"
                | "discoverer"
                | "handler"
                | "handlers"
                | "protocol"
                | "route"
                | "router"
                | "routes"
                | "server"
                | "transport"
        )
    })
}

pub(in crate::searcher) fn is_navigation_reference_doc_path(path: &str) -> bool {
    matches!(
        hybrid_source_class(path),
        HybridSourceClass::Documentation | HybridSourceClass::Readme | HybridSourceClass::Specs
    )
}

pub(in crate::searcher) fn is_entrypoint_runtime_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    matches!(normalized, "src/main.rs" | "src/lib.rs")
        || normalized.ends_with("/src/main.rs")
        || normalized.ends_with("/src/lib.rs")
        || is_go_entrypoint_runtime_path(normalized)
        || is_kotlin_android_entrypoint_runtime_path(normalized)
        || is_roc_entrypoint_runtime_path(normalized)
        || is_lua_entrypoint_runtime_path(normalized)
        || is_python_entrypoint_runtime_path(normalized)
        || is_typescript_entrypoint_runtime_path(normalized)
}

pub(in crate::searcher) fn is_entrypoint_reference_doc_path(path: &str) -> bool {
    path.starts_with("specs/")
        || matches!(hybrid_source_class(path), HybridSourceClass::Documentation)
}

pub(in crate::searcher) fn is_ci_workflow_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with(".github/workflows/")
        && matches!(
            Path::new(normalized)
                .extension()
                .and_then(|ext| ext.to_str()),
            Some("yml" | "yaml")
        )
}

pub(in crate::searcher) fn is_entrypoint_build_workflow_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    if !is_ci_workflow_path(normalized) {
        return false;
    }

    let Some(stem) = Path::new(normalized)
        .file_stem()
        .and_then(|stem| stem.to_str())
    else {
        return false;
    };

    hybrid_identifier_tokens(stem).into_iter().any(|token| {
        matches!(
            token.as_str(),
            "build" | "bundle" | "deploy" | "pages" | "publish" | "release"
        )
    })
}

pub(in crate::searcher) fn is_scripts_ops_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    normalized == "justfile"
        || normalized == "makefile"
        || normalized.starts_with("scripts/")
        || normalized.starts_with("xtask/")
        || normalized.contains("/scripts/")
}