remem-ai 0.4.4

Persistent memory for Claude Code — single binary, zero subprocesses
Documentation
use std::collections::{BTreeMap, HashSet};

use anyhow::{anyhow, Context, Result};
use rusqlite::Connection;

use super::types::{
    CategoryEvaluation, EvidenceRef, GoldenDataset, GoldenEvalReport, GoldenQuery, MetricSums,
    QueryEvaluation, QueryMetrics, QueryStatus,
};

const RANK_K: usize = 10;

#[derive(Default)]
struct CategoryAccumulator {
    total_queries: usize,
    abstention_queries: usize,
    abstention_passed: usize,
    metrics: MetricSums,
}

pub fn load_dataset(dataset_path: &str) -> Result<GoldenDataset> {
    let content = std::fs::read_to_string(dataset_path)
        .with_context(|| format!("read golden eval dataset {dataset_path}"))?;
    let dataset: GoldenDataset = serde_json::from_str(&content)
        .with_context(|| format!("parse golden eval dataset {dataset_path}"))?;
    validate_dataset(&dataset)?;
    Ok(dataset)
}

pub fn run_dataset_path(
    conn: &Connection,
    dataset_path: &str,
    k: usize,
) -> Result<GoldenEvalReport> {
    let dataset = load_dataset(dataset_path)?;
    evaluate_dataset(conn, &dataset, k)
}

pub fn evaluate_dataset(
    conn: &Connection,
    dataset: &GoldenDataset,
    k: usize,
) -> Result<GoldenEvalReport> {
    validate_dataset(dataset)?;
    let k = k.max(1);
    let fetch_limit = k.max(RANK_K) as i64;
    let mut query_reports = Vec::with_capacity(dataset.queries.len());
    let mut overall_sums = MetricSums::default();
    let mut categories = BTreeMap::<String, CategoryAccumulator>::new();
    let mut skipped_queries = 0usize;
    let mut abstention_queries = 0usize;
    let mut abstention_passed = 0usize;

    for query in &dataset.queries {
        let results = crate::retrieval::search::search_with_branch(
            conn,
            Some(&query.query),
            query.project.as_deref(),
            query.memory_type.as_deref(),
            fetch_limit,
            0,
            false,
            query.branch.as_deref(),
        )?;
        let evaluation = evaluate_query(query, &results, k);
        let category = categories.entry(query.category.clone()).or_default();
        category.total_queries += 1;

        if query.expects_abstention() {
            abstention_queries += 1;
            category.abstention_queries += 1;
            if evaluation.status == QueryStatus::Pass {
                abstention_passed += 1;
                category.abstention_passed += 1;
            }
        } else if let Some(metrics) = evaluation.metrics.as_ref() {
            overall_sums.add(metrics);
            category.metrics.add(metrics);
        } else {
            skipped_queries += 1;
        }

        query_reports.push(evaluation);
    }

    Ok(GoldenEvalReport {
        version: dataset.version.clone(),
        description: dataset.description.clone(),
        k,
        rank_k: RANK_K,
        total_queries: dataset.queries.len(),
        scored_queries: overall_sums.averages().map_or(0, |metrics| metrics.count),
        skipped_queries,
        abstention_queries,
        abstention_passed,
        overall: overall_sums.averages(),
        by_category: categories
            .into_iter()
            .map(|(name, category)| {
                (
                    name,
                    CategoryEvaluation {
                        total_queries: category.total_queries,
                        scored_queries: category.metrics.averages().map_or(0, |m| m.count),
                        abstention_queries: category.abstention_queries,
                        abstention_passed: category.abstention_passed,
                        metrics: category.metrics.averages(),
                    },
                )
            })
            .collect(),
        queries: query_reports,
    })
}

fn validate_dataset(dataset: &GoldenDataset) -> Result<()> {
    if dataset.queries.is_empty() {
        return Err(anyhow!(
            "golden eval dataset must contain at least one query"
        ));
    }
    let mut seen_ids = HashSet::new();
    for query in &dataset.queries {
        if query.id.trim().is_empty() {
            return Err(anyhow!("golden eval query id must not be empty"));
        }
        if !seen_ids.insert(query.id.as_str()) {
            return Err(anyhow!("duplicate golden eval query id {}", query.id));
        }
        if query.query.trim().is_empty() {
            return Err(anyhow!(
                "golden eval query {} text must not be empty",
                query.id
            ));
        }
        if query.category.trim().is_empty() {
            return Err(anyhow!(
                "golden eval query {} category must not be empty",
                query.id
            ));
        }
        for evidence_ref in &query.evidence_refs {
            if !evidence_ref.has_match_criteria() {
                return Err(anyhow!(
                    "golden eval query {} contains an empty evidence ref",
                    query.id
                ));
            }
        }
    }
    Ok(())
}

fn evaluate_query(
    query: &GoldenQuery,
    results: &[crate::memory::Memory],
    k: usize,
) -> QueryEvaluation {
    let expected_refs = query.expected_refs();
    if query.expects_abstention() {
        return QueryEvaluation {
            id: query.id.clone(),
            query: query.query.clone(),
            category: query.category.clone(),
            status: if results.is_empty() {
                QueryStatus::Pass
            } else {
                QueryStatus::Fail
            },
            result_count: results.len(),
            matched_refs: 0,
            expected_refs: expected_refs.len(),
            metrics: None,
        };
    }

    if expected_refs.is_empty() {
        return QueryEvaluation {
            id: query.id.clone(),
            query: query.query.clone(),
            category: query.category.clone(),
            status: QueryStatus::Skip,
            result_count: results.len(),
            matched_refs: 0,
            expected_refs: 0,
            metrics: None,
        };
    }

    let metrics = score_results(results, &expected_refs, k);
    let matched_refs = matched_ref_indexes(results, &expected_refs, k).len();
    let status = if metrics.hit_at_k > 0.0 {
        QueryStatus::Hit
    } else {
        QueryStatus::Miss
    };
    QueryEvaluation {
        id: query.id.clone(),
        query: query.query.clone(),
        category: query.category.clone(),
        status,
        result_count: results.len(),
        matched_refs,
        expected_refs: expected_refs.len(),
        metrics: Some(metrics),
    }
}

