use std::sync::Arc;
use async_trait::async_trait;
use brainwires_agents::eval::{EvaluationCase, TrialResult, ndcg_at_k};
use brainwires_cognition::knowledge::entity::{Entity, EntityType};
use brainwires_cognition::knowledge::relationship_graph::RelationshipGraph;
fn make_entity(
name: &str,
entity_type: EntityType,
mention_count: u32,
n_messages: usize,
) -> Entity {
let mut e = Entity::new(name.to_string(), entity_type, "msg-0".to_string(), 0);
for i in 1..mention_count {
e.add_mention(format!("msg-{i}"), i as i64);
}
e.message_ids = (0..n_messages).map(|i| format!("msg-{i}")).collect();
e
}
pub struct EntityImportanceRankingCase;
#[async_trait]
impl EvaluationCase for EntityImportanceRankingCase {
fn name(&self) -> &str {
"entity_importance_ranking"
}
fn category(&self) -> &str {
"entity_resolution"
}
async fn run(&self, trial_id: usize) -> anyhow::Result<TrialResult> {
let start = std::time::Instant::now();
let entities = vec![
make_entity("hub", EntityType::Concept, 20, 10),
make_entity("mid_a", EntityType::Concept, 5, 3),
make_entity("mid_b", EntityType::Concept, 3, 2),
make_entity("peripheral", EntityType::Concept, 1, 1),
];
let ground_truth: Vec<usize> = vec![3, 2, 1, 0];
let scores: Vec<f64> = entities
.iter()
.map(|e| RelationshipGraph::calculate_importance(e) as f64)
.collect();
let ndcg = ndcg_at_k(&scores, &ground_truth, 4);
let ms = start.elapsed().as_millis() as u64;
if ndcg >= 0.8 {
Ok(TrialResult::success(trial_id, ms)
.with_meta("ndcg", serde_json::json!(ndcg))
.with_meta("scores", serde_json::json!(scores)))
} else {
Ok(TrialResult::failure(
trial_id,
ms,
format!("NDCG@4={ndcg:.4} < 0.8 — importance formula does not correctly rank hub > peripheral"),
)
.with_meta("ndcg", serde_json::json!(ndcg))
.with_meta("scores", serde_json::json!(scores)))
}
}
}
pub struct EntitySingleMentionCase;
#[async_trait]
impl EvaluationCase for EntitySingleMentionCase {
fn name(&self) -> &str {
"entity_single_mention_nonzero"
}
fn category(&self) -> &str {
"entity_resolution"
}
async fn run(&self, trial_id: usize) -> anyhow::Result<TrialResult> {
let start = std::time::Instant::now();
let file_entity = make_entity("single_file", EntityType::File, 1, 1);
let var_entity = make_entity("single_var", EntityType::Variable, 1, 1);
let file_score = RelationshipGraph::calculate_importance(&file_entity);
let var_score = RelationshipGraph::calculate_importance(&var_entity);
let ms = start.elapsed().as_millis() as u64;
let detail = format!(
"file_importance={file_score:.4} (expected ≈0.45), \
variable_importance={var_score:.4} (expected ≈0.15). \
Note: mention-count component is 0 for single-mention entities \
due to ln(1)=0; type bonus compensates."
);
if file_score > 0.0 && var_score > 0.0 && file_score > var_score {
Ok(TrialResult::success(trial_id, ms)
.with_meta("file_importance", serde_json::json!(file_score))
.with_meta("variable_importance", serde_json::json!(var_score))
.with_meta("detail", serde_json::json!(detail)))
} else {
Ok(TrialResult::failure(trial_id, ms, detail))
}
}
}
pub struct EntityTypeBonusCase;
#[async_trait]
impl EvaluationCase for EntityTypeBonusCase {
fn name(&self) -> &str {
"entity_type_bonus_ordering"
}
fn category(&self) -> &str {
"entity_resolution"
}
async fn run(&self, trial_id: usize) -> anyhow::Result<TrialResult> {
let start = std::time::Instant::now();
let types_ordered = [
EntityType::File, EntityType::Type, EntityType::Function, EntityType::Error, EntityType::Concept, EntityType::Command, EntityType::Variable, ];
let ground_truth: Vec<usize> = (0..7).rev().collect();
let scores: Vec<f64> = types_ordered
.iter()
.map(|et| {
let e = make_entity("e", et.clone(), 1, 1);
RelationshipGraph::calculate_importance(&e) as f64
})
.collect();
let ndcg = ndcg_at_k(&scores, &ground_truth, 7);
let ms = start.elapsed().as_millis() as u64;
if ndcg >= 0.95 {
Ok(TrialResult::success(trial_id, ms)
.with_meta("ndcg", serde_json::json!(ndcg))
.with_meta("scores", serde_json::json!(scores)))
} else {
Ok(TrialResult::failure(
trial_id,
ms,
format!(
"NDCG@7={ndcg:.4} < 0.95 — type bonus ordering is incorrect. scores={scores:?}"
),
)
.with_meta("ndcg", serde_json::json!(ndcg)))
}
}
}
pub fn entity_importance_suite() -> Vec<Arc<dyn EvaluationCase>> {
vec![
Arc::new(EntityImportanceRankingCase),
Arc::new(EntitySingleMentionCase),
Arc::new(EntityTypeBonusCase),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_ranking_case_passes() {
let case = EntityImportanceRankingCase;
let result = case.run(0).await.unwrap();
assert!(
result.success,
"EntityImportanceRankingCase failed: {:?}",
result.error
);
}
#[tokio::test]
async fn test_single_mention_case_passes() {
let case = EntitySingleMentionCase;
let result = case.run(0).await.unwrap();
assert!(
result.success,
"EntitySingleMentionCase failed: {:?}",
result.error
);
}
#[tokio::test]
async fn test_type_bonus_case_passes() {
let case = EntityTypeBonusCase;
let result = case.run(0).await.unwrap();
assert!(
result.success,
"EntityTypeBonusCase failed: {:?}",
result.error
);
}
}