use crate::types::ScoredChunkId;
use regex::Regex;
use std::collections::HashMap;
pub fn strip_regex_for_semantic(pattern: &str) -> String {
let stripped = pattern
.replace("\\s+", " ")
.replace("\\w+", "")
.replace("\\d+", "")
.replace("\\b", "")
.replace(".*", " ")
.replace(".+", " ")
.replace("\\.", " ");
let cleaned: String = stripped
.chars()
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '_')
.collect();
cleaned.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn merge_query_with_pattern(semantic_query: &str, regex_pattern: &str) -> String {
let pattern_tokens = strip_regex_for_semantic(regex_pattern);
let query_lower = semantic_query.to_lowercase();
let mut tokens: Vec<&str> = semantic_query.split_whitespace().collect();
for token in pattern_tokens.split_whitespace() {
if !query_lower.contains(&token.to_lowercase()) {
tokens.push(token);
}
}
tokens.join(" ")
}
pub fn merge_regex_semantic(
regex_results: &[ScoredChunkId],
semantic_results: &[ScoredChunkId],
w_regex: f32,
w_semantic: f32,
) -> Vec<ScoredChunkId> {
let mut scores: HashMap<u64, f32> = HashMap::new();
for s in regex_results {
let entry = scores.entry(s.chunk_id).or_insert(0.0);
*entry = entry.max(w_regex * s.score);
}
for s in semantic_results {
let entry = scores.entry(s.chunk_id).or_insert(0.0);
*entry = entry.max(w_semantic * s.score);
}
let mut merged: Vec<ScoredChunkId> = scores
.into_iter()
.map(|(chunk_id, score)| ScoredChunkId::new(chunk_id, score))
.collect();
merged.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
merged
}
pub fn compile_pattern(pattern: &str) -> Option<Regex> {
Regex::new(pattern).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_basic_regex() {
assert_eq!(strip_regex_for_semantic(r"fn\s+\w+"), "fn");
}
#[test]
fn strip_escaped_dot() {
assert_eq!(
strip_regex_for_semantic(r"Promise\.allSettled"),
"Promise allSettled"
);
}
#[test]
fn strip_complex_pattern() {
assert_eq!(strip_regex_for_semantic(r"async\s+fn\s+\w+"), "async fn");
}
#[test]
fn merge_query_deduplicates() {
let merged = merge_query_with_pattern("find Promise usage", r"Promise\.allSettled");
let promise_count = merged.matches("Promise").count();
assert_eq!(promise_count, 1);
assert!(merged.contains("allSettled"));
}
#[test]
fn merge_results_uses_max_score() {
let regex = vec![ScoredChunkId::new(1, 0.8), ScoredChunkId::new(2, 0.5)];
let semantic = vec![ScoredChunkId::new(1, 0.6), ScoredChunkId::new(3, 0.9)];
let merged = merge_regex_semantic(®ex, &semantic, 1.0, 1.0);
assert_eq!(merged.len(), 3);
let chunk1 = merged.iter().find(|s| s.chunk_id == 1).expect("chunk 1");
assert!((chunk1.score - 0.8).abs() < f32::EPSILON);
assert!(merged[0].score >= merged[1].score);
assert!(merged[1].score >= merged[2].score);
}
#[test]
fn merge_results_with_weights() {
let regex = vec![ScoredChunkId::new(1, 1.0)];
let semantic = vec![ScoredChunkId::new(1, 1.0)];
let merged = merge_regex_semantic(®ex, &semantic, 0.5, 0.8);
let chunk1 = merged.iter().find(|s| s.chunk_id == 1).expect("chunk 1");
assert!((chunk1.score - 0.8).abs() < f32::EPSILON);
}
#[test]
fn compile_valid_pattern() {
assert!(compile_pattern(r"fn\s+\w+").is_some());
}
#[test]
fn compile_invalid_pattern() {
assert!(compile_pattern(r"[invalid").is_none());
}
}