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
5pub struct AaakDelta {
7 pub content: String,
8 pub hash: String,
9 pub is_full: bool,
10 pub fact_count: usize,
11}
12
13impl ProjectKnowledge {
14 pub fn consolidate(
18 &mut self,
19 summary: &str,
20 session_ids: Vec<String>,
21 policy: &MemoryPolicy,
22 ) -> Result<usize, String> {
23 self.history.push(ConsolidatedInsight {
24 summary: summary.to_string(),
25 from_sessions: session_ids,
26 timestamp: chrono::Utc::now(),
27 });
28
29 let archived = crate::core::memory_capacity::reclaim_store(
30 crate::core::memory_archive::MemoryStore::History,
31 Some(&self.project_hash),
32 &mut self.history,
33 policy.knowledge.max_history,
34 policy.lifecycle.reclaim_headroom_pct,
35 policy.lifecycle.reclaim_enabled,
36 |a, b| {
37 b.timestamp
38 .cmp(&a.timestamp)
39 .then_with(|| b.summary.cmp(&a.summary))
40 },
41 )?;
42 self.updated_at = chrono::Utc::now();
43 Ok(archived.len())
44 }
45
46 pub fn format_summary(&self) -> String {
47 let mut out = String::new();
48 let current_facts: Vec<&KnowledgeFact> =
49 self.facts.iter().filter(|f| f.is_current()).collect();
50
51 if !current_facts.is_empty() {
52 out.push_str("PROJECT KNOWLEDGE:\n");
53 let mut rooms: Vec<(String, usize)> = self.list_rooms();
54 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
55
56 let total_rooms = rooms.len();
57 rooms.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT);
58
59 for (cat, _count) in rooms {
60 out.push_str(&format!(" [{cat}]\n"));
61
62 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
63 .iter()
64 .copied()
65 .filter(|f| f.category == cat)
66 .collect();
67 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
68
69 let total_in_cat = facts_in_cat.len();
70 facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT);
71
72 for f in facts_in_cat {
73 let key = crate::core::sanitize::neutralize_metadata(&f.key);
74 let val = crate::core::sanitize::neutralize_metadata(&f.value);
75 out.push_str(&format!(
76 " {}: {} (confidence: {:.0}%)\n",
77 key,
78 val,
79 f.confidence * 100.0
80 ));
81 }
82 if total_in_cat > crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT {
83 out.push_str(&format!(
84 " … +{} more\n",
85 total_in_cat - crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT
86 ));
87 }
88 }
89
90 if total_rooms > crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT {
91 out.push_str(&format!(
92 " … +{} more rooms\n",
93 total_rooms - crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT
94 ));
95 }
96 }
97
98 if !self.patterns.is_empty() {
99 out.push_str("PROJECT PATTERNS:\n");
100 let mut patterns = self.patterns.clone();
101 patterns.sort_by(|a, b| {
102 b.created_at
103 .cmp(&a.created_at)
104 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
105 .then_with(|| a.description.cmp(&b.description))
106 });
107 let total = patterns.len();
108 patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
109 for p in &patterns {
110 let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
111 let desc = crate::core::sanitize::neutralize_metadata(&p.description);
112 out.push_str(&format!(" [{ty}] {desc}\n"));
113 }
114 if total > crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT {
115 out.push_str(&format!(
116 " … +{} more\n",
117 total - crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT
118 ));
119 }
120 }
121
122 if out.is_empty() {
123 out
124 } else {
125 crate::core::sanitize::fence_content("project_knowledge", out.trim_end())
126 }
127 }
128
129 pub fn format_aaak(&self) -> String {
130 let sens = crate::core::config::Config::load().sensitivity_effective();
137 let current_facts: Vec<&KnowledgeFact> = self
138 .facts
139 .iter()
140 .filter(|f| f.is_current())
141 .filter(|f| {
142 !sens.enabled_effective()
143 || !crate::core::sensitivity::floor_blocks(
144 f.sensitivity
145 .max(crate::core::sensitivity::classify_content(&f.value)),
146 &sens,
147 )
148 })
149 .collect();
150
151 if current_facts.is_empty() && self.patterns.is_empty() {
152 return String::new();
153 }
154
155 let mut out = String::new();
156
157 let mut rooms: Vec<(String, usize)> = self.list_rooms();
158 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
159 rooms.truncate(crate::core::budgets::KNOWLEDGE_AAAK_ROOMS_LIMIT);
160
161 for (cat, _count) in rooms {
162 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
163 .iter()
164 .copied()
165 .filter(|f| f.category == cat)
166 .collect();
167 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
168 facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_AAAK_FACTS_PER_ROOM_LIMIT);
169
170 let items: Vec<String> = facts_in_cat
171 .iter()
172 .map(|f| {
173 let stars = confidence_stars(f.confidence);
174 let key = crate::core::sanitize::neutralize_metadata(&f.key);
175 let val = crate::core::sanitize::neutralize_metadata(&f.value);
176 format!("{key}={val}{stars}")
177 })
178 .collect();
179 out.push_str(&format!(
180 "{}:{}\n",
181 crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
182 items.join("|")
183 ));
184 }
185
186 if !self.patterns.is_empty() {
187 let mut patterns = self.patterns.clone();
188 patterns.sort_by(|a, b| {
189 b.created_at
190 .cmp(&a.created_at)
191 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
192 .then_with(|| a.description.cmp(&b.description))
193 });
194 patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
195 let pat_items: Vec<String> = patterns
196 .iter()
197 .map(|p| {
198 let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
199 let desc = crate::core::sanitize::neutralize_metadata(&p.description);
200 format!("{ty}.{desc}")
201 })
202 .collect();
203 out.push_str(&format!("PAT:{}\n", pat_items.join("|")));
204 }
205
206 if out.is_empty() {
207 out
208 } else {
209 crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
210 }
211 }
212
213 pub fn format_aaak_delta(&self, last_hash: Option<&str>) -> AaakDelta {
214 let full = self.format_aaak();
215 let fact_count = self.facts.iter().filter(|f| f.is_current()).count();
216 if full.is_empty() {
217 return AaakDelta {
218 content: String::new(),
219 hash: String::new(),
220 is_full: false,
221 fact_count,
222 };
223 }
224 let hash = blake3::hash(full.as_bytes()).to_hex().to_string();
225 let is_full = last_hash.is_none_or(|prev| prev != hash);
226 let content = if is_full {
227 full
228 } else {
229 format!(
230 "[AAAK unchanged — {fact_count} facts, hash {}]",
231 &hash[..12]
232 )
233 };
234 AaakDelta {
235 content,
236 hash,
237 is_full,
238 fact_count,
239 }
240 }
241
242 pub fn format_aaak_budgeted(&self, token_budget: usize) -> String {
243 let sens = crate::core::config::Config::load().sensitivity_effective();
244 let current_facts: Vec<&KnowledgeFact> = self
245 .facts
246 .iter()
247 .filter(|f| f.is_current())
248 .filter(|f| {
249 !sens.enabled_effective()
250 || !crate::core::sensitivity::floor_blocks(
251 f.sensitivity
252 .max(crate::core::sensitivity::classify_content(&f.value)),
253 &sens,
254 )
255 })
256 .collect();
257 if current_facts.is_empty() && self.patterns.is_empty() {
258 return String::new();
259 }
260 let mut out = String::new();
261 let mut tokens_used: usize = 0;
262 let mut rooms_omitted: usize = 0;
263 let mut facts_omitted: usize = 0;
264 let mut rooms: Vec<(String, usize)> = self.list_rooms();
265 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
266 for (cat, _count) in &rooms {
267 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
268 .iter()
269 .copied()
270 .filter(|f| &f.category == cat)
271 .collect();
272 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
273 let items: Vec<String> = facts_in_cat
274 .iter()
275 .map(|f| {
276 let stars = confidence_stars(f.confidence);
277 let key = crate::core::sanitize::neutralize_metadata(&f.key);
278 let val = crate::core::sanitize::neutralize_metadata(&f.value);
279 format!("{key}={val}{stars}")
280 })
281 .collect();
282 let line = format!(
283 "{}:{}\n",
284 crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
285 items.join("|")
286 );
287 let line_tokens = crate::core::tokens::count_tokens(&line);
288 if tokens_used + line_tokens > token_budget {
289 rooms_omitted += 1;
290 facts_omitted += facts_in_cat.len();
291 continue;
292 }
293 out.push_str(&line);
294 tokens_used += line_tokens;
295 }
296 if rooms_omitted > 0 {
297 out.push_str(&format!(
298 "[+{facts_omitted} facts in {rooms_omitted} rooms omitted — budget {token_budget} tokens]\n"
299 ));
300 }
301 if out.is_empty() {
302 out
303 } else {
304 crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
305 }
306 }
307
308 pub fn format_wakeup(&self) -> String {
309 let current_facts: Vec<&KnowledgeFact> = self
310 .facts
311 .iter()
312 .filter(|f| f.is_current() && f.confidence >= 0.7)
313 .collect();
314
315 if current_facts.is_empty() {
316 return String::new();
317 }
318
319 let mut top_facts: Vec<&KnowledgeFact> = current_facts;
323 top_facts.sort_by(|a, b| sort_fact_for_output(a, b));
324 top_facts.truncate(20);
325
326 let clusters = super::chunking::cluster_facts(&top_facts);
327 crate::core::sanitize::fence_content(
328 "project_facts_wakeup",
329 &super::chunking::render_chunked(&clusters),
330 )
331 }
332}