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}