use std::collections::HashMap;
use crate::note::path_matches_mention;
use crate::store::helpers::NoteSummary;
use super::config::ScoringConfig;
#[cfg(test)]
fn note_boost(file_path: &str, chunk_name: &str, notes: &[NoteSummary]) -> f32 {
let mut strongest: Option<f32> = None;
for note in notes {
for mention in ¬e.mentions {
if path_matches_mention(file_path, mention) || chunk_name == mention {
match strongest {
Some(prev) if note.sentiment.abs() > prev.abs() => {
strongest = Some(note.sentiment);
}
None => {
strongest = Some(note.sentiment);
}
_ => {}
}
break; }
}
}
match strongest {
Some(s) => 1.0 + s * ScoringConfig::DEFAULT.note_boost_factor,
None => 1.0,
}
}
pub(crate) struct NoteBoostIndex<'a> {
#[cfg(test)]
pub(super) name_sentiments: HashMap<&'a str, f32>,
#[cfg(not(test))]
name_sentiments: HashMap<&'a str, f32>,
#[cfg(test)]
pub(super) path_mentions: Vec<(&'a str, f32)>,
#[cfg(not(test))]
path_mentions: Vec<(&'a str, f32)>,
}
impl<'a> NoteBoostIndex<'a> {
pub fn new(notes: &'a [NoteSummary]) -> Self {
let mut name_sentiments: HashMap<&'a str, f32> = HashMap::new();
let mut path_mentions: Vec<(&'a str, f32)> = Vec::new();
for note in notes {
for mention in ¬e.mentions {
let is_path_like =
mention.contains('/') || mention.contains('.') || mention.contains('\\');
if is_path_like {
path_mentions.push((mention.as_str(), note.sentiment));
} else {
let entry = name_sentiments.entry(mention.as_str()).or_insert(0.0);
if note.sentiment.abs() > entry.abs() {
*entry = note.sentiment;
}
}
}
}
let mut deduped_paths: HashMap<&'a str, f32> = HashMap::new();
for (mention, sentiment) in &path_mentions {
let entry = deduped_paths.entry(mention).or_insert(0.0);
if sentiment.abs() > entry.abs() {
*entry = *sentiment;
}
}
let path_mentions: Vec<(&'a str, f32)> = deduped_paths.into_iter().collect();
Self {
name_sentiments,
path_mentions,
}
}
#[inline]
pub fn boost(&self, file_path: &str, chunk_name: &str) -> f32 {
let mut strongest: Option<f32> = None;
if let Some(&sentiment) = self.name_sentiments.get(chunk_name) {
strongest = Some(sentiment);
}
for &(mention, sentiment) in &self.path_mentions {
if path_matches_mention(file_path, mention) {
match strongest {
Some(prev) if sentiment.abs() > prev.abs() => {
strongest = Some(sentiment);
}
None => {
strongest = Some(sentiment);
}
_ => {}
}
}
}
match strongest {
Some(s) => 1.0 + s * ScoringConfig::DEFAULT.note_boost_factor,
None => 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_note(sentiment: f32, mentions: &[&str]) -> NoteSummary {
NoteSummary {
id: "note:test".to_string(),
text: "test note".to_string(),
sentiment,
mentions: mentions.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn test_note_boost_no_notes() {
let boost = note_boost("src/lib.rs", "my_fn", &[]);
assert_eq!(boost, 1.0);
}
#[test]
fn test_note_boost_no_match() {
let notes = vec![make_note(-0.5, &["other.rs"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert_eq!(boost, 1.0);
}
#[test]
fn test_note_boost_file_match_negative() {
let notes = vec![make_note(-1.0, &["lib.rs"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert!(
(boost - 0.85).abs() < 0.001,
"Expected ~0.85, got {}",
boost
);
}
#[test]
fn test_note_boost_file_match_positive() {
let notes = vec![make_note(1.0, &["lib.rs"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert!(
(boost - 1.15).abs() < 0.001,
"Expected ~1.15, got {}",
boost
);
}
#[test]
fn test_note_boost_name_match() {
let notes = vec![make_note(0.5, &["my_fn"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert!(
(boost - 1.075).abs() < 0.001,
"Expected ~1.075, got {}",
boost
);
}
#[test]
fn test_note_boost_strongest_wins() {
let notes = vec![make_note(0.5, &["lib.rs"]), make_note(-1.0, &["lib.rs"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert!(
(boost - 0.85).abs() < 0.001,
"Expected ~0.85, got {}",
boost
);
}
#[test]
fn test_note_boost_strongest_absolute_preserves_sign() {
let notes = vec![make_note(1.0, &["lib.rs"]), make_note(-0.5, &["lib.rs"])];
let boost = note_boost("src/lib.rs", "my_fn", ¬es);
assert!(
(boost - 1.15).abs() < 0.001,
"Expected ~1.15, got {}",
boost
);
}
#[test]
fn test_note_boost_index_empty_notes() {
let notes: Vec<NoteSummary> = vec![];
let index = NoteBoostIndex::new(¬es);
assert_eq!(index.boost("src/lib.rs", "my_fn"), 1.0);
}
#[test]
fn test_note_boost_index_name_mention_positive() {
let notes = vec![NoteSummary {
id: "1".into(),
text: "good pattern".into(),
sentiment: 0.5,
mentions: vec!["my_fn".into()],
}];
let index = NoteBoostIndex::new(¬es);
let boost = index.boost("src/lib.rs", "my_fn");
assert!(
boost > 1.0,
"Positive sentiment should boost > 1.0, got {boost}"
);
assert!((boost - (1.0 + 0.5 * ScoringConfig::DEFAULT.note_boost_factor)).abs() < 1e-6);
}
#[test]
fn test_note_boost_index_name_mention_negative() {
let notes = vec![NoteSummary {
id: "1".into(),
text: "buggy code".into(),
sentiment: -1.0,
mentions: vec!["broken_fn".into()],
}];
let index = NoteBoostIndex::new(¬es);
let boost = index.boost("src/lib.rs", "broken_fn");
assert!(
boost < 1.0,
"Negative sentiment should reduce score, got {boost}"
);
assert!((boost - (1.0 - 1.0 * ScoringConfig::DEFAULT.note_boost_factor)).abs() < 1e-6);
}
#[test]
fn test_note_boost_index_path_mention() {
let notes = vec![NoteSummary {
id: "1".into(),
text: "important file".into(),
sentiment: 0.5,
mentions: vec!["src/search.rs".into()],
}];
let index = NoteBoostIndex::new(¬es);
let boost = index.boost("src/search.rs", "unrelated_fn");
assert!(
boost > 1.0,
"Path mention should boost matching file, got {boost}"
);
let no_boost = index.boost("src/lib.rs", "unrelated_fn");
assert_eq!(no_boost, 1.0, "Non-matching path should not be boosted");
}
#[test]
fn test_note_boost_index_strongest_absolute_wins() {
let notes = vec![
NoteSummary {
id: "1".into(),
text: "mildly good".into(),
sentiment: 0.5,
mentions: vec!["my_fn".into()],
},
NoteSummary {
id: "2".into(),
text: "very bad".into(),
sentiment: -1.0,
mentions: vec!["my_fn".into()],
},
];
let index = NoteBoostIndex::new(¬es);
let boost = index.boost("src/lib.rs", "my_fn");
assert!(
boost < 1.0,
"Stronger negative should win over weaker positive, got {boost}"
);
assert!((boost - (1.0 - 1.0 * ScoringConfig::DEFAULT.note_boost_factor)).abs() < 1e-6);
}
#[test]
fn test_note_boost_index_name_vs_path_classification() {
let notes = vec![NoteSummary {
id: "1".into(),
text: "note".into(),
sentiment: 0.5,
mentions: vec!["my_fn".into(), "search.rs".into()],
}];
let index = NoteBoostIndex::new(¬es);
assert!(index.name_sentiments.contains_key("my_fn"));
assert!(!index.name_sentiments.contains_key("search.rs"));
assert_eq!(index.path_mentions.len(), 1);
}
#[test]
fn test_note_boost_index_no_match() {
let notes = vec![NoteSummary {
id: "1".into(),
text: "specific note".into(),
sentiment: 1.0,
mentions: vec!["other_fn".into()],
}];
let index = NoteBoostIndex::new(¬es);
assert_eq!(index.boost("src/lib.rs", "my_fn"), 1.0);
}
}