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