lean_ctx/core/knowledge/
persist.rs1use chrono::Utc;
2use std::path::PathBuf;
3
4use super::ranking::hash_project_root;
5use super::types::{ConsolidatedInsight, KnowledgeFact, ProjectKnowledge, ProjectPattern};
6use crate::core::memory_policy::MemoryPolicy;
7
8fn knowledge_dir(project_hash: &str) -> Result<PathBuf, String> {
9 Ok(crate::core::data_dir::lean_ctx_data_dir()?
10 .join("knowledge")
11 .join(project_hash))
12}
13
14impl ProjectKnowledge {
15 pub fn save(&self) -> Result<(), String> {
16 let dir = knowledge_dir(&self.project_hash)?;
17 std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
18
19 let path = dir.join("knowledge.json");
20 let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
21 std::fs::write(&path, json).map_err(|e| e.to_string())
22 }
23
24 pub fn load(project_root: &str) -> Option<Self> {
25 let hash = hash_project_root(project_root);
26 let dir = knowledge_dir(&hash).ok()?;
27 let path = dir.join("knowledge.json");
28
29 if let Ok(content) = std::fs::read_to_string(&path) {
30 if let Ok(k) = serde_json::from_str::<Self>(&content) {
31 return Some(k);
32 }
33 }
34
35 let old_hash = crate::core::project_hash::hash_path_only(project_root);
36 if old_hash != hash {
37 crate::core::project_hash::migrate_if_needed(&old_hash, &hash, project_root);
38 if let Ok(content) = std::fs::read_to_string(&path) {
39 if let Ok(mut k) = serde_json::from_str::<Self>(&content) {
40 k.project_hash = hash;
41 let _ = k.save();
42 return Some(k);
43 }
44 }
45 }
46
47 None
48 }
49
50 pub fn load_or_create(project_root: &str) -> Self {
51 Self::load(project_root).unwrap_or_else(|| Self::new(project_root))
52 }
53
54 pub fn migrate_legacy_empty_root(
57 target_root: &str,
58 policy: &MemoryPolicy,
59 ) -> Result<bool, String> {
60 if target_root.trim().is_empty() {
61 return Ok(false);
62 }
63
64 let Some(legacy) = Self::load("") else {
65 return Ok(false);
66 };
67
68 if !legacy.project_root.trim().is_empty() {
69 return Ok(false);
70 }
71 if legacy.facts.is_empty() && legacy.patterns.is_empty() && legacy.history.is_empty() {
72 return Ok(false);
73 }
74
75 let mut target = Self::load_or_create(target_root);
76
77 fn fact_key(f: &KnowledgeFact) -> String {
78 format!(
79 "{}|{}|{}|{}|{}",
80 f.category, f.key, f.value, f.source_session, f.created_at
81 )
82 }
83 fn pattern_key(p: &ProjectPattern) -> String {
84 format!(
85 "{}|{}|{}|{}",
86 p.pattern_type, p.description, p.source_session, p.created_at
87 )
88 }
89 fn history_key(h: &ConsolidatedInsight) -> String {
90 format!(
91 "{}|{}|{}",
92 h.summary,
93 h.from_sessions.join(","),
94 h.timestamp
95 )
96 }
97
98 let mut seen_facts: std::collections::HashSet<String> =
99 target.facts.iter().map(fact_key).collect();
100 for f in legacy.facts {
101 if seen_facts.insert(fact_key(&f)) {
102 target.facts.push(f);
103 }
104 }
105
106 let mut seen_patterns: std::collections::HashSet<String> =
107 target.patterns.iter().map(pattern_key).collect();
108 for p in legacy.patterns {
109 if seen_patterns.insert(pattern_key(&p)) {
110 target.patterns.push(p);
111 }
112 }
113
114 let mut seen_history: std::collections::HashSet<String> =
115 target.history.iter().map(history_key).collect();
116 for h in legacy.history {
117 if seen_history.insert(history_key(&h)) {
118 target.history.push(h);
119 }
120 }
121
122 target.facts.sort_by(|a, b| {
123 b.created_at
124 .cmp(&a.created_at)
125 .then_with(|| b.confidence.total_cmp(&a.confidence))
126 });
127 if target.facts.len() > policy.knowledge.max_facts {
128 target.facts.truncate(policy.knowledge.max_facts);
129 }
130 target
131 .patterns
132 .sort_by_key(|x| std::cmp::Reverse(x.created_at));
133 if target.patterns.len() > policy.knowledge.max_patterns {
134 target.patterns.truncate(policy.knowledge.max_patterns);
135 }
136 target
137 .history
138 .sort_by_key(|x| std::cmp::Reverse(x.timestamp));
139 if target.history.len() > policy.knowledge.max_history {
140 target.history.truncate(policy.knowledge.max_history);
141 }
142
143 target.updated_at = Utc::now();
144 target.save()?;
145
146 let legacy_hash = crate::core::project_hash::hash_path_only("");
147 let legacy_dir = knowledge_dir(&legacy_hash)?;
148 let legacy_path = legacy_dir.join("knowledge.json");
149 if legacy_path.exists() {
150 let ts = Utc::now().format("%Y%m%d-%H%M%S");
151 let backup = legacy_dir.join(format!("knowledge.legacy-empty-root.{ts}.json"));
152 std::fs::rename(&legacy_path, &backup).map_err(|e| e.to_string())?;
153 }
154
155 Ok(true)
156 }
157}