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