Skip to main content

lean_ctx/core/knowledge/
format.rs

1use 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(&mut self, summary: &str, session_ids: Vec<String>, policy: &MemoryPolicy) {
7        self.history.push(ConsolidatedInsight {
8            summary: summary.to_string(),
9            from_sessions: session_ids,
10            timestamp: chrono::Utc::now(),
11        });
12
13        if self.history.len() > policy.knowledge.max_history {
14            self.history
15                .drain(0..self.history.len() - policy.knowledge.max_history);
16        }
17        self.updated_at = chrono::Utc::now();
18    }
19
20    pub fn format_summary(&self) -> String {
21        let mut out = String::new();
22        let current_facts: Vec<&KnowledgeFact> =
23            self.facts.iter().filter(|f| f.is_current()).collect();
24
25        if !current_facts.is_empty() {
26            out.push_str("PROJECT KNOWLEDGE:\n");
27            let mut rooms: Vec<(String, usize)> = self.list_rooms();
28            rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
29
30            let total_rooms = rooms.len();
31            rooms.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT);
32
33            for (cat, _count) in rooms {
34                out.push_str(&format!("  [{cat}]\n"));
35
36                let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
37                    .iter()
38                    .copied()
39                    .filter(|f| f.category == cat)
40                    .collect();
41                facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
42
43                let total_in_cat = facts_in_cat.len();
44                facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT);
45
46                for f in facts_in_cat {
47                    let key = crate::core::sanitize::neutralize_metadata(&f.key);
48                    let val = crate::core::sanitize::neutralize_metadata(&f.value);
49                    out.push_str(&format!(
50                        "    {}: {} (confidence: {:.0}%)\n",
51                        key,
52                        val,
53                        f.confidence * 100.0
54                    ));
55                }
56                if total_in_cat > crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT {
57                    out.push_str(&format!(
58                        "    … +{} more\n",
59                        total_in_cat - crate::core::budgets::KNOWLEDGE_SUMMARY_FACTS_PER_ROOM_LIMIT
60                    ));
61                }
62            }
63
64            if total_rooms > crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT {
65                out.push_str(&format!(
66                    "  … +{} more rooms\n",
67                    total_rooms - crate::core::budgets::KNOWLEDGE_SUMMARY_ROOMS_LIMIT
68                ));
69            }
70        }
71
72        if !self.patterns.is_empty() {
73            out.push_str("PROJECT PATTERNS:\n");
74            let mut patterns = self.patterns.clone();
75            patterns.sort_by(|a, b| {
76                b.created_at
77                    .cmp(&a.created_at)
78                    .then_with(|| a.pattern_type.cmp(&b.pattern_type))
79                    .then_with(|| a.description.cmp(&b.description))
80            });
81            let total = patterns.len();
82            patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
83            for p in &patterns {
84                let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
85                let desc = crate::core::sanitize::neutralize_metadata(&p.description);
86                out.push_str(&format!("  [{ty}] {desc}\n"));
87            }
88            if total > crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT {
89                out.push_str(&format!(
90                    "  … +{} more\n",
91                    total - crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT
92                ));
93            }
94        }
95
96        if out.is_empty() {
97            out
98        } else {
99            crate::core::sanitize::fence_content("project_knowledge", out.trim_end())
100        }
101    }
102
103    pub fn format_aaak(&self) -> String {
104        let current_facts: Vec<&KnowledgeFact> =
105            self.facts.iter().filter(|f| f.is_current()).collect();
106
107        if current_facts.is_empty() && self.patterns.is_empty() {
108            return String::new();
109        }
110
111        let mut out = String::new();
112
113        let mut rooms: Vec<(String, usize)> = self.list_rooms();
114        rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
115        rooms.truncate(crate::core::budgets::KNOWLEDGE_AAAK_ROOMS_LIMIT);
116
117        for (cat, _count) in rooms {
118            let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
119                .iter()
120                .copied()
121                .filter(|f| f.category == cat)
122                .collect();
123            facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
124            facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_AAAK_FACTS_PER_ROOM_LIMIT);
125
126            let items: Vec<String> = facts_in_cat
127                .iter()
128                .map(|f| {
129                    let stars = confidence_stars(f.confidence);
130                    let key = crate::core::sanitize::neutralize_metadata(&f.key);
131                    let val = crate::core::sanitize::neutralize_metadata(&f.value);
132                    format!("{key}={val}{stars}")
133                })
134                .collect();
135            out.push_str(&format!(
136                "{}:{}\n",
137                crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
138                items.join("|")
139            ));
140        }
141
142        if !self.patterns.is_empty() {
143            let mut patterns = self.patterns.clone();
144            patterns.sort_by(|a, b| {
145                b.created_at
146                    .cmp(&a.created_at)
147                    .then_with(|| a.pattern_type.cmp(&b.pattern_type))
148                    .then_with(|| a.description.cmp(&b.description))
149            });
150            patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
151            let pat_items: Vec<String> = patterns
152                .iter()
153                .map(|p| {
154                    let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
155                    let desc = crate::core::sanitize::neutralize_metadata(&p.description);
156                    format!("{ty}.{desc}")
157                })
158                .collect();
159            out.push_str(&format!("PAT:{}\n", pat_items.join("|")));
160        }
161
162        if out.is_empty() {
163            out
164        } else {
165            crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
166        }
167    }
168
169    pub fn format_wakeup(&self) -> String {
170        let current_facts: Vec<&KnowledgeFact> = self
171            .facts
172            .iter()
173            .filter(|f| f.is_current() && f.confidence >= 0.7)
174            .collect();
175
176        if current_facts.is_empty() {
177            return String::new();
178        }
179
180        let mut top_facts: Vec<&KnowledgeFact> = current_facts;
181        top_facts.sort_by(|a, b| sort_fact_for_output(a, b));
182        top_facts.truncate(10);
183
184        let items: Vec<String> = top_facts
185            .iter()
186            .map(|f| {
187                let cat = crate::core::sanitize::neutralize_metadata(&f.category);
188                let key = crate::core::sanitize::neutralize_metadata(&f.key);
189                let val = crate::core::sanitize::neutralize_metadata(&f.value);
190                format!("{cat}/{key}={val}")
191            })
192            .collect();
193
194        crate::core::sanitize::fence_content(
195            "project_facts_wakeup",
196            &format!("FACTS:{}", items.join("|")),
197        )
198    }
199}