1use chrono::Utc;
2
3use super::ranking::{fact_version_id_v1, hash_project_root, string_similarity};
4use super::types::{
5 AdmissionResult, Contradiction, ContradictionSeverity, KnowledgeArchetype, KnowledgeFact,
6 ProjectKnowledge, 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 forgetting_model: crate::core::memory_lifecycle::ForgettingModel::parse(
23 &policy.lifecycle.forgetting_model,
24 ),
25 base_stability_days: policy.lifecycle.base_stability_days,
26 archetype_aware_decay: policy.lifecycle.archetype_aware_decay,
27 prune_unretrieved_after_days: policy.lifecycle.prune_unretrieved_after_days,
28 };
29 crate::core::memory_lifecycle::run_lifecycle(&mut self.facts, &cfg)
30 }
31
32 pub fn compact_low_value_clusters(&mut self, policy: &MemoryPolicy) -> u32 {
38 if !policy.compaction.enabled {
39 return 0;
40 }
41 let cfg = crate::core::memory_lifecycle::ClusterCompactionConfig {
42 min_cluster: policy.compaction.min_cluster,
43 similarity: policy.compaction.similarity,
44 max_confidence: policy.compaction.max_confidence,
45 max_confirmations: policy.compaction.max_confirmations,
46 };
47 let (collapsed, archived) =
48 crate::core::memory_lifecycle::compact_clusters(&mut self.facts, &cfg);
49 if !archived.is_empty() {
50 let _ = crate::core::memory_lifecycle::archive_facts(&archived);
51 }
52 collapsed as u32
53 }
54
55 pub fn new(project_root: &str) -> Self {
56 Self {
57 project_root: project_root.to_string(),
58 project_hash: hash_project_root(project_root),
59 facts: Vec::new(),
60 patterns: Vec::new(),
61 history: Vec::new(),
62 updated_at: Utc::now(),
63 judged_pairs: Vec::new(),
64 }
65 }
66
67 pub fn check_contradiction(
68 &self,
69 category: &str,
70 key: &str,
71 new_value: &str,
72 policy: &MemoryPolicy,
73 ) -> Option<Contradiction> {
74 let existing = self
75 .facts
76 .iter()
77 .find(|f| f.category == category && f.key == key && f.is_current())?;
78
79 if existing.value.to_lowercase() == new_value.to_lowercase() {
80 return None;
81 }
82
83 let similarity = string_similarity(&existing.value, new_value);
84 if similarity > 0.8 {
85 return None;
86 }
87
88 let severity = if existing.confidence >= 0.9 && existing.confirmation_count >= 2 {
89 ContradictionSeverity::High
90 } else if existing.confidence >= policy.knowledge.contradiction_threshold {
91 ContradictionSeverity::Medium
92 } else {
93 ContradictionSeverity::Low
94 };
95
96 let resolution = match severity {
97 ContradictionSeverity::High => format!(
98 "High-confidence fact [{category}/{key}] changed: '{}' -> '{new_value}' (was confirmed {}x). Previous value archived.",
99 existing.value, existing.confirmation_count
100 ),
101 ContradictionSeverity::Medium => format!(
102 "Fact [{category}/{key}] updated: '{}' -> '{new_value}'",
103 existing.value
104 ),
105 ContradictionSeverity::Low => format!(
106 "Low-confidence fact [{category}/{key}] replaced: '{}' -> '{new_value}'",
107 existing.value
108 ),
109 };
110
111 Some(Contradiction {
112 existing_key: key.to_string(),
113 existing_value: existing.value.clone(),
114 new_value: new_value.to_string(),
115 category: category.to_string(),
116 severity,
117 resolution,
118 })
119 }
120
121 pub fn remember(
122 &mut self,
123 category: &str,
124 key: &str,
125 value: &str,
126 session_id: &str,
127 confidence: f32,
128 policy: &MemoryPolicy,
129 ) -> Option<Contradiction> {
130 let contradiction = self.check_contradiction(category, key, value, policy);
131
132 if let Some(existing) = self
133 .facts
134 .iter_mut()
135 .find(|f| f.category == category && f.key == key && f.is_current())
136 {
137 let now = Utc::now();
138 let same_value_ci = existing.value.to_lowercase() == value.to_lowercase();
139 let similarity = string_similarity(&existing.value, value);
140
141 if existing.value == value || same_value_ci || similarity > 0.8 {
142 existing.last_confirmed = now;
143 existing.source_session = session_id.to_string();
144 existing.confidence = f32::midpoint(existing.confidence, confidence);
145 existing.confirmation_count += 1;
146 existing.revision_count += 1;
147
148 if existing.value != value && similarity > 0.8 && value.len() > existing.value.len()
149 {
150 existing.value = value.to_string();
151 }
152 } else {
153 let superseded = fact_version_id_v1(existing);
154 let next_revision = existing.revision_count + 1;
155 existing.valid_until = Some(now);
156 existing.valid_from = existing.valid_from.or(Some(existing.created_at));
157
158 self.facts.push(KnowledgeFact {
159 category: category.to_string(),
160 key: key.to_string(),
161 value: value.to_string(),
162 source_session: session_id.to_string(),
163 confidence,
164 created_at: now,
165 last_confirmed: now,
166 retrieval_count: 0,
167 last_retrieved: None,
168 valid_from: Some(now),
169 valid_until: None,
170 supersedes: Some(superseded),
171 confirmation_count: 1,
172 feedback_up: 0,
173 feedback_down: 0,
174 last_feedback: None,
175 privacy: FactPrivacy::default(),
176 sensitivity: crate::core::sensitivity::classify_content(value),
177 imported_from: None,
178 archetype: KnowledgeArchetype::infer_from_category(category),
179 fidelity: None,
180 revision_count: next_revision,
181 });
182 }
183 } else {
184 let now = Utc::now();
185 self.facts.push(KnowledgeFact {
186 category: category.to_string(),
187 key: key.to_string(),
188 value: value.to_string(),
189 source_session: session_id.to_string(),
190 confidence,
191 created_at: now,
192 last_confirmed: now,
193 retrieval_count: 0,
194 last_retrieved: None,
195 valid_from: Some(now),
196 valid_until: None,
197 supersedes: None,
198 confirmation_count: 1,
199 feedback_up: 0,
200 feedback_down: 0,
201 last_feedback: None,
202 privacy: FactPrivacy::default(),
203 sensitivity: crate::core::sensitivity::classify_content(value),
204 imported_from: None,
205 archetype: KnowledgeArchetype::infer_from_category(category),
206 fidelity: None,
207 revision_count: 1,
208 });
209 }
210
211 if self.facts.len() > policy.knowledge.max_facts {
218 let _ = self.run_memory_lifecycle(policy);
219 }
220
221 self.updated_at = Utc::now();
222
223 let action = if contradiction.is_some() {
224 "contradict"
225 } else {
226 "remember"
227 };
228 crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
229 category: category.to_string(),
230 key: key.to_string(),
231 action: action.to_string(),
232 });
233
234 contradiction
235 }
236
237 pub fn remember_admitted(
245 &mut self,
246 category: &str,
247 key: &str,
248 value: &str,
249 session_id: &str,
250 confidence: f32,
251 policy: &MemoryPolicy,
252 ) -> AdmissionResult {
253 let adm = &policy.admission;
254
255 let has_exact = self
258 .facts
259 .iter()
260 .any(|f| f.category == category && f.key == key && f.is_current());
261 if !adm.enabled || has_exact {
262 return AdmissionResult::Stored(
263 self.remember(category, key, value, session_id, confidence, policy),
264 );
265 }
266
267 if adm.min_salience > 0 {
269 let salience = crate::core::memory_salience::text_salience(value);
270 if salience < adm.min_salience {
271 return AdmissionResult::RejectedLowSalience {
272 salience,
273 floor: adm.min_salience,
274 };
275 }
276 }
277
278 if adm.auto_merge_similarity > 0.0
280 && let Some(merged) = self.merge_near_duplicate(
281 category,
282 value,
283 session_id,
284 confidence,
285 adm.auto_merge_similarity,
286 )
287 {
288 return merged;
289 }
290
291 AdmissionResult::Stored(self.remember(category, key, value, session_id, confidence, policy))
292 }
293
294 fn merge_near_duplicate(
300 &mut self,
301 category: &str,
302 value: &str,
303 session_id: &str,
304 confidence: f32,
305 threshold: f32,
306 ) -> Option<AdmissionResult> {
307 let mut best: Option<(usize, f32)> = None;
308 for (i, f) in self.facts.iter().enumerate() {
309 if !f.is_current() || f.category != category {
310 continue;
311 }
312 let sim = string_similarity(value, &f.value);
313 if sim >= threshold && best.is_none_or(|(_, bs)| sim > bs) {
314 best = Some((i, sim));
315 }
316 }
317 let (idx, _) = best?;
318 let now = Utc::now();
319 let f = &mut self.facts[idx];
320 f.last_confirmed = now;
321 f.source_session = session_id.to_string();
322 f.confidence = f32::midpoint(f.confidence, confidence);
323 f.confirmation_count += 1;
324 f.revision_count += 1;
325 if value.len() > f.value.len() {
326 f.value = value.to_string();
327 }
328 Some(AdmissionResult::Merged {
329 category: f.category.clone(),
330 key: f.key.clone(),
331 confirmations: f.confirmation_count,
332 value: f.value.clone(),
333 })
334 }
335
336 pub fn add_pattern(
337 &mut self,
338 pattern_type: &str,
339 description: &str,
340 examples: Vec<String>,
341 session_id: &str,
342 policy: &MemoryPolicy,
343 ) {
344 if let Some(existing) = self
345 .patterns
346 .iter_mut()
347 .find(|p| p.pattern_type == pattern_type && p.description == description)
348 {
349 for ex in &examples {
350 if !existing.examples.contains(ex) {
351 existing.examples.push(ex.clone());
352 }
353 }
354 return;
355 }
356
357 self.patterns.push(ProjectPattern {
358 pattern_type: pattern_type.to_string(),
359 description: description.to_string(),
360 examples,
361 source_session: session_id.to_string(),
362 created_at: Utc::now(),
363 });
364
365 if self.patterns.len() > policy.knowledge.max_patterns {
366 self.patterns.truncate(policy.knowledge.max_patterns);
367 }
368 self.updated_at = Utc::now();
369 }
370
371 pub fn remove_fact(&mut self, category: &str, key: &str) -> bool {
372 let before = self.facts.len();
373 self.facts
374 .retain(|f| !(f.category == category && f.key == key));
375 let removed = self.facts.len() < before;
376 if removed {
377 self.updated_at = Utc::now();
378 }
379 removed
380 }
381}