Skip to main content

lean_ctx/core/knowledge/
core.rs

1use chrono::Utc;
2
3use super::ranking::{fact_version_id_v1, hash_project_root, string_similarity};
4use super::types::{
5    Contradiction, ContradictionSeverity, KnowledgeFact, ProjectKnowledge, ProjectPattern,
6};
7use crate::core::memory_boundary::FactPrivacy;
8use crate::core::memory_policy::MemoryPolicy;
9
10impl ProjectKnowledge {
11    pub fn run_memory_lifecycle(
12        &mut self,
13        policy: &MemoryPolicy,
14    ) -> crate::core::memory_lifecycle::LifecycleReport {
15        let cfg = crate::core::memory_lifecycle::LifecycleConfig {
16            max_facts: policy.knowledge.max_facts,
17            decay_rate_per_day: policy.lifecycle.decay_rate,
18            low_confidence_threshold: policy.lifecycle.low_confidence_threshold,
19            stale_days: policy.lifecycle.stale_days,
20            consolidation_similarity: policy.lifecycle.similarity_threshold,
21        };
22        crate::core::memory_lifecycle::run_lifecycle(&mut self.facts, &cfg)
23    }
24
25    pub fn new(project_root: &str) -> Self {
26        Self {
27            project_root: project_root.to_string(),
28            project_hash: hash_project_root(project_root),
29            facts: Vec::new(),
30            patterns: Vec::new(),
31            history: Vec::new(),
32            updated_at: Utc::now(),
33        }
34    }
35
36    pub fn check_contradiction(
37        &self,
38        category: &str,
39        key: &str,
40        new_value: &str,
41        policy: &MemoryPolicy,
42    ) -> Option<Contradiction> {
43        let existing = self
44            .facts
45            .iter()
46            .find(|f| f.category == category && f.key == key && f.is_current())?;
47
48        if existing.value.to_lowercase() == new_value.to_lowercase() {
49            return None;
50        }
51
52        let similarity = string_similarity(&existing.value, new_value);
53        if similarity > 0.8 {
54            return None;
55        }
56
57        let severity = if existing.confidence >= 0.9 && existing.confirmation_count >= 2 {
58            ContradictionSeverity::High
59        } else if existing.confidence >= policy.knowledge.contradiction_threshold {
60            ContradictionSeverity::Medium
61        } else {
62            ContradictionSeverity::Low
63        };
64
65        let resolution = match severity {
66            ContradictionSeverity::High => format!(
67                "High-confidence fact [{category}/{key}] changed: '{}' -> '{new_value}' (was confirmed {}x). Previous value archived.",
68                existing.value, existing.confirmation_count
69            ),
70            ContradictionSeverity::Medium => format!(
71                "Fact [{category}/{key}] updated: '{}' -> '{new_value}'",
72                existing.value
73            ),
74            ContradictionSeverity::Low => format!(
75                "Low-confidence fact [{category}/{key}] replaced: '{}' -> '{new_value}'",
76                existing.value
77            ),
78        };
79
80        Some(Contradiction {
81            existing_key: key.to_string(),
82            existing_value: existing.value.clone(),
83            new_value: new_value.to_string(),
84            category: category.to_string(),
85            severity,
86            resolution,
87        })
88    }
89
90    pub fn remember(
91        &mut self,
92        category: &str,
93        key: &str,
94        value: &str,
95        session_id: &str,
96        confidence: f32,
97        policy: &MemoryPolicy,
98    ) -> Option<Contradiction> {
99        let contradiction = self.check_contradiction(category, key, value, policy);
100
101        if let Some(existing) = self
102            .facts
103            .iter_mut()
104            .find(|f| f.category == category && f.key == key && f.is_current())
105        {
106            let now = Utc::now();
107            let same_value_ci = existing.value.to_lowercase() == value.to_lowercase();
108            let similarity = string_similarity(&existing.value, value);
109
110            if existing.value == value || same_value_ci || similarity > 0.8 {
111                existing.last_confirmed = now;
112                existing.source_session = session_id.to_string();
113                existing.confidence = f32::midpoint(existing.confidence, confidence);
114                existing.confirmation_count += 1;
115
116                if existing.value != value && similarity > 0.8 && value.len() > existing.value.len()
117                {
118                    existing.value = value.to_string();
119                }
120            } else {
121                let superseded = fact_version_id_v1(existing);
122                existing.valid_until = Some(now);
123                existing.valid_from = existing.valid_from.or(Some(existing.created_at));
124
125                self.facts.push(KnowledgeFact {
126                    category: category.to_string(),
127                    key: key.to_string(),
128                    value: value.to_string(),
129                    source_session: session_id.to_string(),
130                    confidence,
131                    created_at: now,
132                    last_confirmed: now,
133                    retrieval_count: 0,
134                    last_retrieved: None,
135                    valid_from: Some(now),
136                    valid_until: None,
137                    supersedes: Some(superseded),
138                    confirmation_count: 1,
139                    feedback_up: 0,
140                    feedback_down: 0,
141                    last_feedback: None,
142                    privacy: FactPrivacy::default(),
143                    imported_from: None,
144                });
145            }
146        } else {
147            let now = Utc::now();
148            self.facts.push(KnowledgeFact {
149                category: category.to_string(),
150                key: key.to_string(),
151                value: value.to_string(),
152                source_session: session_id.to_string(),
153                confidence,
154                created_at: now,
155                last_confirmed: now,
156                retrieval_count: 0,
157                last_retrieved: None,
158                valid_from: Some(now),
159                valid_until: None,
160                supersedes: None,
161                confirmation_count: 1,
162                feedback_up: 0,
163                feedback_down: 0,
164                last_feedback: None,
165                privacy: FactPrivacy::default(),
166                imported_from: None,
167            });
168        }
169
170        if self.facts.len() > policy.knowledge.max_facts.saturating_mul(2) {
171            let _ = self.run_memory_lifecycle(policy);
172        }
173
174        self.updated_at = Utc::now();
175
176        let action = if contradiction.is_some() {
177            "contradict"
178        } else {
179            "remember"
180        };
181        crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
182            category: category.to_string(),
183            key: key.to_string(),
184            action: action.to_string(),
185        });
186
187        contradiction
188    }
189
190    pub fn add_pattern(
191        &mut self,
192        pattern_type: &str,
193        description: &str,
194        examples: Vec<String>,
195        session_id: &str,
196        policy: &MemoryPolicy,
197    ) {
198        if let Some(existing) = self
199            .patterns
200            .iter_mut()
201            .find(|p| p.pattern_type == pattern_type && p.description == description)
202        {
203            for ex in &examples {
204                if !existing.examples.contains(ex) {
205                    existing.examples.push(ex.clone());
206                }
207            }
208            return;
209        }
210
211        self.patterns.push(ProjectPattern {
212            pattern_type: pattern_type.to_string(),
213            description: description.to_string(),
214            examples,
215            source_session: session_id.to_string(),
216            created_at: Utc::now(),
217        });
218
219        if self.patterns.len() > policy.knowledge.max_patterns {
220            self.patterns.truncate(policy.knowledge.max_patterns);
221        }
222        self.updated_at = Utc::now();
223    }
224
225    pub fn remove_fact(&mut self, category: &str, key: &str) -> bool {
226        let before = self.facts.len();
227        self.facts
228            .retain(|f| !(f.category == category && f.key == key));
229        let removed = self.facts.len() < before;
230        if removed {
231            self.updated_at = Utc::now();
232        }
233        removed
234    }
235}