rustcodegraph 1.2.10

Local-first code intelligence graph and CLI for AI agents
Documentation
use std::collections::HashSet;

use rustcodegraph::types::{SearchResult, Subgraph};

use super::types::EvalResult;

const PASS_THRESHOLD: f64 = 0.5;

pub(super) fn score_search_nodes(
    case_id: &str,
    expected_symbols: &[&str],
    results: &[SearchResult],
    latency_ms: f64,
) -> EvalResult {
    let expected_lower = expected_symbols
        .iter()
        .map(|symbol| symbol.to_ascii_lowercase())
        .collect::<Vec<_>>();
    let result_names = results
        .iter()
        .map(|result| result.node.name.to_ascii_lowercase())
        .collect::<Vec<_>>();

    let mut found = Vec::new();
    let mut missed = Vec::new();
    let mut first_rank = 0usize;

    for (i, expected) in expected_lower.iter().enumerate() {
        if let Some(idx) = result_names.iter().position(|name| name == expected) {
            found.push(expected_symbols[i].to_owned());
            if first_rank == 0 {
                first_rank = idx + 1;
            }
        } else {
            missed.push(expected_symbols[i].to_owned());
        }
    }

    let recall = if expected_symbols.is_empty() {
        0.0
    } else {
        found.len() as f64 / expected_symbols.len() as f64
    };
    let mrr = if first_rank > 0 {
        1.0 / first_rank as f64
    } else {
        0.0
    };

    EvalResult {
        case_id: case_id.to_owned(),
        pass: recall >= PASS_THRESHOLD,
        recall,
        mrr,
        found_symbols: found,
        missed_symbols: missed,
        node_count: None,
        edge_count: None,
        edge_density: None,
        latency_ms,
    }
}

pub(super) fn score_find_relevant_context(
    case_id: &str,
    expected_symbols: &[&str],
    subgraph: &Subgraph,
    latency_ms: f64,
) -> EvalResult {
    let node_names = subgraph
        .nodes
        .values()
        .map(|node| node.name.to_ascii_lowercase())
        .collect::<HashSet<_>>();

    let mut found = Vec::new();
    let mut missed = Vec::new();

    for symbol in expected_symbols {
        if node_names.contains(&symbol.to_ascii_lowercase()) {
            found.push((*symbol).to_owned());
        } else {
            missed.push((*symbol).to_owned());
        }
    }

    let recall = if expected_symbols.is_empty() {
        0.0
    } else {
        found.len() as f64 / expected_symbols.len() as f64
    };
    let node_count = subgraph.nodes.len();
    let edge_count = subgraph.edges.len();
    let edge_density = if node_count > 0 {
        edge_count as f64 / node_count as f64
    } else {
        0.0
    };

    EvalResult {
        case_id: case_id.to_owned(),
        pass: recall >= PASS_THRESHOLD,
        recall,
        mrr: 0.0,
        found_symbols: found,
        missed_symbols: missed,
        node_count: Some(node_count),
        edge_count: Some(edge_count),
        edge_density: Some(edge_density),
        latency_ms,
    }
}