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