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}