frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use super::{
    HybridPlaybookWitnessOutcome, HybridWitnessGroup, HybridWitnessMatchBy, HybridWitnessMatchMode,
    HybridWitnessRequirement,
};

fn path_matches_prefix(candidate: &str, prefix: &str) -> bool {
    let normalized_candidate = candidate.trim().trim_matches('/');
    let normalized_prefix = prefix.trim().trim_matches('/');
    if normalized_candidate.is_empty() || normalized_prefix.is_empty() {
        return false;
    }
    normalized_candidate == normalized_prefix
        || normalized_candidate
            .strip_prefix(normalized_prefix)
            .is_some_and(|suffix| suffix.starts_with('/'))
}

fn witness_group_match(
    group: &HybridWitnessGroup,
    matched_paths: &[String],
) -> (HybridWitnessMatchBy, Option<String>, bool) {
    if let Some(path) = group
        .match_any
        .iter()
        .find_map(|expected| {
            matched_paths
                .iter()
                .find(|candidate| *candidate == expected)
        })
        .cloned()
    {
        return (HybridWitnessMatchBy::Exact, Some(path), true);
    }

    if group.match_mode == HybridWitnessMatchMode::ExactOrPrefix {
        if let Some(path) = group
            .accepted_prefixes
            .iter()
            .find_map(|prefix| {
                matched_paths
                    .iter()
                    .find(|candidate| path_matches_prefix(candidate, prefix))
            })
            .cloned()
        {
            return (HybridWitnessMatchBy::Prefix, Some(path), false);
        }
    }

    (HybridWitnessMatchBy::None, None, false)
}

pub(super) fn witness_outcomes(
    groups: &[HybridWitnessGroup],
    matched_paths: &[String],
    semantic_status_ok: bool,
    target_only: bool,
) -> Vec<HybridPlaybookWitnessOutcome> {
    groups
        .iter()
        .filter(|group| {
            !target_only
                || matches!(group.required_when, HybridWitnessRequirement::Always)
                || semantic_status_ok
        })
        .map(|group| {
            let required = if target_only {
                true
            } else {
                match group.required_when {
                    HybridWitnessRequirement::Always => true,
                    HybridWitnessRequirement::SemanticOk => semantic_status_ok,
                }
            };
            let (matched_by, matched_path, exact_matched) =
                witness_group_match(group, matched_paths);
            let passed = !required || exact_matched;
            HybridPlaybookWitnessOutcome {
                group_id: group.group_id.clone(),
                match_any: group.match_any.clone(),
                match_mode: group.match_mode,
                accepted_prefixes: group.accepted_prefixes.clone(),
                required_when: group.required_when,
                matched_by,
                matched_path,
                passed,
            }
        })
        .collect()
}

pub(super) fn semantic_status_allowed(allowed_statuses: &[String], semantic_status: &str) -> bool {
    if allowed_statuses.is_empty() {
        return true;
    }

    let semantic_status = semantic_status.trim().to_ascii_lowercase();
    if allowed_statuses
        .iter()
        .any(|status| status.trim().eq_ignore_ascii_case(&semantic_status))
    {
        return true;
    }

    semantic_status == "unavailable"
        && allowed_statuses
            .iter()
            .any(|status| status.trim().eq_ignore_ascii_case("disabled"))
}