use crate::bm25;
use chrono::{DateTime, Utc};
use codemem_core::{GraphBackend, MemoryNode, ScoreBreakdown};
const CODE_GRAPH_W_PAGERANK: f64 = 0.4;
const CODE_GRAPH_W_BETWEENNESS: f64 = 0.3;
const CODE_GRAPH_W_CONNECTIVITY: f64 = 0.2;
const CODE_GRAPH_W_EDGE_WEIGHT: f64 = 0.1;
const TEMPORAL_DECAY_HOURS: f64 = 30.0 * 24.0;
const RECENCY_DECAY_HOURS: f64 = 7.0 * 24.0;
pub fn graph_strength_for_memory(graph: &dyn GraphBackend, memory_id: &str) -> f64 {
let metrics = match graph.raw_graph_metrics_for_memory(memory_id) {
Some(m) => m,
None => return 0.0,
};
let code_score = if metrics.code_neighbor_count > 0 {
let connectivity = (metrics.code_neighbor_count as f64 / 5.0).min(1.0);
let avg_edge_w = (metrics.total_edge_weight / metrics.code_neighbor_count as f64).min(1.0);
CODE_GRAPH_W_PAGERANK * metrics.max_pagerank
+ CODE_GRAPH_W_BETWEENNESS * metrics.max_betweenness
+ CODE_GRAPH_W_CONNECTIVITY * connectivity
+ CODE_GRAPH_W_EDGE_WEIGHT * avg_edge_w
} else {
0.0
};
let memory_score = if metrics.memory_neighbor_count > 0 {
let connectivity = (metrics.memory_neighbor_count as f64 / 10.0).min(1.0);
let avg_edge_w =
(metrics.memory_edge_weight / metrics.memory_neighbor_count as f64).min(1.0);
0.6 * connectivity + 0.4 * avg_edge_w
} else {
0.0
};
let score = match (
metrics.code_neighbor_count > 0,
metrics.memory_neighbor_count > 0,
) {
(true, true) => 0.7 * code_score + 0.3 * memory_score,
(true, false) => code_score,
(false, true) => memory_score,
(false, false) => 0.0,
};
score.min(1.0)
}
pub use codemem_core::truncate as truncate_content;
pub fn compute_score(
memory: &MemoryNode,
query_tokens: &[&str],
vector_similarity: f64,
graph: &dyn GraphBackend,
bm25: &bm25::Bm25Index,
now: DateTime<Utc>,
) -> ScoreBreakdown {
let token_overlap = if query_tokens.is_empty() {
0.0
} else {
let indexed_score = bm25.score_with_tokens_str(query_tokens, &memory.id);
if indexed_score > 0.0 {
indexed_score
} else {
bm25.score_text_with_tokens_str(query_tokens, &memory.content)
}
};
let age_hours = (now - memory.updated_at).num_hours().max(0) as f64;
let temporal = (-age_hours / TEMPORAL_DECAY_HOURS).exp();
let tag_matching = if !query_tokens.is_empty() {
let tag_str: String = memory.tags.join(" ").to_lowercase();
let matches = query_tokens
.iter()
.filter(|qt| tag_str.contains(**qt))
.count();
matches as f64 / query_tokens.len() as f64
} else {
0.0
};
let access_hours = (now - memory.last_accessed_at).num_hours().max(0) as f64;
let recency = (-access_hours / RECENCY_DECAY_HOURS).exp();
let graph_strength = graph_strength_for_memory(graph, &memory.id);
ScoreBreakdown {
vector_similarity,
graph_strength,
token_overlap,
temporal,
tag_matching,
importance: memory.importance,
confidence: memory.confidence,
recency,
}
}
#[cfg(test)]
#[path = "tests/scoring_tests.rs"]
mod tests;