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 judged_pairs: Vec::new(),
35 }
36 }
37
38 pub fn check_contradiction(
39 &self,
40 category: &str,
41 key: &str,
42 new_value: &str,
43 policy: &MemoryPolicy,
44 ) -> Option<Contradiction> {
45 let existing = self
46 .facts
47 .iter()
48 .find(|f| f.category == category && f.key == key && f.is_current())?;
49
50 if existing.value.to_lowercase() == new_value.to_lowercase() {
51 return None;
52 }
53
54 let similarity = string_similarity(&existing.value, new_value);
55 if similarity > 0.8 {
56 return None;
57 }
58
59 let severity = if existing.confidence >= 0.9 && existing.confirmation_count >= 2 {
60 ContradictionSeverity::High
61 } else if existing.confidence >= policy.knowledge.contradiction_threshold {
62 ContradictionSeverity::Medium
63 } else {
64 ContradictionSeverity::Low
65 };
66
67 let resolution = match severity {
68 ContradictionSeverity::High => format!(
69 "High-confidence fact [{category}/{key}] changed: '{}' -> '{new_value}' (was confirmed {}x). Previous value archived.",
70 existing.value, existing.confirmation_count
71 ),
72 ContradictionSeverity::Medium => format!(
73 "Fact [{category}/{key}] updated: '{}' -> '{new_value}'",
74 existing.value
75 ),
76 ContradictionSeverity::Low => format!(
77 "Low-confidence fact [{category}/{key}] replaced: '{}' -> '{new_value}'",
78 existing.value
79 ),
80 };
81
82 Some(Contradiction {
83 existing_key: key.to_string(),
84 existing_value: existing.value.clone(),
85 new_value: new_value.to_string(),
86 category: category.to_string(),
87 severity,
88 resolution,
89 })
90 }
91
92 pub fn remember(
93 &mut self,
94 category: &str,
95 key: &str,
96 value: &str,
97 session_id: &str,
98 confidence: f32,
99 policy: &MemoryPolicy,
100 ) -> Option<Contradiction> {
101 let contradiction = self.check_contradiction(category, key, value, policy);
102
103 if let Some(existing) = self
104 .facts
105 .iter_mut()
106 .find(|f| f.category == category && f.key == key && f.is_current())
107 {
108 let now = Utc::now();
109 let same_value_ci = existing.value.to_lowercase() == value.to_lowercase();
110 let similarity = string_similarity(&existing.value, value);
111
112 if existing.value == value || same_value_ci || similarity > 0.8 {
113 existing.last_confirmed = now;
114 existing.source_session = session_id.to_string();
115 existing.confidence = f32::midpoint(existing.confidence, confidence);
116 existing.confirmation_count += 1;
117 existing.revision_count += 1;
118
119 if existing.value != value && similarity > 0.8 && value.len() > existing.value.len()
120 {
121 existing.value = value.to_string();
122 }
123 } else {
124 let superseded = fact_version_id_v1(existing);
125 let next_revision = existing.revision_count + 1;
126 existing.valid_until = Some(now);
127 existing.valid_from = existing.valid_from.or(Some(existing.created_at));
128
129 self.facts.push(KnowledgeFact {
130 category: category.to_string(),
131 key: key.to_string(),
132 value: value.to_string(),
133 source_session: session_id.to_string(),
134 confidence,
135 created_at: now,
136 last_confirmed: now,
137 retrieval_count: 0,
138 last_retrieved: None,
139 valid_from: Some(now),
140 valid_until: None,
141 supersedes: Some(superseded),
142 confirmation_count: 1,
143 feedback_up: 0,
144 feedback_down: 0,
145 last_feedback: None,
146 privacy: FactPrivacy::default(),
147 imported_from: None,
148 archetype: KnowledgeArchetype::default(),
149 fidelity: None,
150 revision_count: next_revision,
151 });
152 }
153 } else {
154 let now = Utc::now();
155 self.facts.push(KnowledgeFact {
156 category: category.to_string(),
157 key: key.to_string(),
158 value: value.to_string(),
159 source_session: session_id.to_string(),
160 confidence,
161 created_at: now,
162 last_confirmed: now,
163 retrieval_count: 0,
164 last_retrieved: None,
165 valid_from: Some(now),
166 valid_until: None,
167 supersedes: None,
168 confirmation_count: 1,
169 feedback_up: 0,
170 feedback_down: 0,
171 last_feedback: None,
172 privacy: FactPrivacy::default(),
173 imported_from: None,
174 archetype: KnowledgeArchetype::default(),
175 fidelity: None,
176 revision_count: 1,
177 });
178 }
179
180 if self.facts.len() > policy.knowledge.max_facts.saturating_mul(2) {
181 let _ = self.run_memory_lifecycle(policy);
182 }
183
184 self.updated_at = Utc::now();
185
186 let action = if contradiction.is_some() {
187 "contradict"
188 } else {
189 "remember"
190 };
191 crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
192 category: category.to_string(),
193 key: key.to_string(),
194 action: action.to_string(),
195 });
196
197 contradiction
198 }
199
200 pub fn add_pattern(
201 &mut self,
202 pattern_type: &str,
203 description: &str,
204 examples: Vec<String>,
205 session_id: &str,
206 policy: &MemoryPolicy,
207 ) {
208 if let Some(existing) = self
209 .patterns
210 .iter_mut()
211 .find(|p| p.pattern_type == pattern_type && p.description == description)
212 {
213 for ex in &examples {
214 if !existing.examples.contains(ex) {
215 existing.examples.push(ex.clone());
216 }
217 }
218 return;
219 }
220
221 self.patterns.push(ProjectPattern {
222 pattern_type: pattern_type.to_string(),
223 description: description.to_string(),
224 examples,
225 source_session: session_id.to_string(),
226 created_at: Utc::now(),
227 });
228
229 if self.patterns.len() > policy.knowledge.max_patterns {
230 self.patterns.truncate(policy.knowledge.max_patterns);
231 }
232 self.updated_at = Utc::now();
233 }
234
235 pub fn remove_fact(&mut self, category: &str, key: &str) -> bool {
236 let before = self.facts.len();
237 self.facts
238 .retain(|f| !(f.category == category && f.key == key));
239 let removed = self.facts.len() < before;
240 if removed {
241 self.updated_at = Utc::now();
242 }
243 removed
244 }
245}