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 }];
310
311 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
312 assert_eq!(result.skipped, 1);
313 assert_eq!(result.added, 0);
314 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
315 }
316
317 #[test]
318 fn import_replace_existing() {
319 let policy = default_policy();
320 let mut k = ProjectKnowledge::new("/tmp/test");
321 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
322
323 let incoming = vec![KnowledgeFact {
324 category: "arch".into(),
325 key: "db".into(),
326 value: "MySQL".into(),
327 source_session: "import".into(),
328 confidence: 0.8,
329 created_at: Utc::now(),
330 last_confirmed: Utc::now(),
331 retrieval_count: 0,
332 last_retrieved: None,
333 valid_from: Some(Utc::now()),
334 valid_until: None,
335 supersedes: None,
336 confirmation_count: 1,
337 feedback_up: 0,
338 feedback_down: 0,
339 last_feedback: None,
340 privacy: FactPrivacy::default(),
341 imported_from: None,
342 }];
343
344 let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
345 assert_eq!(result.replaced, 1);
346 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
347 assert_eq!(current.len(), 1);
348 assert_eq!(current[0].value, "MySQL");
349 }
350
351 #[test]
352 fn import_adds_new_facts() {
353 let policy = default_policy();
354 let mut k = ProjectKnowledge::new("/tmp/test");
355 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
356
357 let incoming = vec![KnowledgeFact {
358 category: "security".into(),
359 key: "auth".into(),
360 value: "JWT".into(),
361 source_session: "import".into(),
362 confidence: 0.9,
363 created_at: Utc::now(),
364 last_confirmed: Utc::now(),
365 retrieval_count: 0,
366 last_retrieved: None,
367 valid_from: Some(Utc::now()),
368 valid_until: None,
369 supersedes: None,
370 confirmation_count: 1,
371 feedback_up: 0,
372 feedback_down: 0,
373 last_feedback: None,
374 privacy: FactPrivacy::default(),
375 imported_from: None,
376 }];
377
378 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
379 assert_eq!(result.added, 1);
380 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
381 }
382
383 #[test]
384 fn parse_simple_json_array() {
385 let data = r#"[
386 {"category": "arch", "key": "db", "value": "PostgreSQL"},
387 {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
388 ]"#;
389 let facts = parse_import_data(data).unwrap();
390 assert_eq!(facts.len(), 2);
391 assert_eq!(facts[0].category, "arch");
392 assert_eq!(facts[1].confidence, 0.9);
393 }
394
395 #[test]
396 fn parse_jsonl_format() {
397 let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
398 {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
399 let facts = parse_import_data(data).unwrap();
400 assert_eq!(facts.len(), 2);
401 }
402
403 #[test]
404 fn export_simple_only_current() {
405 let policy = default_policy();
406 let mut k = ProjectKnowledge::new("/tmp/test");
407 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
408 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
409
410 let exported = k.export_simple();
411 assert_eq!(exported.len(), 1);
412 assert_eq!(exported[0].value, "MySQL");
413 }
414
415 #[test]
416 fn import_merge_parse() {
417 assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
418 assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
419 assert_eq!(
420 ImportMerge::parse("skip-existing"),
421 Some(ImportMerge::SkipExisting)
422 );
423 assert_eq!(
424 ImportMerge::parse("skip_existing"),
425 Some(ImportMerge::SkipExisting)
426 );
427 assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
428 assert!(ImportMerge::parse("invalid").is_none());
429 }
430}