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        // #212 — pre-prompt sensitivity floor: never inject facts whose stored or
105        // freshly-classified sensitivity meets/exceeds the configured floor. The
106        // short-circuit means zero classification cost when disabled (default).
107        let sens = crate::core::config::Config::load().sensitivity;
108        let current_facts: Vec<&KnowledgeFact> = self
109            .facts
110            .iter()
111            .filter(|f| f.is_current())
112            .filter(|f| {
113                !sens.enabled_effective()
114                    || !crate::core::sensitivity::floor_blocks(
115                        f.sensitivity
116                            .max(crate::core::sensitivity::classify_content(&f.value)),
117                        &sens,
118                    )
119            })
120            .collect();
121
122        if current_facts.is_empty() && self.patterns.is_empty() {
123            return String::new();
124        }
125
126        let mut out = String::new();
127
128        let mut rooms: Vec<(String, usize)> = self.list_rooms();
129        rooms.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
130        rooms.truncate(crate::core::budgets::KNOWLEDGE_AAAK_ROOMS_LIMIT);
131
132        for (cat, _count) in rooms {
133            let mut facts_in_cat: Vec<&KnowledgeFact> = current_facts
134                .iter()
135                .copied()
136                .filter(|f| f.category == cat)
137                .collect();
138            facts_in_cat.sort_by(|a, b| sort_fact_for_output(a, b));
139            facts_in_cat.truncate(crate::core::budgets::KNOWLEDGE_AAAK_FACTS_PER_ROOM_LIMIT);
140
141            let items: Vec<String> = facts_in_cat
142                .iter()
143                .map(|f| {
144                    let stars = confidence_stars(f.confidence);
145                    let key = crate::core::sanitize::neutralize_metadata(&f.key);
146                    let val = crate::core::sanitize::neutralize_metadata(&f.value);
147                    format!("{key}={val}{stars}")
148                })
149                .collect();
150            out.push_str(&format!(
151                "{}:{}\n",
152                crate::core::sanitize::neutralize_metadata(&cat.to_uppercase()),
153                items.join("|")
154            ));
155        }
156
157        if !self.patterns.is_empty() {
158            let mut patterns = self.patterns.clone();
159            patterns.sort_by(|a, b| {
160                b.created_at
161                    .cmp(&a.created_at)
162                    .then_with(|| a.pattern_type.cmp(&b.pattern_type))
163                    .then_with(|| a.description.cmp(&b.description))
164            });
165            patterns.truncate(crate::core::budgets::KNOWLEDGE_PATTERNS_LIMIT);
166            let pat_items: Vec<String> = patterns
167                .iter()
168                .map(|p| {
169                    let ty = crate::core::sanitize::neutralize_metadata(&p.pattern_type);
170                    let desc = crate::core::sanitize::neutralize_metadata(&p.description);
171                    format!("{ty}.{desc}")
172                })
173                .collect();
174            out.push_str(&format!("PAT:{}\n", pat_items.join("|")));
175        }
176
177        if out.is_empty() {
178            out
179        } else {
180            crate::core::sanitize::fence_content("project_memory_aaak", out.trim_end())
181        }
182    }
183
184    pub fn format_wakeup(&self) -> String {
185        let current_facts: Vec<&KnowledgeFact> = self
186            .facts
187            .iter()
188            .filter(|f| f.is_current() && f.confidence >= 0.7)
189            .collect();
190
191        if current_facts.is_empty() {
192            return String::new();
193        }
194
195        // Theta-gamma chunking (#543): salience-ordered top-K facts grouped
196        // into 4±1-sized thematic chunks; shared headers amortize category
197        // prefixes (token savings) and prime related facts (recall).
198        let mut top_facts: Vec<&KnowledgeFact> = current_facts;
199        top_facts.sort_by(|a, b| sort_fact_for_output(a, b));
200        top_facts.truncate(20);
201
202        let clusters = super::chunking::cluster_facts(&top_facts);
203        crate::core::sanitize::fence_content(
204            "project_facts_wakeup",
205            &super::chunking::render_chunked(&clusters),
206        )
207    }
208}