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}