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 ) -> usize {
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 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;
126 let current_facts: Vec<&KnowledgeFact> = self
127 .facts
128 .iter()
129 .filter(|f| f.is_current())
130 .filter(|f| {
131 !sens.enabled_effective()
132 || !crate::core::sensitivity::floor_blocks(
133 f.sensitivity
134 .max(crate::core::sensitivity::classify_content(&f.value)),
135 &sens,
136 )
137 })
138 .collect();
139
140 if current_facts.is_empty() && self.patterns.is_empty() {
141 return String::new();
142 }
143
144 let mut out = String::new();
145
146 let mut rooms: Vec<(String, usize)> = self.list_rooms();
147 rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
148 rooms.truncate(crate::core::budgets::KNOWLEDGE_AAAK_ROOMS_LIMIT);
149
150 for (cat, _count) in rooms {
151 let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
152 .iter()
153 .copied()
154 .filter(|f| f.category == cat)
155 .collect();
156 facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
157 facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_AAAK_FACTS_PER_ROOM_LIMIT);
158
159 let items: Vec<String> = facts_in_cat
160 .iter()
161 .map(|f| {
162 let stars = confidence_stars(f.confidence);
163 let key = crate::core::sanitize::neutralize_metadata(&f.key);
164 let val = crate::core::sanitize::neutralize_metadata(&f.value);
165 format!("{key}={val}{stars}")
166 })
167 .collect();
168 out.push_str(&format!(
169 "{}:{}\n",
170 crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
171 items.join("|")
172 ));
173 }
174
175 if !self.patterns.is_empty() {
176 let mut patterns = self.patterns.clone();
177 patterns.sort_by(|a, b| {
178 b.created_at
179 .cmp(&a.created_at)
180 .then_with(|| a.pattern_type.cmp(&b.pattern_type))
181 .then_with(|| a.description.cmp(&b.description))
182 });
183 patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
184 let pat_items: Vec<String> = patterns
185 .iter()
186 .map(|p| {
187 let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
188 let desc = crate::core::sanitize::neutralize_metadata(&p.description);
189 format!("{ty}.{desc}")
190 })
191 .collect();
192 out.push_str(&format!("PAT:{}\n", pat_items.join("|")));
193 }
194
195 if out.is_empty() {
196 out
197 } else {
198 crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
199 }
200 }
201
202 pub fn format_wakeup(&self) -> String {
203 let current_facts: Vec<&KnowledgeFact> = self
204 .facts
205 .iter()
206 .filter(|f| f.is_current() && f.confidence >= 0.7)
207 .collect();
208
209 if current_facts.is_empty() {
210 return String::new();
211 }
212
213 let mut top_facts: Vec<&KnowledgeFact> = current_facts;
217 top_facts.sort_by(|a, b| sort_fact_for_output(a, b));
218 top_facts.truncate(20);
219
220 let clusters = super::chunking::cluster_facts(&top_facts);
221 crate::core::sanitize::fence_content(
222 "project_facts_wakeup",
223 &super::chunking::render_chunked(&clusters),
224 )
225 }
226}