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
13pub(super) fn admit_fact_fields<const N: usize>(
16 project_root: &str,
17 fields: [&str; N],
18) -> Option<[String; N]> {
19 let admission = super::protection::KnowledgeAdmission::capture(project_root).ok()?;
20 let mut admitted: [String; N] = std::array::from_fn(|_| String::new());
21 for (slot, field) in admitted.iter_mut().zip(fields) {
22 *slot = admission.admit_text(field)?;
23 }
24 admission.authorize_gateway().ok()?;
25 Some(admitted)
26}
27
28impl ProjectKnowledge {
29 pub fn run_memory_lifecycle(
35 &mut self,
36 policy: &MemoryPolicy,
37 ) -> Result<crate::core::memory_lifecycle::LifecycleReport, String> {
38 super::protection::current(&self.project_root)?;
39 let cfg = crate::core::memory_lifecycle::LifecycleConfig::from_policy(policy);
40 let before = self.facts.clone();
41 let report = crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
42 std::cell::RefCell::new(Some(std::path::PathBuf::from(&self.project_root))),
43 || crate::core::memory_lifecycle::run_lifecycle(&mut self.facts, &cfg),
44 );
45 if report.is_ok() {
46 self.rebuild_index();
47 } else {
48 self.facts = before;
49 }
50 report
51 }
52
53 pub fn new(project_root: &str) -> Self {
54 Self {
55 project_root: project_root.to_string(),
56 project_hash: hash_project_root(project_root),
57 facts: Vec::new(),
58 patterns: Vec::new(),
59 history: Vec::new(),
60 updated_at: Utc::now(),
61 judged_pairs: Vec::new(),
62 index: Default::default(),
63 withheld: Vec::new(),
64 }
65 }
66
67 pub fn rebuild_index(&mut self) {
70 self.index = build_knowledge_index(&self.facts);
71 }
72
73 pub fn check_contradiction(
74 &self,
75 category: &str,
76 key: &str,
77 new_value: &str,
78 policy: &MemoryPolicy,
79 ) -> Option<Contradiction> {
80 let existing = self
81 .facts
82 .iter()
83 .find(|f| f.category == category && f.key == key && f.is_current())?;
84
85 if existing.value.to_lowercase() == new_value.to_lowercase() {
86 return None;
87 }
88
89 let similarity = string_similarity(&existing.value, new_value);
90 if similarity > 0.8 {
91 return None;
92 }
93
94 let severity = if existing.confidence >= 0.9 && existing.confirmation_count >= 2 {
95 ContradictionSeverity::High
96 } else if existing.confidence >= policy.knowledge.contradiction_threshold {
97 ContradictionSeverity::Medium
98 } else {
99 ContradictionSeverity::Low
100 };
101
102 let resolution = match severity {
103 ContradictionSeverity::High => format!(
104 "High-confidence fact [{category}/{key}] changed: '{}' -> '{new_value}' (was confirmed {}x). Previous value archived.",
105 existing.value, existing.confirmation_count
106 ),
107 ContradictionSeverity::Medium => format!(
108 "Fact [{category}/{key}] updated: '{}' -> '{new_value}'",
109 existing.value
110 ),
111 ContradictionSeverity::Low => format!(
112 "Low-confidence fact [{category}/{key}] replaced: '{}' -> '{new_value}'",
113 existing.value
114 ),
115 };
116
117 Some(Contradiction {
118 existing_key: key.to_string(),
119 existing_value: existing.value.clone(),
120 new_value: new_value.to_string(),
121 category: category.to_string(),
122 severity,
123 resolution,
124 })
125 }
126
127 pub fn remember(
128 &mut self,
129 category: &str,
130 key: &str,
131 value: &str,
132 session_id: &str,
133 confidence: f32,
134 policy: &MemoryPolicy,
135 ) -> Option<Contradiction> {
136 self.remember_with_origin(
137 category,
138 key,
139 value,
140 session_id,
141 confidence,
142 policy,
143 super::FactOrigin::Local,
144 )
145 }
146
147 pub(crate) fn remember_with_origin(
148 &mut self,
149 category: &str,
150 key: &str,
151 value: &str,
152 session_id: &str,
153 confidence: f32,
154 policy: &MemoryPolicy,
155 origin: super::FactOrigin,
156 ) -> Option<Contradiction> {
157 if super::persist::is_machine_derived(category, key)
162 && !super::persist::machine_derived_writes_allowed()
163 {
164 return None;
165 }
166 let Some([category, key, value]) =
170 admit_fact_fields(&self.project_root, [category, key, value])
171 else {
172 tracing::info!("[knowledge] fact withheld by the context gateway");
173 return None;
174 };
175 let (category, key, value) = (category.as_str(), key.as_str(), value.as_str());
176 let contradiction = self.check_contradiction(category, key, value, policy);
177
178 if let Some(existing) = self.facts.iter_mut().find(|f| {
179 f.category == category && f.key == key && f.is_current() && f.origin == origin
180 }) {
181 let now = Utc::now();
182 let same_value_ci = existing.value.to_lowercase() == value.to_lowercase();
183 let similarity = string_similarity(&existing.value, value);
184
185 if existing.value == value || same_value_ci || similarity > 0.8 {
186 existing.last_confirmed = now;
187 existing.source_session = session_id.to_string();
188 existing.confidence = f32::midpoint(existing.confidence, confidence);
189 existing.confirmation_count += 1;
190 existing.revision_count += 1;
191
192 if existing.value != value && similarity > 0.8 && value.len() > existing.value.len()
193 {
194 existing.value = value.to_string();
195 }
196 } else {
197 let superseded = fact_version_id_v1(existing);
198 let next_revision = existing.revision_count + 1;
199 let validity = existing.validity;
201 existing.valid_until = Some(now);
202 existing.valid_from = existing.valid_from.or(Some(existing.created_at));
203
204 self.facts.push(KnowledgeFact {
205 anchors: Vec::new(),
206 review: None,
207 capture: Default::default(),
208 origin: origin.clone(),
209 category: category.to_string(),
210 key: key.to_string(),
211 value: value.to_string(),
212 source_session: session_id.to_string(),
213 confidence,
214 created_at: now,
215 last_confirmed: now,
216 retrieval_count: 0,
217 last_retrieved: None,
218 valid_from: Some(now),
219 valid_until: None,
220 supersedes: Some(superseded),
221 confirmation_count: 1,
222 feedback_up: 0,
223 feedback_down: 0,
224 last_feedback: None,
225 privacy: FactPrivacy::default(),
226 sensitivity: crate::core::sensitivity::classify_content(value),
227 imported_from: None,
228 archetype: KnowledgeArchetype::infer_from_category(category),
229 fidelity: None,
230 revision_count: next_revision,
231 status: None,
232 validity,
233 });
234 }
235 } else {
236 let now = Utc::now();
237 self.facts.push(KnowledgeFact {
238 anchors: Vec::new(),
239 review: None,
240 capture: Default::default(),
241 origin,
242 category: category.to_string(),
243 key: key.to_string(),
244 value: value.to_string(),
245 source_session: session_id.to_string(),
246 confidence,
247 created_at: now,
248 last_confirmed: now,
249 retrieval_count: 0,
250 last_retrieved: None,
251 valid_from: Some(now),
252 valid_until: None,
253 supersedes: None,
254 confirmation_count: 1,
255 feedback_up: 0,
256 feedback_down: 0,
257 last_feedback: None,
258 privacy: FactPrivacy::default(),
259 sensitivity: crate::core::sensitivity::classify_content(value),
260 imported_from: None,
261 archetype: KnowledgeArchetype::infer_from_category(category),
262 fidelity: None,
263 revision_count: 1,
264 status: None,
265 validity: None,
266 });
267 }
268
269 self.updated_at = Utc::now();
270 self.rebuild_index();
271
272 let action = if contradiction.is_some() {
273 "contradict"
274 } else {
275 "remember"
276 };
277 crate::core::events::emit(crate::core::events::EventKind::KnowledgeUpdate {
278 category: category.to_string(),
279 key: key.to_string(),
280 action: action.to_string(),
281 });
282
283 contradiction
284 }
285
286 pub fn remember_admitted(
294 &mut self,
295 category: &str,
296 key: &str,
297 value: &str,
298 session_id: &str,
299 confidence: f32,
300 policy: &MemoryPolicy,
301 ) -> AdmissionResult {
302 let adm = &policy.admission;
303
304 let has_exact = self
307 .facts
308 .iter()
309 .any(|f| f.category == category && f.key == key && f.is_current());
310 if !adm.enabled || has_exact {
311 return AdmissionResult::Stored(
312 self.remember(category, key, value, session_id, confidence, policy),
313 );
314 }
315
316 if adm.min_salience > 0 {
318 let salience = crate::core::memory_salience::text_salience(value);
319 if salience < adm.min_salience {
320 return AdmissionResult::RejectedLowSalience {
321 salience,
322 floor: adm.min_salience,
323 };
324 }
325 }
326
327 if adm.auto_merge_similarity > 0.0
329 && let Some(merged) = self.merge_near_duplicate(
330 category,
331 value,
332 session_id,
333 confidence,
334 adm.auto_merge_similarity,
335 )
336 {
337 return merged;
338 }
339
340 AdmissionResult::Stored(self.remember(category, key, value, session_id, confidence, policy))
341 }
342
343 fn merge_near_duplicate(
349 &mut self,
350 category: &str,
351 value: &str,
352 session_id: &str,
353 confidence: f32,
354 threshold: f32,
355 ) -> Option<AdmissionResult> {
356 let mut best: Option<(usize, f32)> = None;
357 for (i, f) in self.facts.iter().enumerate() {
358 if !f.is_current() || f.category != category || f.origin != super::FactOrigin::Local {
359 continue;
360 }
361 let sim = string_similarity(value, &f.value);
362 if sim >= threshold && best.is_none_or(|(_, bs)| sim > bs) {
363 best = Some((i, sim));
364 }
365 }
366 let (idx, _) = best?;
367 let now = Utc::now();
368 let f = &mut self.facts[idx];
369 f.last_confirmed = now;
370 f.source_session = session_id.to_string();
371 f.confidence = f32::midpoint(f.confidence, confidence);
372 f.confirmation_count += 1;
373 f.revision_count += 1;
374 if value.len() > f.value.len() {
375 f.value = value.to_string();
376 }
377 let result = AdmissionResult::Merged {
378 category: f.category.clone(),
379 key: f.key.clone(),
380 confirmations: f.confirmation_count,
381 value: f.value.clone(),
382 };
383 self.rebuild_index();
384 Some(result)
385 }
386
387 pub fn add_pattern(
388 &mut self,
389 pattern_type: &str,
390 description: &str,
391 examples: Vec<String>,
392 session_id: &str,
393 policy: &MemoryPolicy,
394 ) {
395 if let Some(existing) = self
396 .patterns
397 .iter_mut()
398 .find(|p| p.pattern_type == pattern_type && p.description == description)
399 {
400 for ex in &examples {
401 if !existing.examples.contains(ex) {
402 existing.examples.push(ex.clone());
403 }
404 }
405 return;
406 }
407
408 self.patterns.push(ProjectPattern {
409 pattern_type: pattern_type.to_string(),
410 description: description.to_string(),
411 examples,
412 source_session: session_id.to_string(),
413 created_at: Utc::now(),
414 });
415
416 if let Err(error) = crate::core::memory_capacity::reclaim_store(
420 crate::core::memory_archive::MemoryStore::Patterns,
421 Some(&self.project_hash),
422 &mut self.patterns,
423 policy.knowledge.max_patterns,
424 policy.lifecycle.reclaim_headroom_pct,
425 policy.lifecycle.reclaim_enabled,
426 |a, b| {
427 b.created_at
428 .cmp(&a.created_at)
429 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
430 .then_with(|| a.description.cmp(&b.description))
431 },
432 ) {
433 tracing::warn!(%error, "pattern capacity reclaim failed");
434 }
435 self.updated_at = Utc::now();
436 }
437
438 pub fn remove_fact(&mut self, category: &str, key: &str) -> bool {
439 let before = self.facts.len();
440 self.facts
441 .retain(|f| !(f.category == category && f.key == key));
442 let removed = self.facts.len() < before;
443 if removed {
444 self.updated_at = Utc::now();
445 self.rebuild_index();
446 }
447 removed
448 }
449}