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