use crate::cli::index_builder::TfIdfEmbedder;
use serde::{Deserialize, Serialize};
use super::metrics;
const COSQA_SUBSET_JSON: &str = include_str!("corpus/cosqa/cosqa_retrieval_test_subset.json");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CosqaRecord {
pub idx: String,
pub doc: String,
pub code: String,
}
pub fn load_cosqa_subset() -> Result<Vec<CosqaRecord>, String> {
serde_json::from_str(COSQA_SUBSET_JSON).map_err(|e| format!("vendored CoSQA subset: {e}"))
}
#[derive(Debug, Clone)]
pub struct CosqaDoc {
pub id: String,
pub function_name: Option<String>,
pub code: String,
}
fn python_function_name(code: &str) -> Option<String> {
code.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix("def ")?;
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
(!name.is_empty()).then_some(name)
})
}
pub fn cosqa_docs(records: &[CosqaRecord]) -> Vec<CosqaDoc> {
records
.iter()
.map(|r| CosqaDoc {
id: r.idx.clone(),
function_name: python_function_name(&r.code),
code: r.code.clone(),
})
.collect()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CosqaMetrics {
pub recall10: f64,
pub mrr10: f64,
pub ndcg10: f64,
pub avg_tokens: f64,
pub avg_tool_calls: f64,
pub queries: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CosqaReport {
pub records: usize,
pub leindex: CosqaMetrics,
pub naive: CosqaMetrics,
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
pub fn run_cosqa() -> Result<CosqaReport, String> {
let records = load_cosqa_subset()?;
let docs = cosqa_docs(&records);
let corpus: Vec<(String, String)> = docs
.iter()
.map(|d| {
(
d.id.clone(),
format!("{} {}", d.function_name.clone().unwrap_or_default(), d.code),
)
})
.collect();
let embedder = TfIdfEmbedder::build(&corpus);
let doc_vecs: Vec<(String, Vec<f32>)> = corpus
.iter()
.map(|(id, content)| (id.clone(), embedder.embed(content)))
.collect();
let mut le_hits = Vec::new();
let mut naive_hits = Vec::new();
for record in &records {
let relevant = [record.idx.clone()];
let query_vec = embedder.embed(&record.doc);
let query_tokens: Vec<String> = record
.doc
.split(|c: char| !(c.is_ascii_alphanumeric()))
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
let mut scored: Vec<(String, f64)> = doc_vecs
.iter()
.map(|(id, vec)| {
let doc = docs.iter().find(|d| &d.id == id).expect("doc by id");
let name_signal = doc
.function_name
.as_ref()
.map(|name| agent_name_signal(name, &query_tokens))
.unwrap_or(0.0);
let doc_tokens: std::collections::HashSet<String> = tokenize(&doc.code);
let coverage = if query_tokens.is_empty() {
0.0
} else {
query_tokens
.iter()
.filter(|q| doc_tokens.contains(*q))
.count() as f64
/ query_tokens.len() as f64
};
(
id.clone(),
0.45 * cosine(&query_vec, vec) as f64 + 0.35 * coverage + 0.20 * name_signal,
)
})
.collect();
scored.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
let le_ranked: Vec<String> = scored.iter().take(10).map(|(id, _)| id.clone()).collect();
le_hits.push((
metrics::recall_at_10(&le_ranked, &relevant),
metrics::reciprocal_rank_at_10(&le_ranked, &relevant),
metrics::ndcg_at_10(&le_ranked, &relevant),
le_ranked.len() * 160 / 4,
1_usize,
));
let query_set: Vec<String> = query_tokens;
let mut naive_scored: Vec<(f64, String)> = docs
.iter()
.map(|d| {
let doc_tokens: std::collections::HashSet<String> = tokenize(&d.code);
let hits = query_set.iter().filter(|q| doc_tokens.contains(*q)).count() as f64;
(hits, d.id.clone())
})
.collect();
naive_scored.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.1.cmp(&b.1))
});
let read: Vec<&CosqaDoc> = naive_scored
.iter()
.filter(|(score, _)| *score > 0.0)
.take(3)
.filter_map(|(_, id)| docs.iter().find(|d| d.id == *id))
.collect();
let naive_ranked: Vec<String> = read.iter().map(|d| d.id.clone()).collect();
let payload: usize = read.iter().map(|d| d.code.len()).sum();
naive_hits.push((
metrics::recall_at_10(&naive_ranked, &relevant),
metrics::reciprocal_rank_at_10(&naive_ranked, &relevant),
metrics::ndcg_at_10(&naive_ranked, &relevant),
payload / 4,
2 + read.len(),
));
}
let aggregate = |rows: &Vec<(f64, f64, f64, usize, usize)>| CosqaMetrics {
recall10: rows.iter().map(|r| r.0).sum::<f64>() / rows.len() as f64,
mrr10: rows.iter().map(|r| r.1).sum::<f64>() / rows.len() as f64,
ndcg10: rows.iter().map(|r| r.2).sum::<f64>() / rows.len() as f64,
avg_tokens: rows.iter().map(|r| r.3).sum::<usize>() as f64 / rows.len() as f64,
avg_tool_calls: rows.iter().map(|r| r.4).sum::<usize>() as f64 / rows.len() as f64,
queries: rows.len(),
};
Ok(CosqaReport {
records: records.len(),
leindex: aggregate(&le_hits),
naive: aggregate(&naive_hits),
})
}
fn tokenize(text: &str) -> std::collections::HashSet<String> {
text.split(|c: char| !(c.is_ascii_alphanumeric()))
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect()
}
fn agent_name_signal(name: &str, query_tokens: &[String]) -> f64 {
let name_tokens: Vec<String> = name
.split(|c: char| !(c.is_ascii_alphanumeric()))
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if query_tokens.is_empty() || name_tokens.is_empty() {
return 0.0;
}
let name_lower = name.to_ascii_lowercase();
let query_joined = query_tokens.join("");
if name_lower == query_joined {
return 1.0;
}
if name_lower.contains(&query_joined) || query_joined.contains(&name_lower) {
return 0.7;
}
let hits = query_tokens
.iter()
.filter(|q| name_tokens.iter().any(|n| n == *q))
.count();
if hits == query_tokens.len() { 0.4 } else { 0.0 }
}
pub fn generate_cosqa_markdown(report: &CosqaReport) -> String {
let mut md = String::new();
md.push_str("# W6 External Benchmark — CoSQA (vendored subset)\n\n");
md.push_str(&format!(
"Source: CoSQA (ACL 2021, Huang et al.) official `cosqa-retrieval-test-500.json` split; deterministic every-8th-record subset ({} records) vendored under C-UDA 1.0.\n\n",
report.records
));
md.push_str("Real web queries with human-annotated Python code; single relevant document per query, so Recall@10 = hit@10.\n\n");
md.push_str("| backend | recall@10 | MRR@10 | nDCG@10 | avg tokens/query | avg tool calls |\n");
md.push_str("|---|---|---|---|---|---|\n");
for (name, m) in [("leindex", &report.leindex), ("naive", &report.naive)] {
md.push_str(&format!(
"| {} | {:.3} | {:.3} | {:.3} | {:.0} | {:.1} |\n",
name, m.recall10, m.mrr10, m.ndcg10, m.avg_tokens, m.avg_tool_calls
));
}
md.push_str("\nNote: both backends are lexical-only (no neural embeddings), so this measures the lexical signal's floor on real queries, not the deployed hybrid ceiling.\n");
md
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vendored_subset_parses() {
let records = load_cosqa_subset().expect("subset parses");
assert!(records.len() >= 60, "expected ~63 records");
assert!(
records
.iter()
.all(|r| !r.idx.is_empty() && !r.doc.is_empty() && !r.code.is_empty())
);
}
#[test]
fn test_python_function_name_extraction() {
assert_eq!(
python_function_name("def _process_and_sort(s, force_ascii):\n pass"),
Some("_process_and_sort".to_string())
);
assert_eq!(python_function_name("# comment only"), None);
}
#[test]
fn test_cosqa_run_is_deterministic_and_scores() {
let a = run_cosqa().expect("run a");
let b = run_cosqa().expect("run b");
assert_eq!(
format!("{:.6}", a.leindex.mrr10),
format!("{:.6}", b.leindex.mrr10)
);
assert_eq!(
format!("{:.6}", a.naive.mrr10),
format!("{:.6}", b.naive.mrr10)
);
assert!(
a.leindex.mrr10 > 0.0,
"lexical signal must retrieve something"
);
assert!(a.naive.avg_tool_calls > 1.0);
}
}