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    /// Record one consolidated insight, then losslessly reclaim history to its
7    /// capacity headroom. Returns the number of older insights archived (0 when
8    /// under cap). Was a hard `drain` that dropped old history permanently (#995).
9    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        // #212 — pre-prompt sensitivity floor: never inject facts whose stored or
123        // freshly-classified sensitivity meets/exceeds the configured floor. The
124        // short-circuit means zero classification cost when disabled (default).
125        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        // Theta-gamma chunking (#543): salience-ordered top-K facts grouped
214        // into 4±1-sized thematic chunks; shared headers amortize category
215        // prefixes (token savings) and prime related facts (recall).
216        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}