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