use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use super::corpus::Corpus;
use super::metrics::{self, CaseMetrics, EvalResults};
use super::report;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FusedSignal {
Tfidf,
Pdg,
Dense,
Fragment,
Reranker,
}
impl FusedSignal {
pub fn all() -> [FusedSignal; 5] {
[
FusedSignal::Tfidf,
FusedSignal::Pdg,
FusedSignal::Dense,
FusedSignal::Fragment,
FusedSignal::Reranker,
]
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Tfidf => "tfidf",
Self::Pdg => "pdg",
Self::Dense => "dense",
Self::Fragment => "fragment",
Self::Reranker => "reranker",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FusedProfile {
pub enabled_signals: Vec<FusedSignal>,
pub signal_weights: HashMap<FusedSignal, f64>,
pub embedding_model: String,
pub reranker_model: String,
}
impl Default for FusedProfile {
fn default() -> Self {
Self {
enabled_signals: FusedSignal::all().to_vec(),
signal_weights: HashMap::new(),
embedding_model: "qwen3-embedding-0.6b-fp16".to_string(),
reranker_model: "qwen3-reranker-0.6b".to_string(),
}
}
}
impl FusedProfile {
pub fn ablate(base: &FusedProfile, drop_signal: FusedSignal) -> FusedProfile {
FusedProfile {
enabled_signals: base
.enabled_signals
.iter()
.copied()
.filter(|s| *s != drop_signal)
.collect(),
signal_weights: base.signal_weights.clone(),
embedding_model: base.embedding_model.clone(),
reranker_model: base.reranker_model.clone(),
}
}
pub fn no_reranker() -> FusedProfile {
FusedProfile {
enabled_signals: FusedSignal::all()
.iter()
.copied()
.filter(|s| *s != FusedSignal::Reranker)
.collect(),
signal_weights: HashMap::new(),
embedding_model: "qwen3-embedding-0.6b-fp16".to_string(),
reranker_model: "none".to_string(),
}
}
pub fn is_enabled(&self, signal: FusedSignal) -> bool {
self.enabled_signals.contains(&signal)
}
pub fn weight(&self, signal: FusedSignal) -> f64 {
self.signal_weights.get(&signal).copied().unwrap_or(1.0)
}
pub fn label(&self) -> String {
if self.enabled_signals.len() == FusedSignal::all().len() {
format!("full-fused ({})", self.embedding_model)
} else {
let dropped: Vec<&str> = FusedSignal::all()
.iter()
.filter(|s| !self.enabled_signals.contains(s))
.map(|s| s.as_str())
.collect();
format!("ablate-{} ({})", dropped.join("-"), self.embedding_model)
}
}
}
#[derive(Debug, Clone)]
pub struct RankedResults {
pub ranked_symbols: Vec<String>,
pub ranked_files: Vec<String>,
pub latency: Duration,
}
pub trait RetrievalBackend {
fn index(&mut self) -> Result<(), BackendError>;
fn query(&self, query: &str, profile: &FusedProfile) -> Result<RankedResults, BackendError>;
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct BackendError(pub String);
impl std::fmt::Display for BackendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for BackendError {}
pub struct EvalHarness {
corpus: Corpus,
}
impl EvalHarness {
pub fn new(corpus: Corpus) -> Self {
Self { corpus }
}
pub fn with_default_corpus() -> Result<Self, super::corpus::CorpusVerificationError> {
let corpus = super::corpus::load_and_verify_corpus()?;
Ok(Self { corpus })
}
pub fn corpus(&self) -> &Corpus {
&self.corpus
}
pub fn run(
&self,
backend: &mut dyn RetrievalBackend,
profile: &FusedProfile,
) -> Result<HarnessReport, HarnessError> {
backend.index().map_err(HarnessError::Backend)?;
let mut case_metrics: Vec<CaseMetrics> = Vec::new();
let mut latencies: Vec<Duration> = Vec::new();
for case in self.corpus.eval_cases() {
let results = backend
.query(&case.query, profile)
.map_err(HarnessError::Backend)?;
latencies.push(results.latency);
case_metrics.push(metrics::compute_case_metrics(
case,
&results.ranked_symbols,
&results.ranked_files,
));
}
let results = metrics::compute_eval_results(case_metrics);
let latency_stats = compute_latency_stats(&latencies);
Ok(HarnessReport {
profile: profile.clone(),
backend_name: backend.name().to_string(),
results,
latency: latency_stats,
corpus_name: "default".to_string(),
eval_case_count: self.corpus.eval_len(),
})
}
pub fn run_ablation(
&self,
backend: &mut dyn RetrievalBackend,
base_profile: &FusedProfile,
) -> Result<FusedAblationReport, HarnessError> {
let full_report = self.run(backend, base_profile)?;
let mut ablations: HashMap<FusedSignal, HarnessReport> = HashMap::new();
for signal in FusedSignal::all() {
let ablated_profile = FusedProfile::ablate(base_profile, signal);
let ablated_report = self.run(backend, &ablated_profile)?;
ablations.insert(signal, ablated_report);
}
Ok(FusedAblationReport {
full: full_report,
ablations,
base_profile: base_profile.clone(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatencyStats {
pub p50_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
pub mean_ms: f64,
pub count: usize,
}
fn compute_latency_stats(latencies: &[Duration]) -> LatencyStats {
if latencies.is_empty() {
return LatencyStats {
p50_ms: 0.0,
p95_ms: 0.0,
p99_ms: 0.0,
mean_ms: 0.0,
count: 0,
};
}
let mut sorted: Vec<f64> = latencies.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
let pct = |p: f64| -> f64 {
let idx = ((p / 100.0) * (n as f64 - 1.0)).round() as usize;
sorted[idx.min(n - 1)]
};
let mean: f64 = sorted.iter().sum::<f64>() / n as f64;
LatencyStats {
p50_ms: pct(50.0),
p95_ms: pct(95.0),
p99_ms: pct(99.0),
mean_ms: mean,
count: n,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarnessReport {
pub profile: FusedProfile,
pub backend_name: String,
pub results: EvalResults,
pub latency: LatencyStats,
pub corpus_name: String,
pub eval_case_count: usize,
}
impl HarnessReport {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FusedAblationReport {
pub full: HarnessReport,
pub ablations: HashMap<FusedSignal, HarnessReport>,
pub base_profile: FusedProfile,
}
impl FusedAblationReport {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn signal_contribution_mrr10(&self, signal: FusedSignal) -> f64 {
let full_mrr = self.full.results.aggregate.mean_mrr_10;
let ablated_mrr = self
.ablations
.get(&signal)
.map(|r| r.results.aggregate.mean_mrr_10)
.unwrap_or(full_mrr);
full_mrr - ablated_mrr
}
pub fn summary(&self) -> report::AblationSummary {
let mut signal_impacts: Vec<(String, f64)> = FusedSignal::all()
.iter()
.map(|s| (s.as_str().to_string(), self.signal_contribution_mrr10(*s)))
.collect();
signal_impacts.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
report::AblationSummary {
base_model: self.base_profile.embedding_model.clone(),
full_mrr10: self.full.results.aggregate.mean_mrr_10,
full_recall10: self.full.results.aggregate.mean_recall_10,
signal_impacts_mrr10: signal_impacts,
}
}
}
#[derive(Debug)]
pub enum HarnessError {
Backend(BackendError),
}
impl std::fmt::Display for HarnessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Backend(e) => write!(f, "Backend error: {e}"),
}
}
}
impl std::error::Error for HarnessError {}
pub struct MockBackend {
mock_results: HashMap<String, RankedResults>,
signal_quality: HashMap<FusedSignal, f64>,
}
impl MockBackend {
pub fn new() -> Self {
Self {
mock_results: HashMap::new(),
signal_quality: HashMap::new(),
}
}
pub fn register(&mut self, query: &str, results: RankedResults) {
self.mock_results.insert(query.to_string(), results);
}
pub fn set_signal_quality(&mut self, signal: FusedSignal, quality: f64) {
self.signal_quality.insert(signal, quality);
}
}
impl Default for MockBackend {
fn default() -> Self {
Self::new()
}
}
impl RetrievalBackend for MockBackend {
fn index(&mut self) -> Result<(), BackendError> {
Ok(()) }
fn query(&self, query: &str, profile: &FusedProfile) -> Result<RankedResults, BackendError> {
let results = self
.mock_results
.get(query)
.cloned()
.unwrap_or(RankedResults {
ranked_symbols: Vec::new(),
ranked_files: Vec::new(),
latency: Duration::from_micros(100),
});
if !profile.is_enabled(FusedSignal::Reranker) && results.ranked_symbols.len() > 1 {
let reranker_quality = self
.signal_quality
.get(&FusedSignal::Reranker)
.copied()
.unwrap_or(0.9);
if reranker_quality < 1.0 {
let mut degraded = results.ranked_symbols.clone();
degraded.swap(0, 1);
return Ok(RankedResults {
ranked_symbols: degraded,
ranked_files: results.ranked_files,
latency: results.latency,
});
}
}
Ok(results)
}
fn name(&self) -> &str {
"mock-backend"
}
}
pub fn register_perfect_mock(backend: &mut MockBackend, corpus: &Corpus) {
for case in corpus.eval_cases() {
backend.register(
&case.query,
RankedResults {
ranked_symbols: case.relevant_symbols.clone(),
ranked_files: case.relevant_files.clone(),
latency: Duration::from_micros(100),
},
);
}
}
pub fn register_noisy_mock(backend: &mut MockBackend, corpus: &Corpus, noise_items: &[&str]) {
for case in corpus.eval_cases() {
let mut ranked = case.relevant_symbols.clone();
for noise in noise_items {
ranked.push(noise.to_string());
}
backend.register(
&case.query,
RankedResults {
ranked_symbols: ranked,
ranked_files: case.relevant_files.clone(),
latency: Duration::from_micros(100),
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::corpus::{EvalCategory, Language, Split};
#[test]
fn test_fused_profile_default_all_signals() {
let profile = FusedProfile::default();
for signal in FusedSignal::all() {
assert!(profile.is_enabled(signal));
}
}
#[test]
fn test_fused_profile_ablate_one() {
let base = FusedProfile::default();
let ablated = FusedProfile::ablate(&base, FusedSignal::Reranker);
assert!(!ablated.is_enabled(FusedSignal::Reranker));
assert!(ablated.is_enabled(FusedSignal::Tfidf));
assert!(ablated.is_enabled(FusedSignal::Dense));
}
#[test]
fn test_fused_profile_no_reranker() {
let profile = FusedProfile::no_reranker();
assert!(!profile.is_enabled(FusedSignal::Reranker));
assert!(profile.is_enabled(FusedSignal::Tfidf));
assert!(profile.is_enabled(FusedSignal::Dense));
assert_eq!(profile.reranker_model, "none");
}
#[test]
fn test_fused_profile_label() {
let full = FusedProfile::default();
assert!(full.label().contains("full-fused"));
let ablated = FusedProfile::ablate(&full, FusedSignal::Reranker);
assert!(ablated.label().contains("ablate-reranker"));
}
#[test]
fn test_latency_stats_basic() {
let latencies = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
Duration::from_millis(40),
Duration::from_millis(50),
Duration::from_millis(60),
Duration::from_millis(70),
Duration::from_millis(80),
Duration::from_millis(90),
Duration::from_millis(100),
];
let stats = compute_latency_stats(&latencies);
assert_eq!(stats.count, 10);
assert!(stats.p50_ms > 0.0);
assert!(stats.p95_ms >= stats.p50_ms);
assert!(stats.p99_ms >= stats.p95_ms);
}
#[test]
fn test_latency_stats_empty() {
let stats = compute_latency_stats(&[]);
assert_eq!(stats.count, 0);
assert_eq!(stats.p50_ms, 0.0);
}
#[test]
fn test_mock_backend_query() {
let mut backend = MockBackend::new();
backend.register(
"hello",
RankedResults {
ranked_symbols: vec!["func_a".to_string()],
ranked_files: vec!["src/a.rs".to_string()],
latency: Duration::from_micros(50),
},
);
let profile = FusedProfile::default();
let results = backend.query("hello", &profile).expect("query");
assert_eq!(results.ranked_symbols, vec!["func_a".to_string()]);
}
#[test]
fn test_mock_backend_unknown_query() {
let backend = MockBackend::new();
let profile = FusedProfile::default();
let results = backend.query("unknown", &profile).expect("query");
assert!(results.ranked_symbols.is_empty());
}
#[test]
fn test_harness_run_with_perfect_mock() {
let mut corpus = Corpus::new();
for cat in EvalCategory::all() {
for i in 0..2 {
corpus.add_case(crate::eval::corpus::CorpusCase {
id: format!("{}_{i}", cat.as_str()),
query: format!("query_{cat:?}_{i}"),
category: cat,
split: if i == 0 { Split::Train } else { Split::Eval },
relevant_symbols: vec![format!("sym_{cat:?}_{i}")],
relevant_files: vec![format!("src/{cat:?}_{i}.rs")],
languages: vec![Language::Rust],
expected_position: None,
notes: None,
hard_negatives: Vec::new(),
});
}
}
let mut backend = MockBackend::new();
register_perfect_mock(&mut backend, &corpus);
let harness = EvalHarness::new(corpus);
let profile = FusedProfile::default();
let report = harness.run(&mut backend, &profile).expect("run");
assert_eq!(report.eval_case_count, 14);
assert!((report.results.aggregate.mean_recall_1 - 1.0).abs() < 1e-10);
assert!((report.results.aggregate.mean_mrr_10 - 1.0).abs() < 1e-10);
}
#[test]
fn test_harness_run_ablation() {
let mut corpus = Corpus::new();
for cat in EvalCategory::all() {
for i in 0..2 {
corpus.add_case(crate::eval::corpus::CorpusCase {
id: format!("{}_{i}", cat.as_str()),
query: format!("query_{cat:?}_{i}"),
category: cat,
split: if i == 0 { Split::Train } else { Split::Eval },
relevant_symbols: vec![format!("sym_{cat:?}_{i}")],
relevant_files: vec![format!("src/{cat:?}_{i}.rs")],
languages: vec![Language::Rust],
expected_position: None,
notes: None,
hard_negatives: Vec::new(),
});
}
}
let mut backend = MockBackend::new();
register_perfect_mock(&mut backend, &corpus);
backend.set_signal_quality(FusedSignal::Reranker, 0.8);
let harness = EvalHarness::new(corpus);
let profile = FusedProfile::default();
let ablation = harness
.run_ablation(&mut backend, &profile)
.expect("ablation");
assert!(
(ablation.full.results.aggregate.mean_mrr_10 - 1.0).abs() < 1e-10,
"Perfect mock should give MRR@10 = 1.0"
);
assert_eq!(ablation.ablations.len(), 5);
}
#[test]
fn test_harness_report_json_serialization() {
let report = HarnessReport {
profile: FusedProfile::default(),
backend_name: "test".to_string(),
results: metrics::compute_eval_results(vec![]),
latency: LatencyStats {
p50_ms: 1.0,
p95_ms: 5.0,
p99_ms: 10.0,
mean_ms: 2.0,
count: 10,
},
corpus_name: "test".to_string(),
eval_case_count: 10,
};
let json = report.to_json().expect("json");
assert!(json.contains("\"backend_name\""));
}
#[test]
fn test_signal_contribution() {
let mut corpus = super::super::corpus::build_default_corpus();
let _ = &mut corpus; let mut backend = MockBackend::new();
super::register_perfect_mock(&mut backend, &corpus);
let harness = EvalHarness::new(corpus);
let profile = FusedProfile::default();
let ablation = harness
.run_ablation(&mut backend, &profile)
.expect("ablation");
for signal in FusedSignal::all() {
let contribution = ablation.signal_contribution_mrr10(signal);
assert!(
contribution.abs() < 1e-10,
"Signal {signal:?} contribution should be ~0 with perfect mock, got {contribution}"
);
}
}
#[test]
fn test_ablation_summary() {
let corpus = super::super::corpus::build_default_corpus();
let mut backend = MockBackend::new();
register_perfect_mock(&mut backend, &corpus);
let harness = EvalHarness::new(corpus);
let profile = FusedProfile::default();
let ablation = harness
.run_ablation(&mut backend, &profile)
.expect("ablation");
let summary = ablation.summary();
assert!(!summary.base_model.is_empty());
assert!((summary.full_mrr10 - 1.0).abs() < 1e-10);
assert_eq!(summary.signal_impacts_mrr10.len(), 5);
}
#[test]
fn test_register_noisy_mock() {
let mut corpus = super::super::corpus::build_default_corpus();
let mut backend = MockBackend::new();
register_noisy_mock(&mut backend, &corpus, &["noise1", "noise2"]);
let case = corpus.eval_cases().next().expect("at least one case");
let profile = FusedProfile::default();
let results = backend.query(&case.query, &profile).expect("query");
assert!(results.ranked_symbols.contains(&"noise1".to_string()));
assert!(results.ranked_symbols.contains(&"noise2".to_string()));
let _ = &mut corpus;
}
}