fn score_results(
    results: &[crate::memory::Memory],
    expected_refs: &[EvidenceRef],
    k: usize,
) -> QueryMetrics {
    let top_k = k.min(results.len());
    let top_rank_k = RANK_K.min(results.len());
    let relevance_at_k: Vec<bool> = results
        .iter()
        .take(top_k)
        .map(|memory| {
            expected_refs
                .iter()
                .any(|evidence_ref| evidence_ref.matches(memory))
        })
        .collect();
    let relevance_at_rank_k: Vec<bool> = results
        .iter()
        .take(top_rank_k)
        .map(|memory| {
            expected_refs
                .iter()
                .any(|evidence_ref| evidence_ref.matches(memory))
        })
        .collect();
    let matched_refs = matched_ref_indexes(results, expected_refs, k).len();
    let relevant_hits = relevance_at_k
        .iter()
        .filter(|is_relevant| **is_relevant)
        .count();
    let precision_denominator = top_k.max(1);

    QueryMetrics {
        hit_at_k: if relevant_hits > 0 { 1.0 } else { 0.0 },
        mrr_at_10: reciprocal_rank_from_relevance(&relevance_at_rank_k),
        precision_at_k: relevant_hits as f64 / precision_denominator as f64,
        recall_at_k: matched_refs as f64 / expected_refs.len() as f64,
        ndcg_at_10: ndcg_at_k(results, expected_refs, RANK_K),
        evidence_recall_at_k: matched_refs as f64 / expected_refs.len() as f64,
    }
}

fn matched_ref_indexes(
    results: &[crate::memory::Memory],
    expected_refs: &[EvidenceRef],
    k: usize,
) -> HashSet<usize> {
    let mut matched = HashSet::new();
    for memory in results.iter().take(k) {
        for (index, evidence_ref) in expected_refs.iter().enumerate() {
            if evidence_ref.matches(memory) {
                matched.insert(index);
            }
        }
    }
    matched
}

fn reciprocal_rank_from_relevance(relevance: &[bool]) -> f64 {
    relevance
        .iter()
        .position(|is_relevant| *is_relevant)
        .map_or(0.0, |index| 1.0 / (index as f64 + 1.0))
}

fn ndcg_at_k(results: &[crate::memory::Memory], expected_refs: &[EvidenceRef], k: usize) -> f64 {
    if k == 0 || expected_refs.is_empty() {
        return 0.0;
    }

    let matches_by_rank: Vec<Vec<usize>> = results
        .iter()
        .take(k)
        .map(|memory| {
            expected_refs
                .iter()
                .enumerate()
                .filter_map(|(index, evidence_ref)| evidence_ref.matches(memory).then_some(index))
                .collect()
        })
        .collect();
    let dcg = best_dcg_for_matchable_ranks(&matches_by_rank);
    let ideal_hits = expected_refs.len().min(k);
    let idcg: f64 = (0..ideal_hits)
        .map(|index| 1.0 / (index as f64 + 2.0).log2())
        .sum();
    if idcg == 0.0 {
        0.0
    } else {
        dcg / idcg
    }
}

fn best_dcg_for_matchable_ranks(matches_by_rank: &[Vec<usize>]) -> f64 {
    let mut best = 0.0;
    for mask in 1usize..(1usize << matches_by_rank.len()) {
        let dcg: f64 = (0..matches_by_rank.len())
            .filter(|rank| (mask & (1usize << rank)) != 0)
            .map(|rank| 1.0 / (rank as f64 + 2.0).log2())
            .sum();
        if dcg > best && can_assign_unique_refs(matches_by_rank, mask) {
            best = dcg;
        }
    }
    best
}

fn can_assign_unique_refs(matches_by_rank: &[Vec<usize>], mask: usize) -> bool {
    let mut selected_ranks: Vec<usize> = (0..matches_by_rank.len())
        .filter(|rank| (mask & (1usize << rank)) != 0)
        .collect();
    if selected_ranks
        .iter()
        .any(|rank| matches_by_rank[*rank].is_empty())
    {
        return false;
    }
    selected_ranks.sort_by_key(|rank| matches_by_rank[*rank].len());

    let mut assigned_refs = HashSet::new();
    assign_rank_ref(0, &selected_ranks, matches_by_rank, &mut assigned_refs)
}

fn assign_rank_ref(
    selected_index: usize,
    selected_ranks: &[usize],
    matches_by_rank: &[Vec<usize>],
    assigned_refs: &mut HashSet<usize>,
) -> bool {
    let Some(rank) = selected_ranks.get(selected_index).copied() else {
        return true;
    };

    for ref_index in &matches_by_rank[rank] {
        if assigned_refs.insert(*ref_index) {
            if assign_rank_ref(
                selected_index + 1,
                selected_ranks,
                matches_by_rank,
                assigned_refs,
            ) {
                return true;
            }
            assigned_refs.remove(ref_index);
        }
    }
    false
}