Skip to main content

lean_ctx/core/knowledge/
mod.rs

1mod core;
2mod fact;
3mod format;
4mod import_export;
5mod persist;
6mod query;
7mod ranking;
8mod types;
9
10pub use import_export::{parse_import_data, ImportMerge, ImportResult, SimpleFactEntry};
11pub use types::*;
12
13#[cfg(test)]
14mod tests {
15    use super::*;
16    use crate::core::memory_boundary::FactPrivacy;
17    use crate::core::memory_policy::MemoryPolicy;
18    use chrono::Utc;
19
20    fn default_policy() -> MemoryPolicy {
21        MemoryPolicy::default()
22    }
23
24    #[test]
25    fn remember_and_recall() {
26        let policy = default_policy();
27        let mut k = ProjectKnowledge::new("/tmp/test-project");
28        k.remember(
29            "architecture",
30            "auth",
31            "JWT RS256",
32            "session-1",
33            0.9,
34            &policy,
35        );
36        k.remember("api", "rate-limit", "100/min", "session-1", 0.8, &policy);
37
38        let results = k.recall("auth");
39        assert_eq!(results.len(), 1);
40        assert_eq!(results[0].value, "JWT RS256");
41
42        let results = k.recall("api rate");
43        assert_eq!(results.len(), 1);
44        assert_eq!(results[0].key, "rate-limit");
45    }
46
47    #[test]
48    fn upsert_existing_fact() {
49        let policy = default_policy();
50        let mut k = ProjectKnowledge::new("/tmp/test");
51        k.remember("arch", "db", "PostgreSQL", "s1", 0.7, &policy);
52        k.remember(
53            "arch",
54            "db",
55            "PostgreSQL 16 with pgvector",
56            "s2",
57            0.95,
58            &policy,
59        );
60
61        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
62        assert_eq!(current.len(), 1);
63        assert_eq!(current[0].value, "PostgreSQL 16 with pgvector");
64    }
65
66    #[test]
67    fn contradiction_detection() {
68        let policy = default_policy();
69        let mut k = ProjectKnowledge::new("/tmp/test");
70        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
71        k.facts[0].confirmation_count = 3;
72
73        let contradiction = k.check_contradiction("arch", "db", "MySQL", &policy);
74        assert!(contradiction.is_some());
75        let c = contradiction.unwrap();
76        assert_eq!(c.severity, ContradictionSeverity::High);
77    }
78
79    #[test]
80    fn temporal_validity() {
81        let policy = default_policy();
82        let mut k = ProjectKnowledge::new("/tmp/test");
83        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
84        k.facts[0].confirmation_count = 3;
85
86        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
87
88        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
89        assert_eq!(current.len(), 1);
90        assert_eq!(current[0].value, "MySQL");
91
92        let all_db: Vec<_> = k.facts.iter().filter(|f| f.key == "db").collect();
93        assert_eq!(all_db.len(), 2);
94    }
95
96    #[test]
97    fn confirmation_count() {
98        let policy = default_policy();
99        let mut k = ProjectKnowledge::new("/tmp/test");
100        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
101        assert_eq!(k.facts[0].confirmation_count, 1);
102
103        k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
104        assert_eq!(k.facts[0].confirmation_count, 2);
105    }
106
107    #[test]
108    fn remove_fact() {
109        let policy = default_policy();
110        let mut k = ProjectKnowledge::new("/tmp/test");
111        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
112        assert!(k.remove_fact("arch", "db"));
113        assert!(k.facts.is_empty());
114        assert!(!k.remove_fact("arch", "db"));
115    }
116
117    #[test]
118    fn list_rooms() {
119        let policy = default_policy();
120        let mut k = ProjectKnowledge::new("/tmp/test");
121        k.remember("architecture", "auth", "JWT", "s1", 0.9, &policy);
122        k.remember("architecture", "db", "PG", "s1", 0.9, &policy);
123        k.remember("deploy", "host", "AWS", "s1", 0.8, &policy);
124
125        let rooms = k.list_rooms();
126        assert_eq!(rooms.len(), 2);
127    }
128
129    #[test]
130    fn aaak_format() {
131        let policy = default_policy();
132        let mut k = ProjectKnowledge::new("/tmp/test");
133        k.remember("architecture", "auth", "JWT RS256", "s1", 0.95, &policy);
134        k.remember("architecture", "db", "PostgreSQL", "s1", 0.7, &policy);
135
136        let aaak = k.format_aaak();
137        assert!(aaak.contains("ARCHITECTURE:"));
138        assert!(aaak.contains("auth=JWT RS256"));
139    }
140
141    #[test]
142    fn consolidate_history() {
143        let policy = default_policy();
144        let mut k = ProjectKnowledge::new("/tmp/test");
145        k.consolidate(
146            "Migrated from REST to GraphQL",
147            vec!["s1".into(), "s2".into()],
148            &policy,
149        );
150        assert_eq!(k.history.len(), 1);
151        assert_eq!(k.history[0].from_sessions.len(), 2);
152    }
153
154    #[test]
155    fn format_summary_output() {
156        let policy = default_policy();
157        let mut k = ProjectKnowledge::new("/tmp/test");
158        k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
159        k.add_pattern(
160            "naming",
161            "snake_case for functions",
162            vec!["get_user()".into()],
163            "s1",
164            &policy,
165        );
166        let summary = k.format_summary();
167        assert!(summary.contains("PROJECT KNOWLEDGE:"));
168        assert!(summary.contains("auth: JWT RS256"));
169        assert!(summary.contains("PROJECT PATTERNS:"));
170    }
171
172    #[test]
173    fn temporal_recall_at_time() {
174        let policy = default_policy();
175        let mut k = ProjectKnowledge::new("/tmp/test");
176        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
177        k.facts[0].confirmation_count = 3;
178
179        let before_change = Utc::now();
180        std::thread::sleep(std::time::Duration::from_millis(10));
181
182        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
183
184        let results = k.recall_at_time("db", before_change);
185        assert_eq!(results.len(), 1);
186        assert_eq!(results[0].value, "PostgreSQL");
187
188        let results_now = k.recall_at_time("db", Utc::now());
189        assert_eq!(results_now.len(), 1);
190        assert_eq!(results_now[0].value, "MySQL");
191    }
192
193    #[test]
194    fn timeline_shows_history() {
195        let policy = default_policy();
196        let mut k = ProjectKnowledge::new("/tmp/test");
197        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
198        k.facts[0].confirmation_count = 3;
199        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
200
201        let timeline = k.timeline("arch");
202        assert_eq!(timeline.len(), 2);
203        assert!(!timeline[0].is_current());
204        assert!(timeline[1].is_current());
205    }
206
207    #[test]
208    fn wakeup_format() {
209        let policy = default_policy();
210        let mut k = ProjectKnowledge::new("/tmp/test");
211        k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
212        k.remember("arch", "db", "PG", "s1", 0.8, &policy);
213
214        let wakeup = k.format_wakeup();
215        assert!(wakeup.contains("FACTS:"));
216        assert!(wakeup.contains("arch/auth=JWT"));
217        assert!(wakeup.contains("arch/db=PG"));
218    }
219
220    #[test]
221    fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
222        let policy = default_policy();
223        let mut k = ProjectKnowledge::new("/tmp/test");
224        k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
225        k.remember("decision", "d1", "important", "s1", 0.85, &policy);
226
227        let wakeup = k.format_wakeup();
228        let items = wakeup
229            .strip_prefix("FACTS:")
230            .unwrap_or(&wakeup)
231            .split('|')
232            .collect::<Vec<_>>();
233        assert!(
234            items
235                .first()
236                .is_some_and(|s| s.contains("decision/d1=important")),
237            "expected decision first in wakeup: {wakeup}"
238        );
239    }
240
241    #[test]
242    fn low_confidence_contradiction() {
243        let policy = default_policy();
244        let mut k = ProjectKnowledge::new("/tmp/test");
245        k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
246
247        let c = k.check_contradiction("arch", "db", "MySQL", &policy);
248        assert!(c.is_some());
249        assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
250    }
251
252    #[test]
253    fn no_contradiction_for_same_value() {
254        let policy = default_policy();
255        let mut k = ProjectKnowledge::new("/tmp/test");
256        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
257
258        let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
259        assert!(c.is_none());
260    }
261
262    #[test]
263    fn no_contradiction_for_similar_values() {
264        let policy = default_policy();
265        let mut k = ProjectKnowledge::new("/tmp/test");
266        k.remember(
267            "arch",
268            "db",
269            "PostgreSQL 16 production database server",
270            "s1",
271            0.95,
272            &policy,
273        );
274
275        let c = k.check_contradiction(
276            "arch",
277            "db",
278            "PostgreSQL 16 production database server config",
279            &policy,
280        );
281        assert!(c.is_none());
282    }
283
284    #[test]
285    fn import_skip_existing() {
286        let policy = default_policy();
287        let mut k = ProjectKnowledge::new("/tmp/test");
288        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
289
290        let incoming = vec![KnowledgeFact {
291            category: "arch".into(),
292            key: "db".into(),
293            value: "MySQL".into(),
294            source_session: "import".into(),
295            confidence: 0.8,
296            created_at: Utc::now(),
297            last_confirmed: Utc::now(),
298            retrieval_count: 0,
299            last_retrieved: None,
300            valid_from: Some(Utc::now()),
301            valid_until: None,
302            supersedes: None,
303            confirmation_count: 1,
304            feedback_up: 0,
305            feedback_down: 0,
306            last_feedback: None,
307            privacy: FactPrivacy::default(),
308            imported_from: None,
309            archetype: KnowledgeArchetype::default(),
310            fidelity: None,
311        }];
312
313        let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
314        assert_eq!(result.skipped, 1);
315        assert_eq!(result.added, 0);
316        assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
317    }
318
319    #[test]
320    fn import_replace_existing() {
321        let policy = default_policy();
322        let mut k = ProjectKnowledge::new("/tmp/test");
323        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
324
325        let incoming = vec![KnowledgeFact {
326            category: "arch".into(),
327            key: "db".into(),
328            value: "MySQL".into(),
329            source_session: "import".into(),
330            confidence: 0.8,
331            created_at: Utc::now(),
332            last_confirmed: Utc::now(),
333            retrieval_count: 0,
334            last_retrieved: None,
335            valid_from: Some(Utc::now()),
336            valid_until: None,
337            supersedes: None,
338            confirmation_count: 1,
339            feedback_up: 0,
340            feedback_down: 0,
341            last_feedback: None,
342            privacy: FactPrivacy::default(),
343            imported_from: None,
344            archetype: KnowledgeArchetype::default(),
345            fidelity: None,
346        }];
347
348        let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
349        assert_eq!(result.replaced, 1);
350        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
351        assert_eq!(current.len(), 1);
352        assert_eq!(current[0].value, "MySQL");
353    }
354
355    #[test]
356    fn import_adds_new_facts() {
357        let policy = default_policy();
358        let mut k = ProjectKnowledge::new("/tmp/test");
359        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
360
361        let incoming = vec![KnowledgeFact {
362            category: "security".into(),
363            key: "auth".into(),
364            value: "JWT".into(),
365            source_session: "import".into(),
366            confidence: 0.9,
367            created_at: Utc::now(),
368            last_confirmed: Utc::now(),
369            retrieval_count: 0,
370            last_retrieved: None,
371            valid_from: Some(Utc::now()),
372            valid_until: None,
373            supersedes: None,
374            confirmation_count: 1,
375            feedback_up: 0,
376            feedback_down: 0,
377            last_feedback: None,
378            privacy: FactPrivacy::default(),
379            imported_from: None,
380            archetype: KnowledgeArchetype::default(),
381            fidelity: None,
382        }];
383
384        let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
385        assert_eq!(result.added, 1);
386        assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
387    }
388
389    #[test]
390    fn parse_simple_json_array() {
391        let data = r#"[
392            {"category": "arch", "key": "db", "value": "PostgreSQL"},
393            {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
394        ]"#;
395        let facts = parse_import_data(data).unwrap();
396        assert_eq!(facts.len(), 2);
397        assert_eq!(facts[0].category, "arch");
398        assert_eq!(facts[1].confidence, 0.9);
399    }
400
401    #[test]
402    fn parse_jsonl_format() {
403        let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
404                    {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
405        let facts = parse_import_data(data).unwrap();
406        assert_eq!(facts.len(), 2);
407    }
408
409    #[test]
410    fn export_simple_only_current() {
411        let policy = default_policy();
412        let mut k = ProjectKnowledge::new("/tmp/test");
413        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
414        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
415
416        let exported = k.export_simple();
417        assert_eq!(exported.len(), 1);
418        assert_eq!(exported[0].value, "MySQL");
419    }
420
421    #[test]
422    fn import_merge_parse() {
423        assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
424        assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
425        assert_eq!(
426            ImportMerge::parse("skip-existing"),
427            Some(ImportMerge::SkipExisting)
428        );
429        assert_eq!(
430            ImportMerge::parse("skip_existing"),
431            Some(ImportMerge::SkipExisting)
432        );
433        assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
434        assert!(ImportMerge::parse("invalid").is_none());
435    }
436}