frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use std::time::Instant;

use serde::Serialize;

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct SearchStageSample {
    pub elapsed_us: u64,
    pub input_count: usize,
    pub output_count: usize,
}

impl SearchStageSample {
    pub const fn new(elapsed_us: u64, input_count: usize, output_count: usize) -> Self {
        Self {
            elapsed_us,
            input_count,
            output_count,
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct SearchStageAttribution {
    pub candidate_intake: SearchStageSample,
    pub freshness_validation: SearchStageSample,
    pub scan: SearchStageSample,
    pub witness_scoring: SearchStageSample,
    pub graph_expansion: SearchStageSample,
    pub semantic_retrieval: SearchStageSample,
    pub anchor_blending: SearchStageSample,
    pub document_aggregation: SearchStageSample,
    pub final_diversification: SearchStageSample,
}

pub(super) fn elapsed_us(started_at: Instant) -> u64 {
    u64::try_from(started_at.elapsed().as_micros()).unwrap_or(u64::MAX)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn search_stage_sample_new_initializes_fields() {
        let sample = SearchStageSample::new(11, 22, 33);

        assert_eq!(sample.elapsed_us, 11);
        assert_eq!(sample.input_count, 22);
        assert_eq!(sample.output_count, 33);
    }

    #[test]
    fn search_stage_attribution_can_clone_and_preserve_defaults() {
        let attribution = SearchStageAttribution {
            candidate_intake: SearchStageSample::new(1, 2, 3),
            freshness_validation: SearchStageSample::new(4, 5, 6),
            scan: SearchStageSample::new(7, 8, 9),
            witness_scoring: SearchStageSample::new(10, 11, 12),
            graph_expansion: SearchStageSample::new(13, 14, 15),
            semantic_retrieval: SearchStageSample::new(16, 17, 18),
            anchor_blending: SearchStageSample::new(19, 20, 21),
            document_aggregation: SearchStageSample::new(22, 23, 24),
            final_diversification: SearchStageSample::new(25, 26, 27),
        };

        let cloned = attribution.clone();

        assert_eq!(attribution, cloned);
        assert_eq!(
            attribution.freshness_validation.output_count,
            cloned.freshness_validation.output_count
        );
        assert_eq!(
            attribution.graph_expansion.input_count,
            cloned.graph_expansion.input_count
        );
    }
}