lean_ctx/core/
negative_knowledge.rs1use std::collections::{HashMap, HashSet};
9use std::sync::{Mutex, PoisonError};
10
11static GLOBAL: Mutex<Option<NegativeKnowledge>> = Mutex::new(None);
12
13pub fn global() -> std::sync::MutexGuard<'static, Option<NegativeKnowledge>> {
15 GLOBAL.lock().unwrap_or_else(PoisonError::into_inner)
16}
17
18#[derive(Debug, Clone, Default)]
20pub struct NegativeKnowledge {
21 irrelevant_files: HashSet<String>,
23 dead_end_queries: HashSet<String>,
25 absent_features: HashMap<String, HashSet<String>>,
27}
28
29impl NegativeKnowledge {
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn mark_irrelevant(&mut self, path: &str) {
36 self.irrelevant_files.insert(path.to_string());
37 }
38
39 pub fn is_irrelevant(&self, path: &str) -> bool {
41 self.irrelevant_files.contains(path)
42 }
43
44 pub fn record_dead_end_query(&mut self, query: &str) {
46 self.dead_end_queries.insert(query.to_lowercase());
47 }
48
49 pub fn is_dead_end_query(&self, query: &str) -> bool {
51 self.dead_end_queries.contains(&query.to_lowercase())
52 }
53
54 pub fn record_absent(&mut self, path: &str, feature: &str) {
56 self.absent_features
57 .entry(path.to_string())
58 .or_default()
59 .insert(feature.to_string());
60 }
61
62 pub fn is_absent(&self, path: &str, feature: &str) -> bool {
64 self.absent_features
65 .get(path)
66 .is_some_and(|features| features.contains(feature))
67 }
68
69 pub fn invalidate(&mut self, path: &str) {
71 self.irrelevant_files.remove(path);
72 self.absent_features.remove(path);
73 }
74
75 pub fn stats(&self) -> (usize, usize, usize) {
77 (
78 self.irrelevant_files.len(),
79 self.dead_end_queries.len(),
80 self.absent_features.values().map(HashSet::len).sum(),
81 )
82 }
83
84 pub fn reset(&mut self) {
85 self.irrelevant_files.clear();
86 self.dead_end_queries.clear();
87 self.absent_features.clear();
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn mark_and_check_irrelevant() {
97 let mut nk = NegativeKnowledge::new();
98 assert!(!nk.is_irrelevant("src/utils.rs"));
99 nk.mark_irrelevant("src/utils.rs");
100 assert!(nk.is_irrelevant("src/utils.rs"));
101 }
102
103 #[test]
104 fn dead_end_queries_case_insensitive() {
105 let mut nk = NegativeKnowledge::new();
106 nk.record_dead_end_query("DatabasePool");
107 assert!(nk.is_dead_end_query("databasepool"));
108 assert!(nk.is_dead_end_query("DATABASEPOOL"));
109 }
110
111 #[test]
112 fn absent_features_per_file() {
113 let mut nk = NegativeKnowledge::new();
114 nk.record_absent("src/db.rs", "connection_pool");
115 assert!(nk.is_absent("src/db.rs", "connection_pool"));
116 assert!(!nk.is_absent("src/db.rs", "query_builder"));
117 assert!(!nk.is_absent("src/other.rs", "connection_pool"));
118 }
119
120 #[test]
121 fn invalidate_clears_file_knowledge() {
122 let mut nk = NegativeKnowledge::new();
123 nk.mark_irrelevant("src/lib.rs");
124 nk.record_absent("src/lib.rs", "foo");
125 nk.invalidate("src/lib.rs");
126 assert!(!nk.is_irrelevant("src/lib.rs"));
127 assert!(!nk.is_absent("src/lib.rs", "foo"));
128 }
129
130 #[test]
131 fn stats_counts() {
132 let mut nk = NegativeKnowledge::new();
133 nk.mark_irrelevant("a.rs");
134 nk.mark_irrelevant("b.rs");
135 nk.record_dead_end_query("q1");
136 nk.record_absent("c.rs", "x");
137 nk.record_absent("c.rs", "y");
138 assert_eq!(nk.stats(), (2, 1, 2));
139 }
140}