lean_ctx/core/knowledge/
format.rs1use super::ranking::{confidence_stars, sort_fact_for_output};
2use super::types::{ConsolidatedInsight, KnowledgeFact, ProjectKnowledge};
3use crate::core::memory_policy::MemoryPolicy;
4
5impl ProjectKnowledge {
6 pub fn consolidate(
10 &mut self,
11 summary: &str,
12 session_ids: Vec<String>,
13 policy: &MemoryPolicy,
14 ) -> Result<usize, String> {
15 self.history.push(ConsolidatedInsight {
16 summary: summary.to_string(),
17 from_sessions: session_ids,
18 timestamp: chrono::Utc::now(),
19 });
20
21 let archived = crate::core::memory_capacity::reclaim_store(
22 crate::core::memory_archive::MemoryStore::History,
23 Some(&self.project_hash),
24 &mut self.history,
25 policy.knowledge.max_history,
26 policy.lifecycle.reclaim_headroom_pct,
27 policy.lifecycle.reclaim_enabled,
28 |a, b| {
29 b.timestamp
30 .cmp(&a.timestamp)
31 .then_with(|| b.summary.cmp(&a.summary))
32 },
33 )?;
34 self.updated_at = chrono::Utc::now();
35 Ok(archived.len())
36 }
37
38 pub fn format_summary(&self) -> String {
39 let mut out = String::new();
40 let current_facts: Vec<&KnowledgeFact> =
41 self.facts.iter().filter(|f| f.is_current()).collect();
42
43 if !current_facts.is_empty() {
44 out.push_str("PROJECT KNOWLEDGE:\n");
45 let mut rooms: Vec<(String, usize)> = self.list_rooms();
46 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
47
48 let total_rooms = rooms.len();
49 rooms.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT);
50
51 for (cat, _count) in rooms {
52 out.push_str(&format!(" [{cat}]\n"));
53
54 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
55 .iter()
56 .copied()
57 .filter(|f| f.category == cat)
58 .collect();
59 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
60
61 let total_in_cat = facts_in_cat.len();
62 facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT);
63
64 for f in facts_in_cat {
65 let key = crate::core::sanitize::neutralize_metadata(&f.key);
66 let val = crate::core::sanitize::neutralize_metadata(&f.value);
67 out.push_str(&format!(
68 " {}: {} (confidence: {:.0}%)\n",
69 key,
70 val,
71 f.confidence * 100.0
72 ));
73 }
74 if total_in_cat > crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT {
75 out.push_str(&format!(
76 " … +{} more\n",
77 total_in_cat - crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT
78 ));
79 }
80 }
81
82 if total_rooms > crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT {
83 out.push_str(&format!(
84 " … +{} more rooms\n",
85 total_rooms - crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT
86 ));
87 }
88 }
89
90 if !self.patterns.is_empty() {
91 out.push_str("PROJECT PATTERNS:\n");
92 let mut patterns = self.patterns.clone();
93 patterns.sort_by(|a, b| {
94 b.created_at
95 .cmp(&a.created_at)
96 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
97 .then_with(|| a.description.cmp(&b.description))
98 });
99 let total = patterns.len();
100 patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
101 for p in &patterns {
102 let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
103 let desc = crate::core::sanitize::neutralize_metadata(&p.description);
104 out.push_str(&format!(" [{ty}] {desc}\n"));
105 }
106 if total > crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT {
107 out.push_str(&format!(
108 " … +{} more\n",
109 total - crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT
110 ));
111 }
112 }
113
114 if out.is_empty() {
115 out
116 } else {
117 crate::core::sanitize::fence_content("project_knowledge", out.trim_end())
118 }
119 }
120
121 pub fn format_aaak(&self) -> String {
122 let sens = crate::core::config::Config::load().sensitivity_effective();
129 let current_facts: Vec<&KnowledgeFact> = self
130 .facts
131 .iter()
132 .filter(|f| f.is_current())
133 .filter(|f| {
134 !sens.enabled_effective()
135 || !crate::core::sensitivity::floor_blocks(
136 f.sensitivity
137 .max(crate::core::sensitivity::classify_content(&f.value)),
138 &sens,
139 )
140 })
141 .collect();
142
143 if current_facts.is_empty() && self.patterns.is_empty() {
144 return String::new();
145 }
146
147 let mut out = String::new();
148
149 let mut rooms: Vec<(String, usize)> = self.list_rooms();
150 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
151 rooms.truncate(crate::core::budgets::KNOWLEDGE_AAAK_ROOMS_LIMIT);
152
153 for (cat, _count) in rooms {
154 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
155 .iter()
156 .copied()
157 .filter(|f| f.category == cat)
158 .collect();
159 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
160 facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_AAAK_FACTS_PER_ROOM_LIMIT);
161
162 let items: Vec<String> = facts_in_cat
163 .iter()
164 .map(|f| {
165 let stars = confidence_stars(f.confidence);
166 let key = crate::core::sanitize::neutralize_metadata(&f.key);
167 let val = crate::core::sanitize::neutralize_metadata(&f.value);
168 format!("{key}={val}{stars}")
169 })
170 .collect();
171 out.push_str(&format!(
172 "{}:{}\n",
173 crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
174 items.join("|")
175 ));
176 }
177
178 if !self.patterns.is_empty() {
179 let mut patterns = self.patterns.clone();
180 patterns.sort_by(|a, b| {
181 b.created_at
182 .cmp(&a.created_at)
183 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
184 .then_with(|| a.description.cmp(&b.description))
185 });
186 patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
187 let pat_items: Vec<String> = patterns
188 .iter()
189 .map(|p| {
190 let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
191 let desc = crate::core::sanitize::neutralize_metadata(&p.description);
192 format!("{ty}.{desc}")
193 })
194 .collect();
195 out.push_str(&format!("PAT:{}\n", pat_items.join("|")));
196 }
197
198 if out.is_empty() {
199 out
200 } else {
201 crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
202 }
203 }
204
205 pub fn format_wakeup(&self) -> String {
206 let current_facts: Vec<&KnowledgeFact> = self
207 .facts
208 .iter()
209 .filter(|f| f.is_current() && f.confidence >= 0.7)
210 .collect();
211
212 if current_facts.is_empty() {
213 return String::new();
214 }
215
216 let mut top_facts: Vec<&KnowledgeFact> = current_facts;
220 top_facts.sort_by(|a, b| sort_fact_for_output(a, b));
221 top_facts.truncate(20);
222
223 let clusters = super::chunking::cluster_facts(&top_facts);
224 crate::core::sanitize::fence_content(
225 "project_facts_wakeup",
226 &super::chunking::render_chunked(&clusters),
227 )
228 }
229}