frigg 0.4.2

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

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) enum LaravelUiSurfaceClass {
    BladeView,
    LivewireComponent,
    LivewireView,
    BladeComponent,
}

pub(super) fn is_laravel_livewire_component_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    (normalized.starts_with("app/Livewire/") || normalized.starts_with("app/Http/Livewire/"))
        && normalized.ends_with(".php")
}

pub(super) fn is_laravel_command_or_middleware_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    (normalized.starts_with("app/Console/Commands/")
        || normalized.starts_with("app/Http/Middleware/"))
        && normalized.ends_with(".php")
}

pub(super) fn is_laravel_job_or_listener_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    (normalized.starts_with("app/Jobs/")
        || normalized.starts_with("app/Listeners/")
        || normalized.starts_with("app/Events/")
        || normalized.starts_with("app/Mail/"))
        && normalized.ends_with(".php")
}

pub(super) fn is_laravel_view_component_class_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("app/View/Components/") && normalized.ends_with(".php")
}

pub(super) fn is_laravel_provider_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("app/Providers/") && normalized.ends_with(".php")
}

pub(super) fn is_laravel_core_provider_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    matches!(
        Path::new(&normalized)
            .file_name()
            .and_then(|name| name.to_str()),
        Some(
            "appserviceprovider.php"
                | "authserviceprovider.php"
                | "broadcastserviceprovider.php"
                | "configurationserviceprovider.php"
                | "duskserviceprovider.php"
                | "eventserviceprovider.php"
        )
    )
}

pub(super) fn is_laravel_route_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("routes/") && normalized.ends_with(".php")
}

pub(super) fn is_laravel_bootstrap_entrypoint_path(path: &str) -> bool {
    path.trim_start_matches("./") == "bootstrap/app.php"
}

pub(super) fn is_laravel_blade_view_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("resources/views/") && normalized.ends_with(".blade.php")
}

pub(super) fn is_laravel_layout_blade_view_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    normalized.starts_with("resources/views/")
        && normalized.ends_with(".blade.php")
        && (normalized.starts_with("resources/views/layouts/")
            || matches!(
                Path::new(&normalized)
                    .file_name()
                    .and_then(|name| name.to_str()),
                Some("layout.blade.php")
            ))
}

pub(super) fn is_laravel_livewire_view_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("resources/views/livewire/") && normalized.ends_with(".blade.php")
}

pub(super) fn is_laravel_non_livewire_blade_view_path(path: &str) -> bool {
    is_laravel_blade_view_path(path)
        && !is_laravel_livewire_view_path(path)
        && !is_laravel_blade_component_path(path)
}

pub(super) fn is_laravel_blade_component_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("resources/views/")
        && normalized.ends_with(".blade.php")
        && normalized
            .split('/')
            .any(|component| component.eq_ignore_ascii_case("components"))
}

pub(super) fn is_laravel_root_blade_component_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./");
    normalized.starts_with("resources/views/components/") && normalized.ends_with(".blade.php")
}

pub(super) fn is_laravel_nested_blade_component_path(path: &str) -> bool {
    is_laravel_blade_component_path(path) && !is_laravel_root_blade_component_path(path)
}

pub(super) fn is_laravel_form_action_blade_path(path: &str) -> bool {
    let normalized = path.trim_start_matches("./").to_ascii_lowercase();
    normalized.starts_with("resources/views/")
        && normalized.ends_with(".blade.php")
        && (normalized.contains("/forms/")
            || normalized.contains("/partials/")
            || normalized.contains("/modals/"))
}

pub(super) fn laravel_ui_surface_class(path: &str) -> Option<LaravelUiSurfaceClass> {
    if is_laravel_non_livewire_blade_view_path(path) {
        Some(LaravelUiSurfaceClass::BladeView)
    } else if is_laravel_livewire_component_path(path) {
        Some(LaravelUiSurfaceClass::LivewireComponent)
    } else if is_laravel_livewire_view_path(path) {
        Some(LaravelUiSurfaceClass::LivewireView)
    } else if is_laravel_blade_component_path(path) {
        Some(LaravelUiSurfaceClass::BladeComponent)
    } else {
        None
    }
}

pub(super) fn laravel_ui_surface_novelty_bonus(
    surface: LaravelUiSurfaceClass,
    prefer_blade_components: bool,
) -> f32 {
    if prefer_blade_components {
        return match surface {
            LaravelUiSurfaceClass::BladeComponent => 0.86,
            LaravelUiSurfaceClass::BladeView => 0.24,
            LaravelUiSurfaceClass::LivewireView => 0.14,
            LaravelUiSurfaceClass::LivewireComponent => 0.10,
        };
    }

    match surface {
        LaravelUiSurfaceClass::BladeView => 0.80,
        LaravelUiSurfaceClass::LivewireView => 0.72,
        LaravelUiSurfaceClass::LivewireComponent => 0.22,
        LaravelUiSurfaceClass::BladeComponent => 0.10,
    }
}

pub(super) fn laravel_ui_surface_repeat_penalty(
    surface: LaravelUiSurfaceClass,
    prefer_blade_components: bool,
) -> f32 {
    if prefer_blade_components {
        return match surface {
            LaravelUiSurfaceClass::BladeComponent => 0.18,
            LaravelUiSurfaceClass::BladeView => 0.42,
            LaravelUiSurfaceClass::LivewireView => 0.36,
            LaravelUiSurfaceClass::LivewireComponent => 0.26,
        };
    }

    match surface {
        LaravelUiSurfaceClass::BladeView => 0.10,
        LaravelUiSurfaceClass::LivewireView => 0.14,
        LaravelUiSurfaceClass::LivewireComponent => 0.22,
        LaravelUiSurfaceClass::BladeComponent => 0.58,
    }
}