use crate::domain::IndexEntry;
#[cfg(test)]
use crate::domain::Location;
#[derive(Debug)]
pub struct SanitizeResult {
pub kept: Vec<IndexEntry>,
pub pruned: Vec<(IndexEntry, Vec<String>)>,
}
pub fn sanitize_entries(entries: Vec<IndexEntry>, staged_files: &[String]) -> SanitizeResult {
let mut kept = Vec::new();
let mut pruned = Vec::new();
for entry in entries {
let locations = entry.get_locations();
if locations.is_empty() {
kept.push(entry);
continue;
}
let mut matched_any = false;
let mut orphaned_paths = Vec::new();
for loc in &locations {
if location_matches_staged(&loc.file, staged_files) {
matched_any = true;
} else {
orphaned_paths.push(loc.file.clone());
}
}
if matched_any {
kept.push(entry);
} else {
pruned.push((entry, orphaned_paths));
}
}
SanitizeResult { kept, pruned }
}
fn location_matches_staged(loc_file: &str, staged_files: &[String]) -> bool {
let normalized = loc_file.replace('\\', "/");
staged_files.iter().any(|staged| {
let staged_normalized = staged.replace('\\', "/");
if staged_normalized == normalized {
return true;
}
if staged_normalized.ends_with(&format!("/{}", normalized)) {
return true;
}
if normalized.ends_with(&format!("/{}", staged_normalized)) {
return true;
}
if normalized == staged_normalized.trim_start_matches('/') {
return true;
}
if staged_normalized == normalized.trim_start_matches('/') {
return true;
}
false
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{Category, Role};
#[test]
fn test_sanitize_keeps_matching_entries() {
let entries = vec![IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Updated auth logic",
vec![Location::range("src/auth.rs".to_string(), 10, 20)],
)];
let staged = vec!["src/auth.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 1);
assert_eq!(result.pruned.len(), 0);
}
#[test]
fn test_sanitize_prunes_orphaned_entries() {
let entries = vec![IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Updated auth logic",
vec![Location::range("src/auth.rs".to_string(), 10, 20)],
)];
let staged = vec!["src/main.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 0);
assert_eq!(result.pruned.len(), 1);
assert_eq!(result.pruned[0].1, vec!["src/auth.rs"]);
}
#[test]
fn test_sanitize_keeps_general_memories() {
let entries = vec![IndexEntry::new(
Role::User,
Category::Intent,
"General decision without file location",
)];
let staged = vec!["src/main.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 1); assert_eq!(result.pruned.len(), 0);
}
#[test]
fn test_sanitize_partial_match_keeps_entry() {
let entries = vec![IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Updated multiple files",
vec![
Location::range("src/auth.rs".to_string(), 10, 20), Location::range("src/main.rs".to_string(), 5, 10), ],
)];
let staged = vec!["src/main.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 1); assert_eq!(result.pruned.len(), 0);
}
#[test]
fn test_sanitize_all_orphaned_returns_all_pruned() {
let entries = vec![
IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Memory for reverted file",
vec![Location::file("src/reverted.rs".to_string())],
),
IndexEntry::with_locations(
Role::User,
Category::Intent,
"Intent for another reverted file",
vec![Location::line("src/another.rs".to_string(), 5)],
),
];
let staged = vec!["src/main.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 0);
assert_eq!(result.pruned.len(), 2);
}
#[test]
fn test_sanitize_mixed_entries() {
let entries = vec![
IndexEntry::new(Role::User, Category::Intent, "General intent"),
IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Logic for staged file",
vec![Location::file("src/staged.rs".to_string())],
),
IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Logic for reverted file",
vec![Location::file("src/reverted.rs".to_string())],
),
];
let staged = vec!["src/staged.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 2); assert_eq!(result.pruned.len(), 1); }
#[test]
fn test_path_normalization_windows_backslash() {
let entries = vec![IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Updated with Windows path",
vec![Location::file("src\\auth.rs".to_string())],
)];
let staged = vec!["src/auth.rs".to_string()];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 1); }
#[test]
fn test_empty_staged_files_prunes_all_with_locations() {
let entries = vec![
IndexEntry::new(Role::User, Category::Intent, "General memory"),
IndexEntry::with_locations(
Role::Ai,
Category::Reasoning,
"Memory with location",
vec![Location::file("src/file.rs".to_string())],
),
];
let staged: Vec<String> = vec![];
let result = sanitize_entries(entries, &staged);
assert_eq!(result.kept.len(), 1); assert_eq!(result.pruned.len(), 1); }
}